Migrate problems panel to React (#1532)
* Replace default angular app config editor * Problems panel: migrate module to ts * Problems panel options editor to react * Problems panel react WIP * Fix explore button * Problems panel alert list layout WIP * Refactor * Minor tweaks on panel options * remove outdated tests * Update typescript * Draft for tag event handling * Remove unused files
This commit is contained in:
@@ -1 +0,0 @@
|
||||
<h3 class="page-heading">Zabbix Plugin Config</h3>
|
||||
@@ -1,4 +0,0 @@
|
||||
export class ZabbixAppConfigCtrl {
|
||||
constructor() { }
|
||||
}
|
||||
ZabbixAppConfigCtrl.templateUrl = 'app_config_ctrl/config.html';
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { ExploreUrlState, TimeRange, urlUtil } from "@grafana/data";
|
||||
import { ExploreUrlState, TimeRange, urlUtil } from '@grafana/data';
|
||||
import { MODE_ITEMID, MODE_METRICS } from '../../datasource-zabbix/constants';
|
||||
import { ActionButton } from '../ActionButton/ActionButton';
|
||||
import { expandItemName } from '../../datasource-zabbix/utils';
|
||||
@@ -35,7 +35,7 @@ const openInExplore = (problem: ProblemDTO, panelId: number, range: TimeRange) =
|
||||
item: { filter: itemName },
|
||||
};
|
||||
} else {
|
||||
const itemids = problem.items?.map(p => p.itemid).join(',');
|
||||
const itemids = problem.items?.map((p) => p.itemid).join(',');
|
||||
query = {
|
||||
queryType: MODE_ITEMID,
|
||||
itemids: itemids,
|
||||
@@ -54,4 +54,3 @@ const openInExplore = (problem: ProblemDTO, panelId: number, range: TimeRange) =
|
||||
const url = urlUtil.renderUrl('/explore', { left: exploreState });
|
||||
locationService.push(url);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataQuery, DataSourceJsonData, DataSourceRef, SelectableValue } from '@grafana/data';
|
||||
import { BusEventWithPayload, DataQuery, DataSourceJsonData, DataSourceRef, SelectableValue } from '@grafana/data';
|
||||
|
||||
export interface ZabbixDSOptions extends DataSourceJsonData {
|
||||
username: string;
|
||||
@@ -207,6 +207,8 @@ export interface ProblemDTO {
|
||||
triggerid?: string;
|
||||
eventid?: string;
|
||||
timestamp: number;
|
||||
lastchange?: string;
|
||||
lastchangeUnix?: number;
|
||||
|
||||
/** Name of the trigger. */
|
||||
name?: string;
|
||||
@@ -223,6 +225,7 @@ export interface ProblemDTO {
|
||||
hostTechName?: string;
|
||||
proxy?: string;
|
||||
severity?: string;
|
||||
priority?: string;
|
||||
|
||||
acknowledged?: '1' | '0';
|
||||
acknowledges?: ZBXAcknowledge[];
|
||||
@@ -245,6 +248,7 @@ export interface ProblemDTO {
|
||||
error?: string;
|
||||
|
||||
showAckButton?: boolean;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface ZBXProblem {
|
||||
@@ -323,6 +327,7 @@ export interface ZBXHost {
|
||||
host: string;
|
||||
maintenance_status?: string;
|
||||
proxy_hostid?: string;
|
||||
proxy?: any;
|
||||
}
|
||||
|
||||
export interface ZBXItem {
|
||||
@@ -380,3 +385,7 @@ export interface ZBXAlert {
|
||||
message: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export class ZBXQueryUpdatedEvent extends BusEventWithPayload<any> {
|
||||
static type = 'zbx-query-updated';
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { AppPlugin } from '@grafana/data';
|
||||
import { loadPluginCss } from 'grafana/app/plugins/sdk';
|
||||
|
||||
import './sass/grafana-zabbix.dark.scss';
|
||||
import './sass/grafana-zabbix.light.scss';
|
||||
|
||||
import {ZabbixAppConfigCtrl} from './app_config_ctrl/config';
|
||||
import {loadPluginCss} from 'grafana/app/plugins/sdk';
|
||||
|
||||
loadPluginCss({
|
||||
dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css',
|
||||
light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css'
|
||||
light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css',
|
||||
});
|
||||
|
||||
export {
|
||||
ZabbixAppConfigCtrl as ConfigCtrl
|
||||
};
|
||||
export const plugin = new AppPlugin<{}>();
|
||||
277
src/panel-triggers/ProblemsPanel.tsx
Normal file
277
src/panel-triggers/ProblemsPanel.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { BusEventBase, BusEventWithPayload, dateMath, PanelProps } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'grafana/app/core/core';
|
||||
import { ProblemsPanelOptions } from './types';
|
||||
import { ProblemDTO, ZabbixMetricsQuery, ZBXQueryUpdatedEvent, ZBXTag } from '../datasource-zabbix/types';
|
||||
import { APIExecuteScriptResponse } from '../datasource-zabbix/zabbix/connectors/zabbix_api/types';
|
||||
import ProblemList from './components/Problems/Problems';
|
||||
import { AckProblemData } from './components/AckModal';
|
||||
import AlertList from './components/AlertList/AlertList';
|
||||
|
||||
const PROBLEM_EVENTS_LIMIT = 100;
|
||||
|
||||
interface ProblemsPanelProps extends PanelProps<ProblemsPanelOptions> {}
|
||||
|
||||
export const ProblemsPanel = (props: ProblemsPanelProps): JSX.Element => {
|
||||
const { data, options, timeRange, onOptionsChange } = props;
|
||||
const { layout, showTriggers, triggerSeverity, sortProblems } = options;
|
||||
const theme = useTheme2();
|
||||
|
||||
const prepareProblems = () => {
|
||||
const problems: ProblemDTO[] = [];
|
||||
if (!data?.series) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const dataFrame of data.series) {
|
||||
try {
|
||||
const values = dataFrame.fields[0].values;
|
||||
if (values.toArray) {
|
||||
problems.push(...values.toArray());
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
let triggers = _.cloneDeep(problems);
|
||||
|
||||
triggers = triggers.map((t) => formatTrigger(t));
|
||||
triggers = filterProblems(triggers);
|
||||
triggers = sortTriggers(triggers);
|
||||
|
||||
return triggers;
|
||||
};
|
||||
|
||||
const filterProblems = (problems: ProblemDTO[]) => {
|
||||
let problemsList = _.cloneDeep(problems);
|
||||
|
||||
// Filter acknowledged triggers
|
||||
if (showTriggers === 'unacknowledged') {
|
||||
problemsList = problemsList.filter((trigger) => {
|
||||
return !(trigger.acknowledges && trigger.acknowledges.length);
|
||||
});
|
||||
} else if (showTriggers === 'acknowledged') {
|
||||
problemsList = problemsList.filter((trigger) => {
|
||||
return trigger.acknowledges && trigger.acknowledges.length;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter triggers by severity
|
||||
problemsList = problemsList.filter((problem) => {
|
||||
if (problem.severity) {
|
||||
return triggerSeverity[problem.severity].show;
|
||||
} else {
|
||||
return triggerSeverity[problem.priority].show;
|
||||
}
|
||||
});
|
||||
|
||||
return problemsList;
|
||||
};
|
||||
|
||||
const sortTriggers = (problems: ProblemDTO[]) => {
|
||||
if (sortProblems === 'priority') {
|
||||
problems = _.orderBy(problems, ['severity', 'timestamp', 'eventid'], ['desc', 'desc', 'desc']);
|
||||
} else if (sortProblems === 'lastchange') {
|
||||
problems = _.orderBy(problems, ['timestamp', 'severity', 'eventid'], ['desc', 'desc', 'desc']);
|
||||
}
|
||||
return problems;
|
||||
};
|
||||
|
||||
const formatTrigger = (zabbixTrigger: ProblemDTO) => {
|
||||
const trigger = _.cloneDeep(zabbixTrigger);
|
||||
|
||||
// Set host and proxy that the trigger belongs
|
||||
if (trigger.hosts && trigger.hosts.length) {
|
||||
const host = trigger.hosts[0];
|
||||
trigger.host = host.name;
|
||||
trigger.hostTechName = host.host;
|
||||
if (host.proxy) {
|
||||
trigger.proxy = host.proxy;
|
||||
}
|
||||
}
|
||||
|
||||
// Set tags if present
|
||||
if (trigger.tags && trigger.tags.length === 0) {
|
||||
trigger.tags = null;
|
||||
}
|
||||
|
||||
// Handle multi-line description
|
||||
if (trigger.comments) {
|
||||
trigger.comments = trigger.comments.replace('\n', '<br>');
|
||||
}
|
||||
|
||||
trigger.lastchangeUnix = Number(trigger.lastchange);
|
||||
return trigger;
|
||||
};
|
||||
|
||||
const parseTags = (tagStr: string) => {
|
||||
if (!tagStr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tagStrings = _.map(tagStr.split(','), (tag) => tag.trim());
|
||||
const tags = _.map(tagStrings, (tag) => {
|
||||
const tagParts = tag.split(':');
|
||||
return { tag: tagParts[0].trim(), value: tagParts[1].trim() };
|
||||
});
|
||||
return tags;
|
||||
};
|
||||
|
||||
const tagsToString = (tags: ZBXTag[]) => {
|
||||
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
|
||||
};
|
||||
|
||||
const addTagFilter = (tag, datasource) => {
|
||||
const targets = data.request.targets;
|
||||
let updated = false;
|
||||
for (const target of targets) {
|
||||
if (target.datasource?.uid === datasource?.uid || target.datasource === datasource) {
|
||||
const tagFilter = (target as ZabbixMetricsQuery).tags.filter;
|
||||
let targetTags = parseTags(tagFilter);
|
||||
const newTag = { tag: tag.tag, value: tag.value };
|
||||
targetTags.push(newTag);
|
||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||
const newFilter = tagsToString(targetTags);
|
||||
(target as ZabbixMetricsQuery).tags.filter = newFilter;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
// TODO: investigate is it possible to handle this event
|
||||
const event = new ZBXQueryUpdatedEvent(targets);
|
||||
props.eventBus.publish(event);
|
||||
}
|
||||
};
|
||||
|
||||
const removeTagFilter = (tag, datasource) => {
|
||||
const matchTag = (t) => t.tag === tag.tag && t.value === tag.value;
|
||||
const targets = data.request.targets;
|
||||
let updated = false;
|
||||
for (const target of targets) {
|
||||
if (target.datasource?.uid === datasource?.uid || target.datasource === datasource) {
|
||||
const tagFilter = (target as ZabbixMetricsQuery).tags.filter;
|
||||
let targetTags = parseTags(tagFilter);
|
||||
_.remove(targetTags, matchTag);
|
||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||
const newFilter = tagsToString(targetTags);
|
||||
(target as ZabbixMetricsQuery).tags.filter = newFilter;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if (updated) {
|
||||
// TODO: investigate is it possible to handle this event
|
||||
const event = new ZBXQueryUpdatedEvent(targets);
|
||||
props.eventBus.publish(event);
|
||||
}
|
||||
};
|
||||
|
||||
const getProblemEvents = async (problem: ProblemDTO) => {
|
||||
const triggerids = [problem.triggerid];
|
||||
const timeFrom = Math.ceil(dateMath.parse(timeRange.from).unix());
|
||||
const timeTo = Math.ceil(dateMath.parse(timeRange.to).unix());
|
||||
const ds: any = await getDataSourceSrv().get(problem.datasource);
|
||||
return ds.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
|
||||
};
|
||||
|
||||
const getProblemAlerts = async (problem: ProblemDTO) => {
|
||||
if (!problem.eventid) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const eventids = [problem.eventid];
|
||||
const ds: any = await getDataSourceSrv().get(problem.datasource);
|
||||
return ds.zabbix.getEventAlerts(eventids);
|
||||
};
|
||||
|
||||
const getScripts = async (problem: ProblemDTO) => {
|
||||
const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null;
|
||||
const ds: any = await getDataSourceSrv().get(problem.datasource);
|
||||
return ds.zabbix.getScripts([hostid]);
|
||||
};
|
||||
|
||||
const onExecuteScript = async (problem: ProblemDTO, scriptid: string): Promise<APIExecuteScriptResponse> => {
|
||||
const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null;
|
||||
const ds: any = await getDataSourceSrv().get(problem.datasource);
|
||||
return ds.zabbix.executeScript(hostid, scriptid);
|
||||
};
|
||||
|
||||
const onProblemAck = async (problem: ProblemDTO, data: AckProblemData) => {
|
||||
const { message, action, severity } = data;
|
||||
const eventid = problem.eventid;
|
||||
const grafana_user = (contextSrv.user as any).name;
|
||||
const ack_message = grafana_user + ' (Grafana): ' + message;
|
||||
const ds: any = await getDataSourceSrv().get(problem.datasource);
|
||||
const userIsEditor = contextSrv.isEditor || contextSrv.isGrafanaAdmin;
|
||||
if (ds.disableReadOnlyUsersAck && !userIsEditor) {
|
||||
return { message: 'You have no permissions to acknowledge events.' };
|
||||
}
|
||||
if (eventid) {
|
||||
return ds.zabbix.acknowledgeEvent(eventid, ack_message, action, severity);
|
||||
} else {
|
||||
return { message: 'Trigger has no events. Nothing to acknowledge.' };
|
||||
}
|
||||
};
|
||||
|
||||
const onColumnResize = (newResized) => {
|
||||
onOptionsChange({ ...options, resizedColumns: newResized });
|
||||
};
|
||||
|
||||
const onTagClick = (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => {
|
||||
if (ctrlKey || shiftKey) {
|
||||
removeTagFilter(tag, datasource);
|
||||
} else {
|
||||
addTagFilter(tag, datasource);
|
||||
}
|
||||
};
|
||||
|
||||
const renderList = () => {
|
||||
const problems = prepareProblems();
|
||||
const fontSize = parseInt(options.fontSize.slice(0, options.fontSize.length - 1), 10);
|
||||
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
|
||||
|
||||
return (
|
||||
<AlertList
|
||||
problems={problems}
|
||||
panelOptions={options}
|
||||
pageSize={options.pageSize}
|
||||
fontSize={fontSizeProp}
|
||||
onProblemAck={onProblemAck}
|
||||
onTagClick={onTagClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTable = () => {
|
||||
const problems = prepareProblems();
|
||||
const fontSize = parseInt(options.fontSize.slice(0, options.fontSize.length - 1), 10);
|
||||
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
|
||||
|
||||
return (
|
||||
<ProblemList
|
||||
problems={problems}
|
||||
panelOptions={options}
|
||||
pageSize={options.pageSize}
|
||||
fontSize={fontSizeProp}
|
||||
timeRange={timeRange}
|
||||
getProblemEvents={getProblemEvents}
|
||||
getProblemAlerts={getProblemAlerts}
|
||||
getScripts={getScripts}
|
||||
onExecuteScript={onExecuteScript}
|
||||
onProblemAck={onProblemAck}
|
||||
onColumnResize={onColumnResize}
|
||||
onTagClick={onTagClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{layout === 'list' && renderList()}
|
||||
{layout === 'table' && renderTable()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +1,29 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { ZBXTrigger } from '../../types';
|
||||
import { ProblemDTO } from '../../../datasource-zabbix/types';
|
||||
|
||||
interface AlertAcknowledgesProps {
|
||||
problem: ZBXTrigger;
|
||||
problem: ProblemDTO;
|
||||
onClick: (event?) => void;
|
||||
}
|
||||
|
||||
export default class AlertAcknowledges extends PureComponent<AlertAcknowledgesProps> {
|
||||
handleClick = (event) => {
|
||||
this.props.onClick(event);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { problem } = this.props;
|
||||
const ackRows = problem.acknowledges && problem.acknowledges.map(ack => {
|
||||
return (
|
||||
<tr key={ack.acknowledgeid}>
|
||||
<td>{ack.time}</td>
|
||||
<td>{ack.user}</td>
|
||||
<td>{ack.message}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
const ackRows =
|
||||
problem.acknowledges &&
|
||||
problem.acknowledges.map((ack) => {
|
||||
return (
|
||||
<tr key={ack.acknowledgeid}>
|
||||
<td>{ack.time}</td>
|
||||
<td>{ack.user}</td>
|
||||
<td>{ack.message}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="ack-tooltip">
|
||||
<table className="table">
|
||||
@@ -32,17 +34,19 @@ export default class AlertAcknowledges extends PureComponent<AlertAcknowledgesPr
|
||||
<th className="ack-comments">Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ackRows}
|
||||
</tbody>
|
||||
<tbody>{ackRows}</tbody>
|
||||
</table>
|
||||
{problem.showAckButton &&
|
||||
{problem.showAckButton && (
|
||||
<div className="ack-add-button">
|
||||
<button id="add-acknowledge-btn" className="btn btn-mini btn-inverse gf-form-button" onClick={this.handleClick}>
|
||||
<button
|
||||
id="add-acknowledge-btn"
|
||||
className="btn btn-mini btn-inverse gf-form-button"
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<i className="fa fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,25 +31,32 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
|
||||
ackProblem = (data: AckProblemData) => {
|
||||
const problem = this.props.problem;
|
||||
return this.props.onProblemAck(problem, data);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { problem, panelOptions } = this.props;
|
||||
const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1;
|
||||
const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground });
|
||||
const descriptionClass = classNames('alert-rule-item__text', { 'zbx-description--newline': panelOptions.descriptionAtNewLine });
|
||||
const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', {
|
||||
'zbx-trigger-highlighted': panelOptions.highlightBackground,
|
||||
});
|
||||
const descriptionClass = classNames('alert-rule-item__text', {
|
||||
'zbx-description--newline': panelOptions.descriptionAtNewLine,
|
||||
});
|
||||
|
||||
const problemSeverity = Number(problem.severity);
|
||||
let severityDesc: TriggerSeverity;
|
||||
severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity);
|
||||
severityDesc = _.find(panelOptions.triggerSeverity, (s) => s.priority === problemSeverity);
|
||||
if (problem.severity) {
|
||||
severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity);
|
||||
severityDesc = _.find(panelOptions.triggerSeverity, (s) => s.priority === problemSeverity);
|
||||
}
|
||||
|
||||
const lastchange = formatLastChange(problem.timestamp, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat);
|
||||
const lastchange = formatLastChange(
|
||||
problem.timestamp,
|
||||
panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat
|
||||
);
|
||||
const age = moment.unix(problem.timestamp).fromNow(true);
|
||||
|
||||
let dsName: string = (problem.datasource as string);
|
||||
let dsName: string = problem.datasource as string;
|
||||
if ((problem.datasource as DataSourceRef)?.uid) {
|
||||
const dsInstance = getDataSourceSrv().getInstanceSettings((problem.datasource as DataSourceRef).uid);
|
||||
dsName = dsInstance.name;
|
||||
@@ -64,7 +71,7 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
|
||||
let problemColor: string;
|
||||
if (problem.value === '0') {
|
||||
problemColor = panelOptions.okEventColor;
|
||||
} else if (panelOptions.markAckEvents && problem.acknowledged === "1") {
|
||||
} else if (panelOptions.markAckEvents && problem.acknowledged === '1') {
|
||||
problemColor = panelOptions.ackEventColor;
|
||||
} else {
|
||||
problemColor = severityDesc.color;
|
||||
@@ -77,7 +84,12 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
|
||||
|
||||
return (
|
||||
<li className={cardClass} style={cardStyle}>
|
||||
<AlertIcon problem={problem} color={problemColor} highlightBackground={panelOptions.highlightBackground} blink={blink} />
|
||||
<AlertIcon
|
||||
problem={problem}
|
||||
color={problemColor}
|
||||
highlightBackground={panelOptions.highlightBackground}
|
||||
blink={blink}
|
||||
/>
|
||||
|
||||
<div className="alert-rule-item__body">
|
||||
<div className="alert-rule-item__header">
|
||||
@@ -90,15 +102,16 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
|
||||
|
||||
{panelOptions.showTags && (
|
||||
<span className="zbx-trigger-tags">
|
||||
{problem.tags && problem.tags.map(tag =>
|
||||
<EventTag
|
||||
key={tag.tag + tag.value}
|
||||
tag={tag}
|
||||
datasource={dsName}
|
||||
highlight={tag.tag === problem.correlation_tag}
|
||||
onClick={this.handleTagClick}
|
||||
/>
|
||||
)}
|
||||
{problem.tags &&
|
||||
problem.tags.map((tag) => (
|
||||
<EventTag
|
||||
key={tag.tag + tag.value}
|
||||
tag={tag}
|
||||
datasource={dsName}
|
||||
highlight={tag.tag === problem.correlation_tag}
|
||||
onClick={this.handleTagClick}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -106,25 +119,26 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
|
||||
<div className={descriptionClass}>
|
||||
{panelOptions.statusField && <AlertStatus problem={problem} blink={blink} />}
|
||||
{panelOptions.severityField && (
|
||||
<AlertSeverity severityDesc={severityDesc} blink={blink} highlightBackground={panelOptions.highlightBackground} />
|
||||
<AlertSeverity
|
||||
severityDesc={severityDesc}
|
||||
blink={blink}
|
||||
highlightBackground={panelOptions.highlightBackground}
|
||||
/>
|
||||
)}
|
||||
<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 && (
|
||||
<span className="zbx-description" dangerouslySetInnerHTML={{ __html: problem.comments }} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{panelOptions.descriptionField && panelOptions.descriptionAtNewLine && (
|
||||
<div className="alert-rule-item__text zbx-description--newline" >
|
||||
<div className="alert-rule-item__text zbx-description--newline">
|
||||
<span
|
||||
className="alert-rule-item__info zbx-description"
|
||||
dangerouslySetInnerHTML={{ __html: problem.comments }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -138,30 +152,36 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
|
||||
)}
|
||||
|
||||
<div className="alert-rule-item__time zbx-trigger-lastchange">
|
||||
<span>{lastchange || "last change unknown"}</span>
|
||||
<span>{lastchange || 'last change unknown'}</span>
|
||||
<div className="trigger-info-block zbx-status-icons">
|
||||
{problem.url && <a href={problem.url} target="_blank"><i className="fa fa-external-link"></i></a>}
|
||||
{problem.url && (
|
||||
<a href={problem.url} target="_blank">
|
||||
<i className="fa fa-external-link"></i>
|
||||
</a>
|
||||
)}
|
||||
{problem.state === '1' && (
|
||||
<Tooltip placement="bottom" content={problem.error}>
|
||||
<span><i className="fa fa-question-circle"></i></span>
|
||||
<span>
|
||||
<i className="fa fa-question-circle"></i>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{problem.eventid && (
|
||||
<ModalController>
|
||||
{({ showModal, hideModal }) => (
|
||||
<AlertAcknowledgesButton
|
||||
problem={problem}
|
||||
onClick={() => {
|
||||
showModal(AckModal, {
|
||||
canClose: problem.manual_close === '1',
|
||||
severity: problemSeverity,
|
||||
onSubmit: this.ackProblem,
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ModalController>
|
||||
{({ showModal, hideModal }) => (
|
||||
<AlertAcknowledgesButton
|
||||
problem={problem}
|
||||
onClick={() => {
|
||||
showModal(AckModal, {
|
||||
canClose: problem.manual_close === '1',
|
||||
severity: problemSeverity,
|
||||
onSubmit: this.ackProblem,
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ModalController>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,7 +198,7 @@ interface AlertHostProps {
|
||||
function AlertHost(props: AlertHostProps) {
|
||||
const problem = props.problem;
|
||||
const panel = props.panelOptions;
|
||||
let host = "";
|
||||
let host = '';
|
||||
if (panel.hostField && panel.hostTechNameField) {
|
||||
host = `${problem.host} (${problem.hostTechName})`;
|
||||
} else if (panel.hostField || panel.hostTechNameField) {
|
||||
@@ -204,15 +224,13 @@ interface AlertGroupProps {
|
||||
function AlertGroup(props: AlertGroupProps) {
|
||||
const problem = props.problem;
|
||||
const panel = props.panelOptions;
|
||||
let groupNames = "";
|
||||
let groupNames = '';
|
||||
if (panel.hostGroups) {
|
||||
const groups = _.map(problem.groups, 'name').join(', ');
|
||||
groupNames += `[ ${groups} ]`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="zabbix-hostname">{groupNames}</span>
|
||||
);
|
||||
return <span className="zabbix-hostname">{groupNames}</span>;
|
||||
}
|
||||
|
||||
const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)';
|
||||
@@ -228,11 +246,7 @@ function AlertStatus(props) {
|
||||
{ 'alert-state-ok': problem.value === '0' },
|
||||
{ 'zabbix-trigger--blinked': blink }
|
||||
);
|
||||
return (
|
||||
<span className={className}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
return <span className={className}>{status}</span>;
|
||||
}
|
||||
|
||||
function AlertSeverity(props) {
|
||||
@@ -257,25 +271,29 @@ interface AlertAcknowledgesButtonProps {
|
||||
class AlertAcknowledgesButton extends PureComponent<AlertAcknowledgesButtonProps> {
|
||||
handleClick = (event) => {
|
||||
this.props.onClick(event);
|
||||
}
|
||||
};
|
||||
|
||||
renderTooltipContent = () => {
|
||||
return <AlertAcknowledges problem={this.props.problem} onClick={this.handleClick} />;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { problem } = this.props;
|
||||
let content = null;
|
||||
if (problem.acknowledges && problem.acknowledges.length) {
|
||||
content = (
|
||||
<Tooltip placement="bottom" content={this.renderTooltipContent}>
|
||||
<span><i className="fa fa-comments"></i></span>
|
||||
<Tooltip placement="auto" content={this.renderTooltipContent} interactive>
|
||||
<span>
|
||||
<i className="fa fa-comments"></i>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
} else if (problem.showAckButton) {
|
||||
content = (
|
||||
<Tooltip placement="bottom" content="Acknowledge problem">
|
||||
<span role="button" onClick={this.handleClick}><i className="fa fa-comments-o"></i></span>
|
||||
<span role="button" onClick={this.handleClick}>
|
||||
<i className="fa fa-comments-o"></i>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ export const AlertIcon: FC<Props> = ({ problem, color, blink, highlightBackgroun
|
||||
const severity = Number(problem.severity);
|
||||
const status = problem.value === '1' && severity >= 2 ? 'critical' : 'online';
|
||||
|
||||
const iconClass = cx(
|
||||
'icon-gf',
|
||||
blink && 'zabbix-trigger--blinked',
|
||||
);
|
||||
const iconClass = cx('icon-gf', blink && 'zabbix-trigger--blinked');
|
||||
|
||||
const wrapperClass = cx(
|
||||
'alert-rule-item__icon',
|
||||
!highlightBackground && css`color: ${color}`
|
||||
!highlightBackground &&
|
||||
css`
|
||||
color: ${color};
|
||||
`
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { PureComponent, CSSProperties } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ProblemsPanelOptions, GFTimeRange } from '../../types';
|
||||
import { ProblemsPanelOptions } from '../../types';
|
||||
import { AckProblemData } from '../AckModal';
|
||||
import AlertCard from './AlertCard';
|
||||
import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types';
|
||||
@@ -10,7 +10,6 @@ export interface AlertListProps {
|
||||
problems: ProblemDTO[];
|
||||
panelOptions: ProblemsPanelOptions;
|
||||
loading?: boolean;
|
||||
timeRange?: GFTimeRange;
|
||||
pageSize?: number;
|
||||
fontSize?: number;
|
||||
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void;
|
||||
@@ -44,18 +43,17 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
|
||||
page: newPage,
|
||||
currentProblems: items,
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
handleTagClick = (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => {
|
||||
if (this.props.onTagClick) {
|
||||
this.props.onTagClick(tag, datasource, ctrlKey, shiftKey);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => {
|
||||
return this.props.onProblemAck(problem, data);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { problems, panelOptions } = this.props;
|
||||
@@ -68,15 +66,17 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
|
||||
<div className="triggers-panel-container" key="alertListContainer">
|
||||
<section className="card-section card-list-layout-list">
|
||||
<ol className={alertListClass}>
|
||||
{currentProblems.map((problem, index) =>
|
||||
{currentProblems.map((problem, index) => (
|
||||
<AlertCard
|
||||
key={`${problem.triggerid}-${problem.eventid}-${(problem.datasource as DataSourceRef)?.uid || problem.datasource}-${index}`}
|
||||
key={`${problem.triggerid}-${problem.eventid}-${
|
||||
(problem.datasource as DataSourceRef)?.uid || problem.datasource
|
||||
}-${index}`}
|
||||
problem={problem}
|
||||
panelOptions={panelOptions}
|
||||
onTagClick={this.handleTagClick}
|
||||
onProblemAck={this.handleProblemAck}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
@@ -101,10 +101,9 @@ interface PaginationControlProps {
|
||||
}
|
||||
|
||||
class PaginationControl extends PureComponent<PaginationControlProps> {
|
||||
|
||||
handlePageChange = (index: number) => () => {
|
||||
this.props.onPageChange(index);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { itemsLength, pageIndex, pageSize } = this.props;
|
||||
@@ -116,23 +115,20 @@ class PaginationControl extends PureComponent<PaginationControlProps> {
|
||||
const startPage = Math.max(pageIndex - 3, 0);
|
||||
const endPage = Math.min(pageCount, startPage + 9);
|
||||
|
||||
|
||||
const pageLinks = [];
|
||||
for (let i = startPage; i < endPage; i++) {
|
||||
const pageLinkClass = classNames('triggers-panel-page-link', 'pointer', { 'active': i === pageIndex });
|
||||
const pageLinkClass = classNames('triggers-panel-page-link', 'pointer', { active: i === pageIndex });
|
||||
const value = i + 1;
|
||||
const pageLinkElem = (
|
||||
<li key={value.toString()}>
|
||||
<a className={pageLinkClass} onClick={this.handlePageChange(i)}>{value}</a>
|
||||
<a className={pageLinkClass} onClick={this.handlePageChange(i)}>
|
||||
{value}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
pageLinks.push(pageLinkElem);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{pageLinks}
|
||||
</ul>
|
||||
);
|
||||
return <ul>{pageLinks}</ul>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { ZBXTag } from '../types';
|
||||
import { DataSourceRef } from '@grafana/data';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { ZBXTag } from '../../datasource-zabbix/types';
|
||||
|
||||
const TAG_COLORS = [
|
||||
'#E24D42',
|
||||
@@ -96,10 +96,10 @@ interface EventTagProps {
|
||||
export default class EventTag extends PureComponent<EventTagProps> {
|
||||
handleClick = (event) => {
|
||||
if (this.props.onClick) {
|
||||
const { tag, datasource} = this.props;
|
||||
const { tag, datasource } = this.props;
|
||||
this.props.onClick(tag, datasource, event.ctrlKey, event.shiftKey);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tag, highlight } = this.props;
|
||||
@@ -110,13 +110,12 @@ export default class EventTag extends PureComponent<EventTagProps> {
|
||||
};
|
||||
return (
|
||||
<Tooltip placement="bottom" content="Click to add tag filter or Ctrl/Shift+click to remove">
|
||||
<span className={`label label-tag zbx-tag ${highlight ? 'highlighted' : ''}`}
|
||||
<span
|
||||
className={`label label-tag zbx-tag ${highlight ? 'highlighted' : ''}`}
|
||||
style={style}
|
||||
onClick={this.handleClick}>
|
||||
{tag.value ?
|
||||
`${tag.tag}: ${tag.value}` :
|
||||
`${tag.tag}`
|
||||
}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{tag.value ? `${tag.tag}: ${tag.value}` : `${tag.tag}`}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
|
||||
async componentDidMount() {
|
||||
const scripts = await this.props.getScripts();
|
||||
this.scripts = scripts;
|
||||
const scriptOptions: Array<SelectableValue<string>> = scripts.map(s => {
|
||||
const scriptOptions: Array<SelectableValue<string>> = scripts.map((s) => {
|
||||
return {
|
||||
value: s.scriptid,
|
||||
label: s.name,
|
||||
@@ -55,27 +55,27 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
|
||||
});
|
||||
|
||||
const selectedScript = scriptOptions?.length ? scriptOptions[0] : null;
|
||||
const script = scripts.find(s => selectedScript.value === s.scriptid);
|
||||
const script = scripts.find((s) => selectedScript.value === s.scriptid);
|
||||
|
||||
this.setState({ scriptOptions, selectedScript, script });
|
||||
}
|
||||
|
||||
onChangeSelectedScript = (v: SelectableValue<string>) => {
|
||||
const script = this.scripts.find(s => v.value === s.scriptid);
|
||||
const script = this.scripts.find((s) => v.value === s.scriptid);
|
||||
this.setState({ selectedScript: v, script, errorMessage: '', loading: false, result: '' });
|
||||
};
|
||||
|
||||
dismiss = () => {
|
||||
this.setState({ selectedScript: null, error: false, errorMessage: '', selectError: '', loading: false });
|
||||
this.props.onDismiss();
|
||||
}
|
||||
};
|
||||
|
||||
submit = () => {
|
||||
const { selectedScript } = this.state;
|
||||
|
||||
if (!selectedScript) {
|
||||
return this.setState({
|
||||
selectError: 'Select a script to execute.'
|
||||
selectError: 'Select a script to execute.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,30 +85,33 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
|
||||
scriptid: selectedScript.value,
|
||||
};
|
||||
|
||||
this.props.onSubmit(data).then((result: APIExecuteScriptResponse) => {
|
||||
const message = this.formatResult(result?.value || '');
|
||||
if (result?.response === 'success') {
|
||||
this.setState({ result: message, loading: false });
|
||||
} else {
|
||||
this.setState({ error: true, errorMessage: message, loading: false });
|
||||
}
|
||||
}).catch(err => {
|
||||
let errorMessage = err.data?.message || err.data?.error || err.data || err.statusText || '';
|
||||
errorMessage = this.formatResult(errorMessage);
|
||||
this.setState({
|
||||
error: true,
|
||||
loading: false,
|
||||
errorMessage,
|
||||
this.props
|
||||
.onSubmit(data)
|
||||
.then((result: APIExecuteScriptResponse) => {
|
||||
const message = this.formatResult(result?.value || '');
|
||||
if (result?.response === 'success') {
|
||||
this.setState({ result: message, loading: false });
|
||||
} else {
|
||||
this.setState({ error: true, errorMessage: message, loading: false });
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
let errorMessage = err.data?.message || err.data?.error || err.data || err.statusText || '';
|
||||
errorMessage = this.formatResult(errorMessage);
|
||||
this.setState({
|
||||
error: true,
|
||||
loading: false,
|
||||
errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
formatResult = (result: string) => {
|
||||
const formatted = result.split('\n').map((p, i) => {
|
||||
return <p key={i}>{p}</p>;
|
||||
});
|
||||
return <>{formatted}</>;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
@@ -135,14 +138,8 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
|
||||
>
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-hint">
|
||||
<Select
|
||||
options={scriptOptions}
|
||||
value={selectedScript}
|
||||
onChange={this.onChangeSelectedScript}
|
||||
/>
|
||||
{selectError &&
|
||||
<small className={selectErrorClass}>{selectError}</small>
|
||||
}
|
||||
<Select options={scriptOptions} value={selectedScript} onChange={this.onChangeSelectedScript} />
|
||||
{selectError && <small className={selectErrorClass}>{selectError}</small>}
|
||||
</label>
|
||||
</div>
|
||||
<div className={scriptCommandContainerClass}>
|
||||
@@ -150,17 +147,17 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
|
||||
</div>
|
||||
|
||||
<div className={styles.resultContainer}>
|
||||
{result &&
|
||||
<span className={styles.execResult}>{result}</span>
|
||||
}
|
||||
{error &&
|
||||
<span className={styles.execError}>{errorMessage}</span>
|
||||
}
|
||||
{result && <span className={styles.execResult}>{result}</span>}
|
||||
{error && <span className={styles.execError}>{errorMessage}</span>}
|
||||
</div>
|
||||
|
||||
<div className="gf-form-button-row text-center">
|
||||
<Button variant="primary" onClick={this.submit}>Execute</Button>
|
||||
<Button variant="secondary" onClick={this.dismiss}>Cancel</Button>
|
||||
<Button variant="primary" onClick={this.submit}>
|
||||
Execute
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={this.dismiss}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
@@ -211,8 +208,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
`,
|
||||
execResult: css`
|
||||
`,
|
||||
execResult: css``,
|
||||
execError: css`
|
||||
color: ${red};
|
||||
`,
|
||||
|
||||
69
src/panel-triggers/components/ProblemColorEditor.tsx
Normal file
69
src/panel-triggers/components/ProblemColorEditor.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { FormEvent } from 'react';
|
||||
import {
|
||||
Button,
|
||||
ColorPicker,
|
||||
HorizontalGroup,
|
||||
InlineField,
|
||||
InlineFieldRow,
|
||||
InlineLabel,
|
||||
InlineSwitch,
|
||||
Input,
|
||||
VerticalGroup,
|
||||
} from '@grafana/ui';
|
||||
import { StandardEditorProps } from '@grafana/data';
|
||||
import { GFHeartIcon } from '../../components';
|
||||
import { TriggerSeverity } from '../types';
|
||||
|
||||
type Props = StandardEditorProps<TriggerSeverity[]>;
|
||||
export const ProblemColorEditor = ({ value, onChange }: Props): JSX.Element => {
|
||||
const onSeverityItemChange = (severity: TriggerSeverity) => {
|
||||
value.forEach((v, i) => {
|
||||
if (v.priority === severity.priority) {
|
||||
value[i] = severity;
|
||||
}
|
||||
});
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{value.map((severity, index) => (
|
||||
<ProblemColorEditorRow
|
||||
key={`${severity.priority}-${index}`}
|
||||
value={severity}
|
||||
onChange={(value) => onSeverityItemChange(value)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ProblemColorEditorRowProps {
|
||||
value: TriggerSeverity;
|
||||
onChange: (value?: TriggerSeverity) => void;
|
||||
}
|
||||
|
||||
export const ProblemColorEditorRow = ({ value, onChange }: ProblemColorEditorRowProps): JSX.Element => {
|
||||
const onSeverityNameChange = (v: FormEvent<HTMLInputElement>) => {
|
||||
const newValue = v?.currentTarget?.value;
|
||||
if (newValue !== null) {
|
||||
onChange({ ...value, severity: newValue });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<InlineFieldRow>
|
||||
<InlineField labelWidth={12}>
|
||||
<Input width={24} defaultValue={value.severity} onBlur={onSeverityNameChange} />
|
||||
</InlineField>
|
||||
<InlineLabel width={4}>
|
||||
<ColorPicker color={value.color} onChange={(color) => onChange({ ...value, color })} />
|
||||
</InlineLabel>
|
||||
<InlineField label="Show">
|
||||
<InlineSwitch value={value.show} onChange={() => onChange({ ...value, show: !value.show })} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
@@ -21,12 +21,12 @@ export const AckCell: React.FC<RTCell<ProblemDTO>> = (props: RTCell<ProblemDTO>)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{problem.acknowledges?.length > 0 &&
|
||||
{problem.acknowledges?.length > 0 && (
|
||||
<>
|
||||
<FAIcon icon="comments" />
|
||||
<span className={styles.countLabel}> ({problem.acknowledges?.length})</span>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { ZBXAcknowledge } from '../../types';
|
||||
import { ZBXAcknowledge } from '../../../datasource-zabbix/types';
|
||||
|
||||
interface AcknowledgesListProps {
|
||||
acknowledges: ZBXAcknowledge[];
|
||||
@@ -10,13 +10,25 @@ export default function AcknowledgesList(props: AcknowledgesListProps) {
|
||||
return (
|
||||
<div className="problem-ack-list">
|
||||
<div className="problem-ack-col problem-ack-time">
|
||||
{acknowledges.map(ack => <span key={ack.acknowledgeid} className="problem-ack-time">{ack.time}</span>)}
|
||||
{acknowledges.map((ack) => (
|
||||
<span key={ack.acknowledgeid} className="problem-ack-time">
|
||||
{ack.time}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="problem-ack-col problem-ack-user">
|
||||
{acknowledges.map(ack => <span key={ack.acknowledgeid} className="problem-ack-user">{ack.user}</span>)}
|
||||
{acknowledges.map((ack) => (
|
||||
<span key={ack.acknowledgeid} className="problem-ack-user">
|
||||
{ack.user}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="problem-ack-col problem-ack-message">
|
||||
{acknowledges.map(ack => <span key={ack.acknowledgeid} className="problem-ack-message">{ack.message}</span>)}
|
||||
{acknowledges.map((ack) => (
|
||||
<span key={ack.acknowledgeid} className="problem-ack-message">
|
||||
{ack.message}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import React, { FC, PureComponent } from 'react';
|
||||
import moment from 'moment';
|
||||
import { TimeRange, DataSourceRef } from "@grafana/data";
|
||||
import { TimeRange, DataSourceRef } from '@grafana/data';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import * as utils from '../../../datasource-zabbix/utils';
|
||||
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXGroup, ZBXHost, ZBXTag } from '../../../datasource-zabbix/types';
|
||||
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types';
|
||||
import { GFTimeRange, RTRow, ZBXItem } from '../../types';
|
||||
import { AckModal, AckProblemData } from '../AckModal';
|
||||
import EventTag from '../EventTag';
|
||||
import AcknowledgesList from './AcknowledgesList';
|
||||
import ProblemTimeline from './ProblemTimeline';
|
||||
import { AckButton, ExecScriptButton, ExploreButton, FAIcon, ModalController } from '../../../components';
|
||||
import { ExecScriptData, ExecScriptModal } from '../ExecScriptModal';
|
||||
import ProblemStatusBar from "./ProblemStatusBar";
|
||||
import ProblemStatusBar from './ProblemStatusBar';
|
||||
import { ZBXItem } from '../../../datasource-zabbix/types';
|
||||
import { RTRow } from '../../types';
|
||||
|
||||
interface ProblemDetailsProps extends RTRow<ProblemDTO> {
|
||||
rootWidth: number;
|
||||
timeRange: GFTimeRange;
|
||||
range: TimeRange;
|
||||
timeRange: TimeRange;
|
||||
showTimeline?: boolean;
|
||||
panelId?: number;
|
||||
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
|
||||
@@ -65,16 +65,14 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
|
||||
|
||||
fetchProblemEvents() {
|
||||
const problem = this.props.original;
|
||||
this.props.getProblemEvents(problem)
|
||||
.then(events => {
|
||||
this.props.getProblemEvents(problem).then((events) => {
|
||||
this.setState({ events });
|
||||
});
|
||||
}
|
||||
|
||||
fetchProblemAlerts() {
|
||||
const problem = this.props.original;
|
||||
this.props.getProblemAlerts(problem)
|
||||
.then(alerts => {
|
||||
this.props.getProblemAlerts(problem).then((alerts) => {
|
||||
this.setState({ alerts });
|
||||
});
|
||||
}
|
||||
@@ -97,15 +95,15 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
|
||||
render() {
|
||||
const problem = this.props.original as ProblemDTO;
|
||||
const alerts = this.state.alerts;
|
||||
const { rootWidth, panelId, range } = this.props;
|
||||
const { rootWidth, panelId, timeRange } = this.props;
|
||||
const displayClass = this.state.show ? 'show' : '';
|
||||
const wideLayout = rootWidth > 1200;
|
||||
const compactStatusBar = rootWidth < 800 || problem.acknowledges && wideLayout && rootWidth < 1400;
|
||||
const compactStatusBar = rootWidth < 800 || (problem.acknowledges && wideLayout && rootWidth < 1400);
|
||||
const age = moment.unix(problem.timestamp).fromNow(true);
|
||||
const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0;
|
||||
const problemSeverity = Number(problem.severity);
|
||||
|
||||
let dsName: string = (this.props.original.datasource as string);
|
||||
let dsName: string = this.props.original.datasource as string;
|
||||
if ((this.props.original.datasource as DataSourceRef)?.uid) {
|
||||
const dsInstance = getDataSourceSrv().getInstanceSettings((this.props.original.datasource as DataSourceRef).uid);
|
||||
dsName = dsInstance.name;
|
||||
@@ -117,106 +115,111 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
|
||||
<div className="problem-details">
|
||||
<div className="problem-details-head">
|
||||
<div className="problem-actions-left">
|
||||
<ExploreButton problem={problem} panelId={panelId} range={range}/>
|
||||
<ExploreButton problem={problem} panelId={panelId} range={timeRange} />
|
||||
</div>
|
||||
{problem.showAckButton &&
|
||||
<div className="problem-actions">
|
||||
<ModalController>
|
||||
{({ showModal, hideModal }) => (
|
||||
<ExecScriptButton
|
||||
className="problem-action-button"
|
||||
onClick={() => {
|
||||
showModal(ExecScriptModal, {
|
||||
getScripts: this.getScripts,
|
||||
onSubmit: this.onExecuteScript,
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ModalController>
|
||||
<ModalController>
|
||||
{({ showModal, hideModal }) => (
|
||||
<AckButton
|
||||
className="problem-action-button"
|
||||
onClick={() => {
|
||||
showModal(AckModal, {
|
||||
canClose: problem.manual_close === '1',
|
||||
severity: problemSeverity,
|
||||
onSubmit: this.ackProblem,
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ModalController>
|
||||
</div>
|
||||
}
|
||||
<ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'}/>
|
||||
{problem.showAckButton && (
|
||||
<div className="problem-actions">
|
||||
<ModalController>
|
||||
{({ showModal, hideModal }) => (
|
||||
<ExecScriptButton
|
||||
className="problem-action-button"
|
||||
onClick={() => {
|
||||
showModal(ExecScriptModal, {
|
||||
getScripts: this.getScripts,
|
||||
onSubmit: this.onExecuteScript,
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ModalController>
|
||||
<ModalController>
|
||||
{({ showModal, hideModal }) => (
|
||||
<AckButton
|
||||
className="problem-action-button"
|
||||
onClick={() => {
|
||||
showModal(AckModal, {
|
||||
canClose: problem.manual_close === '1',
|
||||
severity: problemSeverity,
|
||||
onSubmit: this.ackProblem,
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ModalController>
|
||||
</div>
|
||||
)}
|
||||
<ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'} />
|
||||
</div>
|
||||
<div className="problem-details-row">
|
||||
<div className="problem-value-container">
|
||||
<div className="problem-age">
|
||||
<FAIcon icon="clock-o"/>
|
||||
<FAIcon icon="clock-o" />
|
||||
<span>{age}</span>
|
||||
</div>
|
||||
{problem.items && <ProblemItems items={problem.items}/>}
|
||||
{problem.items && <ProblemItems items={problem.items} />}
|
||||
</div>
|
||||
</div>
|
||||
{problem.comments &&
|
||||
<div className="problem-description-row">
|
||||
<div className="problem-description">
|
||||
<Tooltip placement="right" content={problem.comments}>
|
||||
<span className="description-label">Description: </span>
|
||||
</Tooltip>
|
||||
<span>{problem.comments}</span>
|
||||
{problem.comments && (
|
||||
<div className="problem-description-row">
|
||||
<div className="problem-description">
|
||||
<Tooltip placement="right" content={problem.comments}>
|
||||
<span className="description-label">Description: </span>
|
||||
</Tooltip>
|
||||
<span>{problem.comments}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{problem.tags && problem.tags.length > 0 && (
|
||||
<div className="problem-tags">
|
||||
{problem.tags &&
|
||||
problem.tags.map((tag) => (
|
||||
<EventTag
|
||||
key={tag.tag + tag.value}
|
||||
tag={tag}
|
||||
datasource={problem.datasource}
|
||||
highlight={tag.tag === problem.correlation_tag}
|
||||
onClick={this.handleTagClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{this.props.showTimeline && this.state.events.length > 0 && (
|
||||
<ProblemTimeline events={this.state.events} timeRange={this.props.timeRange} />
|
||||
)}
|
||||
{showAcknowledges && !wideLayout && (
|
||||
<div className="problem-ack-container">
|
||||
<h6>
|
||||
<FAIcon icon="reply-all" /> Acknowledges
|
||||
</h6>
|
||||
<AcknowledgesList acknowledges={problem.acknowledges} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showAcknowledges && wideLayout && (
|
||||
<div className="problem-details-middle">
|
||||
<div className="problem-ack-container">
|
||||
<h6>
|
||||
<FAIcon icon="reply-all" /> Acknowledges
|
||||
</h6>
|
||||
<AcknowledgesList acknowledges={problem.acknowledges} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{problem.tags && problem.tags.length > 0 &&
|
||||
<div className="problem-tags">
|
||||
{problem.tags && problem.tags.map(tag =>
|
||||
<EventTag
|
||||
key={tag.tag + tag.value}
|
||||
tag={tag}
|
||||
datasource={problem.datasource}
|
||||
highlight={tag.tag === problem.correlation_tag}
|
||||
onClick={this.handleTagClick}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{this.props.showTimeline && this.state.events.length > 0 &&
|
||||
<ProblemTimeline events={this.state.events} timeRange={this.props.timeRange}/>
|
||||
}
|
||||
{showAcknowledges && !wideLayout &&
|
||||
<div className="problem-ack-container">
|
||||
<h6><FAIcon icon="reply-all"/> Acknowledges</h6>
|
||||
<AcknowledgesList acknowledges={problem.acknowledges}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{showAcknowledges && wideLayout &&
|
||||
<div className="problem-details-middle">
|
||||
<div className="problem-ack-container">
|
||||
<h6><FAIcon icon="reply-all"/> Acknowledges</h6>
|
||||
<AcknowledgesList acknowledges={problem.acknowledges}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
<div className="problem-details-right">
|
||||
<div className="problem-details-right-item">
|
||||
<FAIcon icon="database"/>
|
||||
<FAIcon icon="database" />
|
||||
<span>{dsName}</span>
|
||||
</div>
|
||||
{problem.proxy &&
|
||||
<div className="problem-details-right-item">
|
||||
<FAIcon icon="cloud"/>
|
||||
<span>{problem.proxy}</span>
|
||||
</div>
|
||||
}
|
||||
{problem.groups && <ProblemGroups groups={problem.groups} className="problem-details-right-item"/>}
|
||||
{problem.hosts && <ProblemHosts hosts={problem.hosts} className="problem-details-right-item"/>}
|
||||
{problem.proxy && (
|
||||
<div className="problem-details-right-item">
|
||||
<FAIcon icon="cloud" />
|
||||
<span>{problem.proxy}</span>
|
||||
</div>
|
||||
)}
|
||||
{problem.groups && <ProblemGroups groups={problem.groups} className="problem-details-right-item" />}
|
||||
{problem.hosts && <ProblemHosts hosts={problem.hosts} className="problem-details-right-item" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -232,11 +235,17 @@ interface ProblemItemProps {
|
||||
function ProblemItem(props: ProblemItemProps) {
|
||||
const { item, showName } = props;
|
||||
const itemName = utils.expandItemName(item.name, item.key_);
|
||||
const tooltipContent = () => <>{itemName}<br/>{item.lastvalue}</>;
|
||||
const tooltipContent = () => (
|
||||
<>
|
||||
{itemName}
|
||||
<br />
|
||||
{item.lastvalue}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="problem-item">
|
||||
<FAIcon icon="thermometer-three-quarters"/>
|
||||
<FAIcon icon="thermometer-three-quarters" />
|
||||
{showName && <span className="problem-item-name">{item.name}: </span>}
|
||||
<Tooltip placement="top-start" content={tooltipContent}>
|
||||
<span className="problem-item-value">{item.lastvalue}</span>
|
||||
@@ -252,10 +261,11 @@ interface ProblemItemsProps {
|
||||
const ProblemItems: FC<ProblemItemsProps> = ({ items }) => {
|
||||
return (
|
||||
<div className="problem-items-row">
|
||||
{items.length > 1 ?
|
||||
items.map(item => <ProblemItem item={item} key={item.itemid} showName={true}/>) :
|
||||
<ProblemItem item={items[0]}/>
|
||||
}
|
||||
{items.length > 1 ? (
|
||||
items.map((item) => <ProblemItem item={item} key={item.itemid} showName={true} />)
|
||||
) : (
|
||||
<ProblemItem item={items[0]} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -267,9 +277,9 @@ interface ProblemGroupsProps {
|
||||
|
||||
class ProblemGroups extends PureComponent<ProblemGroupsProps> {
|
||||
render() {
|
||||
return this.props.groups.map(g => (
|
||||
return this.props.groups.map((g) => (
|
||||
<div className={this.props.className || ''} key={g.groupid}>
|
||||
<FAIcon icon="folder"/>
|
||||
<FAIcon icon="folder" />
|
||||
<span>{g.name}</span>
|
||||
</div>
|
||||
));
|
||||
@@ -283,9 +293,9 @@ interface ProblemHostsProps {
|
||||
|
||||
class ProblemHosts extends PureComponent<ProblemHostsProps> {
|
||||
render() {
|
||||
return this.props.hosts.map(h => (
|
||||
return this.props.hosts.map((h) => (
|
||||
<div className={this.props.className || ''} key={h.hostid}>
|
||||
<FAIcon icon="server"/>
|
||||
<FAIcon icon="server" />
|
||||
<span>{h.name}</span>
|
||||
</div>
|
||||
));
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import FAIcon from '../../../components/FAIcon/FAIcon';
|
||||
import { ZBXTrigger, ZBXAlert } from '../../types';
|
||||
import { ZBXAlert, ProblemDTO } from '../../../datasource-zabbix/types';
|
||||
|
||||
export interface ProblemStatusBarProps {
|
||||
problem: ZBXTrigger;
|
||||
problem: ProblemDTO;
|
||||
alerts?: ZBXAlert[];
|
||||
className?: string;
|
||||
}
|
||||
@@ -26,7 +26,11 @@ export default function ProblemStatusBar(props: ProblemStatusBarProps) {
|
||||
<ProblemStatusBarItem icon="wrench" fired={maintenance} tooltip="Host maintenance" />
|
||||
<ProblemStatusBarItem icon="globe" fired={link} link={link && problem.url} tooltip="External link" />
|
||||
<ProblemStatusBarItem icon="bullhorn" fired={multiEvent} tooltip="Trigger generates multiple problem events" />
|
||||
<ProblemStatusBarItem icon="tag" fired={closeByTag} tooltip={`OK event closes problems matched to tag: ${problem.correlation_tag}`} />
|
||||
<ProblemStatusBarItem
|
||||
icon="tag"
|
||||
fired={closeByTag}
|
||||
tooltip={`OK event closes problems matched to tag: ${problem.correlation_tag}`}
|
||||
/>
|
||||
<ProblemStatusBarItem icon="circle-o-notch" fired={actions} tooltip={actionMessage} />
|
||||
<ProblemStatusBarItem icon="question-circle" fired={stateUnknown} tooltip="Current trigger state is unknown" />
|
||||
<ProblemStatusBarItem icon="warning" fired={error} tooltip={problem.error} />
|
||||
@@ -56,5 +60,11 @@ function ProblemStatusBarItem(props: ProblemStatusBarItemProps) {
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return link ? <a href={link} target="_blank">{item}</a> : item;
|
||||
return link ? (
|
||||
<a href={link} target="_blank">
|
||||
{item}
|
||||
</a>
|
||||
) : (
|
||||
item
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { GFTimeRange, ZBXEvent, ZBXAcknowledge } from '../../types';
|
||||
import { ZBXEvent, ZBXAcknowledge } from '../../../datasource-zabbix/types';
|
||||
import { TimeRange } from '@grafana/data';
|
||||
|
||||
const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)';
|
||||
const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)';
|
||||
@@ -12,7 +13,7 @@ const EVENT_REGION_HEIGHT = Math.round(EVENT_POINT_SIZE * 0.6);
|
||||
|
||||
export interface ProblemTimelineProps {
|
||||
events: ZBXEvent[];
|
||||
timeRange: GFTimeRange;
|
||||
timeRange: TimeRange;
|
||||
okColor?: string;
|
||||
problemColor?: string;
|
||||
eventRegionHeight?: number;
|
||||
@@ -51,7 +52,7 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
|
||||
highlightedEvent: null,
|
||||
highlightedRegion: null,
|
||||
showEventInfo: false,
|
||||
eventInfo: {}
|
||||
eventInfo: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,11 +63,11 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
|
||||
}
|
||||
}
|
||||
|
||||
setRootRef = ref => {
|
||||
setRootRef = (ref) => {
|
||||
this.rootRef = ref;
|
||||
const width = ref && ref.clientWidth || 0;
|
||||
const width = (ref && ref.clientWidth) || 0;
|
||||
this.setState({ width });
|
||||
}
|
||||
};
|
||||
|
||||
handlePointHighlight = (index: number, secondIndex?: number) => {
|
||||
const event: ZBXEvent = this.sortedEvents[index];
|
||||
@@ -80,15 +81,15 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
|
||||
showEventInfo: true,
|
||||
highlightedRegion: regionToHighlight,
|
||||
eventInfo: {
|
||||
duration
|
||||
}
|
||||
duration,
|
||||
},
|
||||
});
|
||||
// this.showEventInfo(event);
|
||||
}
|
||||
};
|
||||
|
||||
handlePointUnHighlight = () => {
|
||||
this.setState({ showEventInfo: false, highlightedRegion: null });
|
||||
}
|
||||
};
|
||||
|
||||
handleAckHighlight = (ack: ZBXAcknowledge, index: number) => {
|
||||
this.setState({
|
||||
@@ -96,34 +97,34 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
|
||||
eventInfo: {
|
||||
timestamp: Number(ack.clock),
|
||||
message: ack.message,
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleAckUnHighlight = () => {
|
||||
this.setState({ showEventInfo: false });
|
||||
}
|
||||
};
|
||||
|
||||
showEventInfo = (event: ZBXEvent) => {
|
||||
this.setState({ highlightedEvent: event, showEventInfo: true });
|
||||
}
|
||||
};
|
||||
|
||||
hideEventInfo = () => {
|
||||
this.setState({ showEventInfo: false });
|
||||
}
|
||||
};
|
||||
|
||||
getRegionToHighlight = (index: number): number => {
|
||||
const event = this.sortedEvents[index];
|
||||
const regionToHighlight = event.value === '1' ? index + 1 : index;
|
||||
return regionToHighlight;
|
||||
}
|
||||
};
|
||||
|
||||
getEventDuration(firstIndex: number, secondIndex: number): number {
|
||||
return Math.abs(Number(this.sortedEvents[firstIndex].clock) - Number(this.sortedEvents[secondIndex].clock)) * 1000;
|
||||
}
|
||||
|
||||
sortEvents() {
|
||||
const events = _.sortBy(this.props.events, e => Number(e.clock));
|
||||
const events = _.sortBy(this.props.events, (e) => Number(e.clock));
|
||||
this.sortedEvents = events;
|
||||
return events;
|
||||
}
|
||||
@@ -137,7 +138,7 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
|
||||
}
|
||||
}
|
||||
}
|
||||
return _.sortBy(acks, ack => Number(ack.clock));
|
||||
return _.sortBy(acks, (ack) => Number(ack.clock));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -156,13 +157,14 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
|
||||
const timelineYpos = Math.round(boxHeight / 2 - eventPointSize / 2);
|
||||
|
||||
return (
|
||||
<div className="event-timeline" ref={this.setRootRef} style={{ transform: `translate(${-padding}px, 0)`}}>
|
||||
<TimelineInfoContainer className="timeline-info-container"
|
||||
<div className="event-timeline" ref={this.setRootRef} style={{ transform: `translate(${-padding}px, 0)` }}>
|
||||
<TimelineInfoContainer
|
||||
className="timeline-info-container"
|
||||
event={this.state.highlightedEvent}
|
||||
eventInfo={this.state.eventInfo}
|
||||
show={this.state.showEventInfo}
|
||||
left={padding}
|
||||
/>
|
||||
/>
|
||||
<svg className="event-timeline-canvas" viewBox={`0 0 ${boxWidth} ${boxHeight}`}>
|
||||
<defs>
|
||||
<TimelineSVGFilters />
|
||||
@@ -265,7 +267,7 @@ class TimelineInfoContainer extends PureComponent<TimelineInfoContainerProps> {
|
||||
<span key="ts" className="event-timestamp">
|
||||
<span className="event-timestamp-label">Time: </span>
|
||||
<span className="event-timestamp-value">{tsFormatted}</span>
|
||||
</span>
|
||||
</span>,
|
||||
];
|
||||
}
|
||||
if (eventInfo && eventInfo.duration) {
|
||||
@@ -285,7 +287,7 @@ class TimelineInfoContainer extends PureComponent<TimelineInfoContainerProps> {
|
||||
<span key="ts" className="event-timestamp">
|
||||
<span className="event-timestamp-label">Time: </span>
|
||||
<span className="event-timestamp-value">{tsFormatted}</span>
|
||||
</span>
|
||||
</span>,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -296,20 +298,16 @@ class TimelineInfoContainer extends PureComponent<TimelineInfoContainerProps> {
|
||||
|
||||
return (
|
||||
<div className={className} style={containerStyle}>
|
||||
<div>
|
||||
{infoItems}
|
||||
</div>
|
||||
<div>
|
||||
{durationItem}
|
||||
</div>
|
||||
{eventInfo && eventInfo.message &&
|
||||
<div>{infoItems}</div>
|
||||
<div>{durationItem}</div>
|
||||
{eventInfo && eventInfo.message && (
|
||||
<div>
|
||||
<span key="duration" className="event-timestamp">
|
||||
<span className="event-timestamp-label">Message: </span>
|
||||
<span className="event-timestamp-value">{eventInfo.message}</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -317,7 +315,7 @@ class TimelineInfoContainer extends PureComponent<TimelineInfoContainerProps> {
|
||||
|
||||
interface TimelineRegionsProps {
|
||||
events: ZBXEvent[];
|
||||
timeRange: GFTimeRange;
|
||||
timeRange: TimeRange;
|
||||
width: number;
|
||||
height: number;
|
||||
okColor?: string;
|
||||
@@ -332,7 +330,8 @@ class TimelineRegions extends PureComponent<TimelineRegionsProps> {
|
||||
|
||||
render() {
|
||||
const { events, timeRange, width, height, highlightedRegion } = this.props;
|
||||
const { timeFrom, timeTo } = timeRange;
|
||||
const timeFrom = timeRange.from.unix();
|
||||
const timeTo = timeRange.to.unix();
|
||||
const range = timeTo - timeFrom;
|
||||
|
||||
let firstItem: React.ReactNode;
|
||||
@@ -349,9 +348,7 @@ class TimelineRegions extends PureComponent<TimelineRegionsProps> {
|
||||
width: regionWidth,
|
||||
height: height,
|
||||
};
|
||||
firstItem = (
|
||||
<rect key='0' className={className} {...firstEventAttributes}></rect>
|
||||
);
|
||||
firstItem = <rect key="0" className={className} {...firstEventAttributes}></rect>;
|
||||
}
|
||||
|
||||
const eventsIntervalItems = events.map((event, index) => {
|
||||
@@ -359,7 +356,7 @@ class TimelineRegions extends PureComponent<TimelineRegionsProps> {
|
||||
const nextTs = index < events.length - 1 ? Number(events[index + 1].clock) : timeTo;
|
||||
const duration = (nextTs - ts) / range;
|
||||
const regionWidth = Math.round(duration * width);
|
||||
const posLeft = Math.round((ts - timeFrom) / range * width);
|
||||
const posLeft = Math.round(((ts - timeFrom) / range) * width);
|
||||
const highlighted = highlightedRegion && highlightedRegion - 1 === index;
|
||||
const valueClass = `problem-event--${event.value === '1' ? 'problem' : 'ok'}`;
|
||||
const className = `problem-event-region ${valueClass} ${highlighted ? 'highlighted' : ''}`;
|
||||
@@ -370,21 +367,16 @@ class TimelineRegions extends PureComponent<TimelineRegionsProps> {
|
||||
height: height,
|
||||
};
|
||||
|
||||
return (
|
||||
<rect key={`${event.eventid}-${index}`} className={className} {...attributes} />
|
||||
);
|
||||
return <rect key={`${event.eventid}-${index}`} className={className} {...attributes} />;
|
||||
});
|
||||
|
||||
return [
|
||||
firstItem,
|
||||
eventsIntervalItems
|
||||
];
|
||||
return [firstItem, eventsIntervalItems];
|
||||
}
|
||||
}
|
||||
|
||||
interface TimelinePointsProps {
|
||||
events: ZBXEvent[];
|
||||
timeRange: GFTimeRange;
|
||||
timeRange: TimeRange;
|
||||
width: number;
|
||||
pointSize: number;
|
||||
okColor?: string;
|
||||
@@ -415,7 +407,7 @@ class TimelinePoints extends PureComponent<TimelinePointsProps, TimelinePointsSt
|
||||
order = moveToEnd(order, indexes);
|
||||
const highlighted = highlight ? indexes : null;
|
||||
this.setState({ order, highlighted });
|
||||
}
|
||||
};
|
||||
|
||||
highlightPoint = (index: number) => () => {
|
||||
let pointsToHighlight = [index];
|
||||
@@ -429,12 +421,12 @@ class TimelinePoints extends PureComponent<TimelinePointsProps, TimelinePointsSt
|
||||
}
|
||||
}
|
||||
this.bringToFront(pointsToHighlight, true);
|
||||
}
|
||||
};
|
||||
|
||||
getRegionEvents(index: number) {
|
||||
const events = this.props.events;
|
||||
const event = events[index];
|
||||
if (event.value === '1' && index < events.length ) {
|
||||
if (event.value === '1' && index < events.length) {
|
||||
// Problem event
|
||||
for (let i = index; i < events.length; i++) {
|
||||
if (events[i].value === '0') {
|
||||
@@ -459,22 +451,23 @@ class TimelinePoints extends PureComponent<TimelinePointsProps, TimelinePointsSt
|
||||
return [index];
|
||||
}
|
||||
|
||||
unHighlightPoint = index => () => {
|
||||
unHighlightPoint = (index) => () => {
|
||||
if (this.props.onPointUnHighlight) {
|
||||
this.props.onPointUnHighlight();
|
||||
}
|
||||
const order = this.props.events.map((v, i) => i);
|
||||
this.setState({ order, highlighted: [] });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { events, timeRange, width, pointSize } = this.props;
|
||||
const { timeFrom, timeTo } = timeRange;
|
||||
const timeFrom = timeRange.from.unix();
|
||||
const timeTo = timeRange.to.unix();
|
||||
const range = timeTo - timeFrom;
|
||||
const pointR = pointSize / 2;
|
||||
const eventsItems = events.map((event, i) => {
|
||||
const ts = Number(event.clock);
|
||||
const posLeft = Math.round((ts - timeFrom) / range * width - pointR);
|
||||
const posLeft = Math.round(((ts - timeFrom) / range) * width - pointR);
|
||||
const className = `problem-event-item problem-event--${event.value === '1' ? 'problem' : 'ok'}`;
|
||||
const highlighted = this.state.highlighted.indexOf(i) !== -1;
|
||||
|
||||
@@ -491,7 +484,7 @@ class TimelinePoints extends PureComponent<TimelinePointsProps, TimelinePointsSt
|
||||
);
|
||||
});
|
||||
if (this.state.order.length) {
|
||||
return this.state.order.map(i => eventsItems[i]);
|
||||
return this.state.order.map((i) => eventsItems[i]);
|
||||
}
|
||||
return eventsItems;
|
||||
}
|
||||
@@ -528,13 +521,13 @@ class TimelinePoint extends PureComponent<TimelinePointProps, TimelinePointState
|
||||
if (this.props.onPointHighlight) {
|
||||
this.props.onPointHighlight();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseLeave = () => {
|
||||
if (this.props.onPointUnHighlight) {
|
||||
this.props.onPointUnHighlight();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { x } = this.props;
|
||||
@@ -543,11 +536,13 @@ class TimelinePoint extends PureComponent<TimelinePointProps, TimelinePointState
|
||||
const rInner = Math.round(r * INNER_POINT_SIZE);
|
||||
const className = `${this.props.className || ''} ${this.state.highlighted ? 'highlighted' : ''}`;
|
||||
return (
|
||||
<g className={className}
|
||||
<g
|
||||
className={className}
|
||||
transform={`translate(${cx}, 0)`}
|
||||
filter="url(#dropShadow)"
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseLeave={this.handleMouseLeave}>
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<circle cx={0} cy={0} r={r} className="point-border" />
|
||||
<circle cx={0} cy={0} r={rInner} className="point-core" />
|
||||
</g>
|
||||
@@ -557,7 +552,7 @@ class TimelinePoint extends PureComponent<TimelinePointProps, TimelinePointState
|
||||
|
||||
interface TimelineAcksProps {
|
||||
acknowledges: ZBXAcknowledge[];
|
||||
timeRange: GFTimeRange;
|
||||
timeRange: TimeRange;
|
||||
width: number;
|
||||
size: number;
|
||||
onHighlight?: (ack: ZBXAcknowledge, index: number) => void;
|
||||
@@ -581,7 +576,7 @@ class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
|
||||
this.props.onHighlight(ack, index);
|
||||
}
|
||||
this.bringToFront(index, true);
|
||||
}
|
||||
};
|
||||
|
||||
handleUnHighlight = () => {
|
||||
if (this.props.onUnHighlight) {
|
||||
@@ -589,7 +584,7 @@ class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
|
||||
}
|
||||
const order = this.props.acknowledges.map((v, i) => i);
|
||||
this.setState({ order, highlighted: null });
|
||||
}
|
||||
};
|
||||
|
||||
bringToFront = (index: number, highlight = false) => {
|
||||
const { acknowledges } = this.props;
|
||||
@@ -597,16 +592,17 @@ class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
|
||||
order = moveToEnd(order, [index]);
|
||||
const highlighted = highlight ? index : null;
|
||||
this.setState({ order, highlighted });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { acknowledges, timeRange, width, size } = this.props;
|
||||
const { timeFrom, timeTo } = timeRange;
|
||||
const timeFrom = timeRange.from.unix();
|
||||
const timeTo = timeRange.to.unix();
|
||||
const range = timeTo - timeFrom;
|
||||
const pointR = size / 2;
|
||||
const eventsItems = acknowledges.map((ack, i) => {
|
||||
const ts = Number(ack.clock);
|
||||
const posLeft = Math.round((ts - timeFrom) / range * width - pointR);
|
||||
const posLeft = Math.round(((ts - timeFrom) / range) * width - pointR);
|
||||
const highlighted = this.state.highlighted === i;
|
||||
|
||||
return (
|
||||
@@ -621,7 +617,7 @@ class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
|
||||
);
|
||||
});
|
||||
if (this.state.order.length) {
|
||||
return this.state.order.map(i => eventsItems[i]);
|
||||
return this.state.order.map((i) => eventsItems[i]);
|
||||
}
|
||||
return eventsItems;
|
||||
}
|
||||
@@ -656,13 +652,13 @@ class TimelineAck extends PureComponent<TimelineAckProps, TimelineAckState> {
|
||||
if (this.props.onHighlight) {
|
||||
this.props.onHighlight();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleUnHighlight = () => {
|
||||
if (this.props.onUnHighlight) {
|
||||
this.props.onUnHighlight();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { x } = this.props;
|
||||
@@ -671,11 +667,13 @@ class TimelineAck extends PureComponent<TimelineAckProps, TimelineAckState> {
|
||||
const rInner = Math.round(r * INNER_POINT_SIZE);
|
||||
const className = `problem-event-ack ${this.state.highlighted ? 'highlighted' : ''}`;
|
||||
return (
|
||||
<g className={className}
|
||||
<g
|
||||
className={className}
|
||||
transform={`translate(${cx}, 0)`}
|
||||
filter="url(#dropShadow)"
|
||||
onMouseOver={this.handleHighlight}
|
||||
onMouseLeave={this.handleUnHighlight}>
|
||||
onMouseLeave={this.handleUnHighlight}
|
||||
>
|
||||
<circle cx={0} cy={0} r={r} className="point-border" />
|
||||
<circle cx={0} cy={0} r={rInner} className="point-core" />
|
||||
</g>
|
||||
|
||||
@@ -8,17 +8,17 @@ import EventTag from '../EventTag';
|
||||
import { ProblemDetails } from './ProblemDetails';
|
||||
import { AckProblemData } from '../AckModal';
|
||||
import { FAIcon, GFHeartIcon } from '../../../components';
|
||||
import { GFTimeRange, ProblemsPanelOptions, RTCell, RTResized, TriggerSeverity } from '../../types';
|
||||
import { ProblemsPanelOptions, RTCell, RTResized, TriggerSeverity } from '../../types';
|
||||
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXTag } from '../../../datasource-zabbix/types';
|
||||
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types';
|
||||
import { AckCell } from './AckCell';
|
||||
import { DataSourceRef, TimeRange } from "@grafana/data";
|
||||
import { DataSourceRef, TimeRange } from '@grafana/data';
|
||||
|
||||
export interface ProblemListProps {
|
||||
problems: ProblemDTO[];
|
||||
panelOptions: ProblemsPanelOptions;
|
||||
loading?: boolean;
|
||||
timeRange?: GFTimeRange;
|
||||
timeRange?: TimeRange;
|
||||
range?: TimeRange;
|
||||
pageSize?: number;
|
||||
fontSize?: number;
|
||||
@@ -52,7 +52,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
||||
};
|
||||
}
|
||||
|
||||
setRootRef = ref => {
|
||||
setRootRef = (ref) => {
|
||||
this.rootRef = ref;
|
||||
};
|
||||
|
||||
@@ -60,8 +60,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
||||
return this.props.onProblemAck(problem, data);
|
||||
};
|
||||
|
||||
onExecuteScript = (problem: ProblemDTO, data: AckProblemData) => {
|
||||
};
|
||||
onExecuteScript = (problem: ProblemDTO, data: AckProblemData) => {};
|
||||
|
||||
handlePageSizeChange = (pageSize, pageIndex) => {
|
||||
if (this.props.onPageSizeChange) {
|
||||
@@ -132,10 +131,12 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
||||
const result = [];
|
||||
const options = this.props.panelOptions;
|
||||
const highlightNewerThan = options.highlightNewEvents && options.highlightNewerThan;
|
||||
const statusCell = props => StatusCell(props, highlightNewerThan);
|
||||
const statusIconCell = props => StatusIconCell(props, highlightNewerThan);
|
||||
const hostNameCell = props => <HostCell name={props.original.host} maintenance={props.original.maintenance}/>;
|
||||
const hostTechNameCell = props => <HostCell name={props.original.hostTechName} maintenance={props.original.maintenance}/>;
|
||||
const statusCell = (props) => StatusCell(props, highlightNewerThan);
|
||||
const statusIconCell = (props) => StatusIconCell(props, highlightNewerThan);
|
||||
const hostNameCell = (props) => <HostCell name={props.original.host} maintenance={props.original.maintenance} />;
|
||||
const hostTechNameCell = (props) => (
|
||||
<HostCell name={props.original.hostTechName} maintenance={props.original.maintenance} />
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{ Header: 'Host', id: 'host', show: options.hostField, Cell: hostNameCell },
|
||||
@@ -143,35 +144,62 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
||||
{ Header: 'Host Groups', accessor: 'groups', show: options.hostGroups, Cell: GroupCell },
|
||||
{ Header: 'Proxy', accessor: 'proxy', show: options.hostProxy },
|
||||
{
|
||||
Header: 'Severity', show: options.severityField, className: 'problem-severity', width: 120,
|
||||
accessor: problem => problem.priority,
|
||||
Header: 'Severity',
|
||||
show: options.severityField,
|
||||
className: 'problem-severity',
|
||||
width: 120,
|
||||
accessor: (problem) => problem.priority,
|
||||
id: 'severity',
|
||||
Cell: props => SeverityCell(props, options.triggerSeverity, options.markAckEvents, options.ackEventColor, options.okEventColor),
|
||||
Cell: (props) =>
|
||||
SeverityCell(
|
||||
props,
|
||||
options.triggerSeverity,
|
||||
options.markAckEvents,
|
||||
options.ackEventColor,
|
||||
options.okEventColor
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: '', id: 'statusIcon', show: options.statusIcon, className: 'problem-status-icon', width: 50,
|
||||
Header: '',
|
||||
id: 'statusIcon',
|
||||
show: options.statusIcon,
|
||||
className: 'problem-status-icon',
|
||||
width: 50,
|
||||
accessor: 'value',
|
||||
Cell: statusIconCell,
|
||||
},
|
||||
{ Header: 'Status', accessor: 'value', show: options.statusField, width: 100, Cell: statusCell },
|
||||
{ Header: 'Problem', accessor: 'description', minWidth: 200, Cell: ProblemCell },
|
||||
{
|
||||
Header: 'Ack', id: 'ack', show: options.ackField, width: 70,
|
||||
Cell: props => <AckCell {...props} />
|
||||
Header: 'Ack',
|
||||
id: 'ack',
|
||||
show: options.ackField,
|
||||
width: 70,
|
||||
Cell: (props) => <AckCell {...props} />,
|
||||
},
|
||||
{
|
||||
Header: 'Tags', accessor: 'tags', show: options.showTags, className: 'problem-tags',
|
||||
Cell: props => <TagCell {...props} onTagClick={this.handleTagClick}/>
|
||||
Header: 'Tags',
|
||||
accessor: 'tags',
|
||||
show: options.showTags,
|
||||
className: 'problem-tags',
|
||||
Cell: (props) => <TagCell {...props} onTagClick={this.handleTagClick} />,
|
||||
},
|
||||
{
|
||||
Header: 'Age', className: 'problem-age', width: 100, show: options.ageField, accessor: 'timestamp',
|
||||
Header: 'Age',
|
||||
className: 'problem-age',
|
||||
width: 100,
|
||||
show: options.ageField,
|
||||
accessor: 'timestamp',
|
||||
id: 'age',
|
||||
Cell: AgeCell,
|
||||
},
|
||||
{
|
||||
Header: 'Time', className: 'last-change', width: 150, accessor: 'timestamp',
|
||||
Header: 'Time',
|
||||
className: 'last-change',
|
||||
width: 150,
|
||||
accessor: 'timestamp',
|
||||
id: 'lastchange',
|
||||
Cell: props => LastChangeCell(props, options.customLastChangeFormat && options.lastChangeFormat),
|
||||
Cell: (props) => LastChangeCell(props, options.customLastChangeFormat && options.lastChangeFormat),
|
||||
},
|
||||
{ Header: '', className: 'custom-expander', width: 60, expander: true, Expander: CustomExpander },
|
||||
];
|
||||
@@ -207,25 +235,25 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
|
||||
minRows={0}
|
||||
loading={this.props.loading}
|
||||
noDataText="No problems found"
|
||||
SubComponent={props =>
|
||||
<ProblemDetails {...props}
|
||||
rootWidth={this.rootWidth}
|
||||
timeRange={this.props.timeRange}
|
||||
range={this.props.range}
|
||||
showTimeline={panelOptions.problemTimeline}
|
||||
panelId={this.props.panelId}
|
||||
getProblemEvents={this.props.getProblemEvents}
|
||||
getProblemAlerts={this.props.getProblemAlerts}
|
||||
getScripts={this.props.getScripts}
|
||||
onProblemAck={this.handleProblemAck}
|
||||
onExecuteScript={this.props.onExecuteScript}
|
||||
onTagClick={this.handleTagClick}
|
||||
subRows={false}
|
||||
SubComponent={(props) => (
|
||||
<ProblemDetails
|
||||
{...props}
|
||||
rootWidth={this.rootWidth}
|
||||
timeRange={this.props.timeRange}
|
||||
showTimeline={panelOptions.problemTimeline}
|
||||
panelId={this.props.panelId}
|
||||
getProblemEvents={this.props.getProblemEvents}
|
||||
getProblemAlerts={this.props.getProblemAlerts}
|
||||
getScripts={this.props.getScripts}
|
||||
onProblemAck={this.handleProblemAck}
|
||||
onExecuteScript={this.props.onExecuteScript}
|
||||
onTagClick={this.handleTagClick}
|
||||
subRows={false}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
expanded={this.getExpandedPage(this.state.page)}
|
||||
onExpandedChange={this.handleExpandedChange}
|
||||
onPageChange={page => this.setState({ page })}
|
||||
onPageChange={(page) => this.setState({ page })}
|
||||
onPageSizeChange={this.handlePageSizeChange}
|
||||
onResizedChange={this.handleResizedChange}
|
||||
/>
|
||||
@@ -243,7 +271,7 @@ const HostCell: React.FC<HostCellProps> = ({ name, maintenance }) => {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ paddingRight: '0.4rem' }}>{name}</span>
|
||||
{maintenance && <FAIcon customClass="fired" icon="wrench"/>}
|
||||
{maintenance && <FAIcon customClass="fired" icon="wrench" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -260,15 +288,15 @@ function SeverityCell(
|
||||
|
||||
let severityDesc: TriggerSeverity;
|
||||
const severity = Number(problem.severity);
|
||||
severityDesc = _.find(problemSeverityDesc, s => s.priority === severity);
|
||||
severityDesc = _.find(problemSeverityDesc, (s) => s.priority === severity);
|
||||
if (problem.severity && problem.value === '1') {
|
||||
severityDesc = _.find(problemSeverityDesc, s => s.priority === severity);
|
||||
severityDesc = _.find(problemSeverityDesc, (s) => s.priority === severity);
|
||||
}
|
||||
|
||||
color = problem.value === '0' ? okColor : severityDesc.color;
|
||||
|
||||
// Mark acknowledged triggers with different color
|
||||
if (markAckEvents && problem.acknowledged === "1") {
|
||||
if (markAckEvents && problem.acknowledged === '1') {
|
||||
color = ackEventColor;
|
||||
}
|
||||
|
||||
@@ -290,7 +318,9 @@ function StatusCell(props: RTCell<ProblemDTO>, highlightNewerThan?: string) {
|
||||
newProblem = isNewProblem(props.original, highlightNewerThan);
|
||||
}
|
||||
return (
|
||||
<span className={newProblem ? 'problem-status--new' : ''} style={{ color }}>{status}</span>
|
||||
<span className={newProblem ? 'problem-status--new' : ''} style={{ color }}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,22 +330,21 @@ function StatusIconCell(props: RTCell<ProblemDTO>, highlightNewerThan?: string)
|
||||
if (highlightNewerThan) {
|
||||
newProblem = isNewProblem(props.original, highlightNewerThan);
|
||||
}
|
||||
const className = classNames('zbx-problem-status-icon',
|
||||
const className = classNames(
|
||||
'zbx-problem-status-icon',
|
||||
{ 'problem-status--new': newProblem },
|
||||
{ 'zbx-problem': props.value === '1' },
|
||||
{ 'zbx-ok': props.value === '0' },
|
||||
{ 'zbx-ok': props.value === '0' }
|
||||
);
|
||||
return <GFHeartIcon status={status} className={className}/>;
|
||||
return <GFHeartIcon status={status} className={className} />;
|
||||
}
|
||||
|
||||
function GroupCell(props: RTCell<ProblemDTO>) {
|
||||
let groups = "";
|
||||
let groups = '';
|
||||
if (props.value && props.value.length) {
|
||||
groups = props.value.map(g => g.name).join(', ');
|
||||
groups = props.value.map((g) => g.name).join(', ');
|
||||
}
|
||||
return (
|
||||
<span>{groups}</span>
|
||||
);
|
||||
return <span>{groups}</span>;
|
||||
}
|
||||
|
||||
function ProblemCell(props: RTCell<ProblemDTO>) {
|
||||
@@ -336,7 +365,7 @@ function AgeCell(props: RTCell<ProblemDTO>) {
|
||||
}
|
||||
|
||||
function LastChangeCell(props: RTCell<ProblemDTO>, customFormat?: string) {
|
||||
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
||||
const DEFAULT_TIME_FORMAT = 'DD MMM YYYY HH:mm:ss';
|
||||
const problem = props.original;
|
||||
const timestamp = moment.unix(problem.timestamp);
|
||||
const format = customFormat || DEFAULT_TIME_FORMAT;
|
||||
@@ -358,14 +387,21 @@ class TagCell extends PureComponent<TagCellProps> {
|
||||
render() {
|
||||
const tags = this.props.value || [];
|
||||
return [
|
||||
tags.map(tag => <EventTag key={tag.tag + tag.value} tag={tag} datasource={this.props.original.datasource} onClick={this.handleTagClick}/>)
|
||||
tags.map((tag) => (
|
||||
<EventTag
|
||||
key={tag.tag + tag.value}
|
||||
tag={tag}
|
||||
datasource={this.props.original.datasource}
|
||||
onClick={this.handleTagClick}
|
||||
/>
|
||||
)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function CustomExpander(props: RTCell<any>) {
|
||||
return (
|
||||
<span className={props.isExpanded ? "expanded" : ""}>
|
||||
<span className={props.isExpanded ? 'expanded' : ''}>
|
||||
<i className="fa fa-info-circle"></i>
|
||||
</span>
|
||||
);
|
||||
|
||||
12
src/panel-triggers/components/ResetColumnsEditor.tsx
Normal file
12
src/panel-triggers/components/ResetColumnsEditor.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { StandardEditorProps } from '@grafana/data';
|
||||
|
||||
type Props = StandardEditorProps<any>;
|
||||
export const ResetColumnsEditor = ({ onChange }: Props): JSX.Element => {
|
||||
return (
|
||||
<Button variant="secondary" onClick={() => onChange([])}>
|
||||
Reset columns
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +1,20 @@
|
||||
import _ from 'lodash';
|
||||
import { getNextRefIdChar } from './utils';
|
||||
import { ShowProblemTypes } from '../datasource-zabbix/types';
|
||||
import { ProblemsPanelOptions } from './types';
|
||||
import { PanelModel } from '@grafana/data';
|
||||
|
||||
// Actual schema version
|
||||
export const CURRENT_SCHEMA_VERSION = 8;
|
||||
|
||||
export const getDefaultTarget = (targets?) => {
|
||||
return {
|
||||
group: {filter: ""},
|
||||
host: {filter: ""},
|
||||
application: {filter: ""},
|
||||
trigger: {filter: ""},
|
||||
tags: {filter: ""},
|
||||
proxy: {filter: ""},
|
||||
group: { filter: '' },
|
||||
host: { filter: '' },
|
||||
application: { filter: '' },
|
||||
trigger: { filter: '' },
|
||||
tags: { filter: '' },
|
||||
proxy: { filter: '' },
|
||||
refId: getNextRefIdChar(targets),
|
||||
};
|
||||
};
|
||||
@@ -105,7 +107,7 @@ export function migratePanelSchema(panel) {
|
||||
target.options = migrateOptions(panel);
|
||||
|
||||
_.defaults(target.options, getDefaultTargetOptions());
|
||||
_.defaults(target, { tags: { filter: "" } });
|
||||
_.defaults(target, { tags: { filter: '' } });
|
||||
}
|
||||
|
||||
panel.sortProblems = panel.sortTriggersBy?.value === 'priority' ? 'priority' : 'lastchange';
|
||||
@@ -161,7 +163,7 @@ function isEmptyPanel(panel) {
|
||||
}
|
||||
|
||||
function isEmptyTargets(targets) {
|
||||
return !targets || (_.isArray(targets) && (targets.length === 0 || targets.length === 1 && _.isEmpty(targets[0])));
|
||||
return !targets || (_.isArray(targets) && (targets.length === 0 || (targets.length === 1 && _.isEmpty(targets[0]))));
|
||||
}
|
||||
|
||||
function isDefaultPanel(panel) {
|
||||
@@ -169,7 +171,13 @@ function isDefaultPanel(panel) {
|
||||
}
|
||||
|
||||
function isDefaultTarget(target) {
|
||||
return !target.group?.filter && !target.host?.filter && !target.application?.filter && !target.trigger?.filter && !target.queryType;
|
||||
return (
|
||||
!target.group?.filter &&
|
||||
!target.host?.filter &&
|
||||
!target.application?.filter &&
|
||||
!target.trigger?.filter &&
|
||||
!target.queryType
|
||||
);
|
||||
}
|
||||
|
||||
function isEmptyTarget(target) {
|
||||
@@ -179,3 +187,53 @@ function isEmptyTarget(target) {
|
||||
function isInvalidTarget(target, targetKey) {
|
||||
return target && target.refId === 'A' && targetKey === '0';
|
||||
}
|
||||
|
||||
// This is called when the panel changes from another panel
|
||||
export const problemsPanelMigrationHandler = (panel: PanelModel<Partial<ProblemsPanelOptions>> | any) => {
|
||||
let options = (panel.options ?? {}) as ProblemsPanelOptions;
|
||||
const legacyOptions: Partial<ProblemsPanelOptions> = {
|
||||
layout: panel.layout,
|
||||
hostField: panel.hostField,
|
||||
hostTechNameField: panel.hostTechNameField,
|
||||
hostGroups: panel.hostGroups,
|
||||
hostProxy: panel.hostProxy,
|
||||
showTags: panel.showTags,
|
||||
statusField: panel.statusField,
|
||||
statusIcon: panel.statusIcon,
|
||||
severityField: panel.severityField,
|
||||
ackField: panel.ackField,
|
||||
ageField: panel.ageField,
|
||||
descriptionField: panel.descriptionField,
|
||||
descriptionAtNewLine: panel.descriptionAtNewLine,
|
||||
hostsInMaintenance: panel.hostsInMaintenance,
|
||||
showTriggers: panel.showTriggers,
|
||||
sortProblems: panel.sortProblems,
|
||||
limit: panel.limit,
|
||||
fontSize: panel.fontSize,
|
||||
pageSize: panel.pageSize,
|
||||
problemTimeline: panel.problemTimeline,
|
||||
highlightBackground: panel.highlightBackground,
|
||||
highlightNewEvents: panel.highlightNewEvents,
|
||||
highlightNewerThan: panel.highlightNewerThan,
|
||||
customLastChangeFormat: panel.customLastChangeFormat,
|
||||
lastChangeFormat: panel.lastChangeFormat,
|
||||
resizedColumns: panel.resizedColumns,
|
||||
triggerSeverity: panel.triggerSeverity,
|
||||
okEventColor: panel.okEventColor,
|
||||
ackEventColor: panel.ackEventColor,
|
||||
markAckEvents: panel.markAckEvents,
|
||||
showEvents: panel.showEvents?.value ?? panel.showEvents,
|
||||
};
|
||||
|
||||
return { ...legacyOptions, ...options };
|
||||
};
|
||||
|
||||
// This is called when the panel changes from another panel
|
||||
export const problemsPanelChangedHandler = (
|
||||
panel: PanelModel<Partial<ProblemsPanelOptions>> | any,
|
||||
prevPluginId: string,
|
||||
prevOptions: any
|
||||
) => {
|
||||
let options = (panel.options ?? {}) as ProblemsPanelOptions;
|
||||
return options;
|
||||
};
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Grafana-Zabbix
|
||||
* Zabbix plugin for Grafana.
|
||||
* http://github.com/alexanderzobnin/grafana-zabbix
|
||||
*
|
||||
* Trigger panel.
|
||||
* This feature sponsored by CORE IT
|
||||
* http://www.coreit.fr
|
||||
*
|
||||
* Copyright 2015 Alexander Zobnin alexanderzobnin@gmail.com
|
||||
* Licensed under the Apache License, Version 2.0
|
||||
*/
|
||||
|
||||
import { TriggerPanelCtrl } from './triggers_panel_ctrl';
|
||||
import { loadPluginCss } from 'grafana/app/plugins/sdk';
|
||||
|
||||
loadPluginCss({
|
||||
dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css',
|
||||
light: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.light.css'
|
||||
});
|
||||
|
||||
export {
|
||||
TriggerPanelCtrl as PanelCtrl
|
||||
};
|
||||
214
src/panel-triggers/module.tsx
Normal file
214
src/panel-triggers/module.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { PanelPlugin, StandardEditorProps } from '@grafana/data';
|
||||
import { problemsPanelChangedHandler, problemsPanelMigrationHandler } from './migrations';
|
||||
import { ProblemsPanel } from './ProblemsPanel';
|
||||
import { defaultPanelOptions, ProblemsPanelOptions } from './types';
|
||||
import { ResetColumnsEditor } from './components/ResetColumnsEditor';
|
||||
import { ProblemColorEditor } from './components/ProblemColorEditor';
|
||||
|
||||
export const plugin = new PanelPlugin<ProblemsPanelOptions, {}>(ProblemsPanel)
|
||||
.setPanelChangeHandler(problemsPanelChangedHandler)
|
||||
.setMigrationHandler(problemsPanelMigrationHandler)
|
||||
.setPanelOptions((builder) => {
|
||||
builder
|
||||
.addSelect({
|
||||
path: 'layout',
|
||||
name: 'Layout',
|
||||
defaultValue: defaultPanelOptions.layout,
|
||||
settings: {
|
||||
options: [
|
||||
{ label: 'Table', value: 'table' },
|
||||
{ label: 'List', value: 'list' },
|
||||
],
|
||||
},
|
||||
})
|
||||
.addSelect({
|
||||
path: 'sortProblems',
|
||||
name: 'Sort by',
|
||||
defaultValue: defaultPanelOptions.sortProblems,
|
||||
settings: {
|
||||
options: [
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Last change', value: 'lastchange' },
|
||||
{ label: 'Severity', value: 'priority' },
|
||||
],
|
||||
},
|
||||
})
|
||||
.addSelect({
|
||||
path: 'fontSize',
|
||||
name: 'Font size',
|
||||
defaultValue: defaultPanelOptions.fontSize,
|
||||
settings: {
|
||||
options: fontSizeOptions,
|
||||
},
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'pageSize',
|
||||
name: 'Page size',
|
||||
defaultValue: defaultPanelOptions.pageSize,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'problemTimeline',
|
||||
name: 'Problem timeline',
|
||||
defaultValue: defaultPanelOptions.problemTimeline,
|
||||
showIf: (options) => options.layout === 'table',
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'highlightBackground',
|
||||
name: 'Highlight background',
|
||||
defaultValue: defaultPanelOptions.highlightBackground,
|
||||
showIf: (options) => options.layout === 'list',
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'highlightNewEvents',
|
||||
name: 'Highlight new events',
|
||||
defaultValue: defaultPanelOptions.highlightNewEvents,
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'highlightNewerThan',
|
||||
name: 'Newer than',
|
||||
defaultValue: defaultPanelOptions.highlightNewerThan,
|
||||
showIf: (options) => options.highlightNewEvents,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'customLastChangeFormat',
|
||||
name: 'Custom last change format',
|
||||
defaultValue: defaultPanelOptions.customLastChangeFormat,
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'lastChangeFormat',
|
||||
name: 'Last change format',
|
||||
defaultValue: defaultPanelOptions.lastChangeFormat,
|
||||
description: 'See moment.js dosc for time format http://momentjs.com/docs/#/displaying/format/',
|
||||
settings: {
|
||||
placeholder: 'dddd, MMMM Do YYYY, h:mm:ss a',
|
||||
},
|
||||
showIf: (options) => options.customLastChangeFormat,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'resetColumns',
|
||||
path: 'resizedColumns',
|
||||
name: 'Reset resized columns',
|
||||
editor: ResetColumnsEditor,
|
||||
showIf: (options) => options.layout === 'table',
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'triggerColors',
|
||||
path: 'triggerSeverity',
|
||||
name: 'Problem colors',
|
||||
editor: ProblemColorEditor,
|
||||
category: ['Colors'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'markAckEvents',
|
||||
name: 'Mark acknowledged events',
|
||||
defaultValue: defaultPanelOptions.markAckEvents,
|
||||
category: ['Colors'],
|
||||
})
|
||||
.addColorPicker({
|
||||
path: 'ackEventColor',
|
||||
name: 'Acknowledged color',
|
||||
defaultValue: defaultPanelOptions.ackEventColor,
|
||||
showIf: (options) => options.markAckEvents,
|
||||
// enableNamedColors does not work now
|
||||
settings: [{ enableNamedColors: false }],
|
||||
category: ['Colors'],
|
||||
})
|
||||
.addColorPicker({
|
||||
path: 'okEventColor',
|
||||
name: 'OK event color',
|
||||
defaultValue: defaultPanelOptions.okEventColor,
|
||||
settings: [{ enableNamedColors: false }],
|
||||
category: ['Colors'],
|
||||
})
|
||||
|
||||
// Show/hide fields
|
||||
.addBooleanSwitch({
|
||||
path: 'hostField',
|
||||
name: 'Host name',
|
||||
defaultValue: defaultPanelOptions.hostField,
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'hostTechNameField',
|
||||
name: 'Technical name',
|
||||
defaultValue: defaultPanelOptions.hostTechNameField,
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'hostGroups',
|
||||
name: 'Host groups',
|
||||
defaultValue: defaultPanelOptions.hostGroups,
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'hostProxy',
|
||||
name: 'Host proxy',
|
||||
defaultValue: defaultPanelOptions.hostProxy,
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showTags',
|
||||
name: 'Tags',
|
||||
defaultValue: defaultPanelOptions.showTags,
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'statusField',
|
||||
name: 'Status',
|
||||
defaultValue: defaultPanelOptions.statusField,
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'statusIcon',
|
||||
name: 'Status icon',
|
||||
defaultValue: defaultPanelOptions.statusIcon,
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'severityField',
|
||||
name: 'Severity',
|
||||
defaultValue: defaultPanelOptions.severityField,
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'ackField',
|
||||
name: 'Ack',
|
||||
defaultValue: defaultPanelOptions.ackField,
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'ageField',
|
||||
name: 'Age',
|
||||
defaultValue: defaultPanelOptions.ageField,
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'descriptionField',
|
||||
name: 'Description',
|
||||
defaultValue: defaultPanelOptions.descriptionField,
|
||||
showIf: (options) => options.layout === 'list',
|
||||
category: ['Fields'],
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'descriptionAtNewLine',
|
||||
name: 'At the new line',
|
||||
defaultValue: defaultPanelOptions.descriptionAtNewLine,
|
||||
showIf: (options) => options.layout === 'list',
|
||||
category: ['Fields'],
|
||||
});
|
||||
});
|
||||
|
||||
const fontSizeOptions = [
|
||||
{ label: '80%', value: '80%' },
|
||||
{ label: '90%', value: '90%' },
|
||||
{ label: '100%', value: '100%' },
|
||||
{ label: '110%', value: '110%' },
|
||||
{ label: '120%', value: '120%' },
|
||||
{ label: '130%', value: '130%' },
|
||||
{ label: '150%', value: '150%' },
|
||||
{ label: '160%', value: '160%' },
|
||||
{ label: '180%', value: '180%' },
|
||||
{ label: '200%', value: '200%' },
|
||||
{ label: '220%', value: '220%' },
|
||||
{ label: '250%', value: '250%' },
|
||||
];
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Grafana-Zabbix
|
||||
* Zabbix plugin for Grafana.
|
||||
* http://github.com/alexanderzobnin/grafana-zabbix
|
||||
*
|
||||
* Trigger panel.
|
||||
* This feature sponsored by CORE IT
|
||||
* http://www.coreit.fr
|
||||
*
|
||||
* Copyright 2015 Alexander Zobnin alexanderzobnin@gmail.com
|
||||
* Licensed under the Apache License, Version 2.0
|
||||
*/
|
||||
|
||||
class TriggerPanelOptionsCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope) {
|
||||
$scope.editor = this;
|
||||
this.panelCtrl = $scope.ctrl;
|
||||
this.panel = this.panelCtrl.panel;
|
||||
|
||||
this.layouts = [
|
||||
{ text: 'Table', value: 'table' },
|
||||
{ text: 'List', value: 'list' }
|
||||
];
|
||||
this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
|
||||
this.ackFilters = [
|
||||
'all triggers',
|
||||
'unacknowledged',
|
||||
'acknowledged'
|
||||
];
|
||||
|
||||
this.sortingOptions = [
|
||||
{ text: 'Default', value: 'default' },
|
||||
{ text: 'Last change', value: 'lastchange' },
|
||||
{ text: 'Severity', value: 'priority' },
|
||||
];
|
||||
|
||||
this.showEventsFields = [
|
||||
{ text: 'All', value: [0,1] },
|
||||
{ text: 'OK', value: [0] },
|
||||
{ text: 'Problems', value: 1 }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerPanelOptionsTab() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: true,
|
||||
templateUrl: 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/options_tab.html',
|
||||
controller: TriggerPanelOptionsCtrl,
|
||||
};
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<div class="triggers-panel-container"></div>
|
||||
@@ -1,237 +0,0 @@
|
||||
<div class="editor-row">
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Fields</h5>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Host name"
|
||||
checked="ctrl.panel.hostField"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Technical name"
|
||||
checked="ctrl.panel.hostTechNameField"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Host groups"
|
||||
checked="ctrl.panel.hostGroups"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Host proxy"
|
||||
checked="ctrl.panel.hostProxy"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Tags"
|
||||
checked="ctrl.panel.showTags"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Status"
|
||||
checked="ctrl.panel.statusField"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch ng-if="ctrl.panel.layout === 'table'"
|
||||
class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Status Icon"
|
||||
checked="ctrl.panel.statusIcon"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Severity"
|
||||
checked="ctrl.panel.severityField"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Ack"
|
||||
checked="ctrl.panel.ackField"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Age"
|
||||
checked="ctrl.panel.ageField"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch ng-if="ctrl.panel.layout === 'list'"
|
||||
class="gf-form"
|
||||
label-class="width-9"
|
||||
label="Description"
|
||||
checked="ctrl.panel.descriptionField"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch ng-if="ctrl.panel.descriptionField && ctrl.panel.layout === 'list'"
|
||||
class="gf-form"
|
||||
label-class="width-9"
|
||||
label="At the new line"
|
||||
checked="ctrl.panel.descriptionAtNewLine"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">View options</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Layout</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input"
|
||||
ng-model="ctrl.panel.layout"
|
||||
ng-options="opt.value as opt.text for opt in editor.layouts"
|
||||
ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Sort by</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input"
|
||||
ng-model="ctrl.panel.sortProblems"
|
||||
ng-options="f.value as f.text for f in editor.sortingOptions"
|
||||
ng-change="ctrl.reRenderProblems()">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Font size</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input"
|
||||
ng-model="ctrl.panel.fontSize"
|
||||
ng-options="f for f in editor.fontSizes"
|
||||
ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Page size</label>
|
||||
<input class="gf-form-input width-8"
|
||||
type="number" placeholder="10"
|
||||
ng-model="ctrl.panel.pageSize"
|
||||
ng-model-onblur ng-change="ctrl.render()">
|
||||
</div>
|
||||
<gf-form-switch ng-if="ctrl.panel.layout === 'table'"
|
||||
class="gf-form"
|
||||
label-class="width-10"
|
||||
label="Problem timeline"
|
||||
checked="ctrl.panel.problemTimeline"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch ng-if="ctrl.panel.layout !== 'table'"
|
||||
class="gf-form"
|
||||
label-class="width-10"
|
||||
label="Highlight background"
|
||||
checked="ctrl.panel.highlightBackground"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-10"
|
||||
label="Highlight new events"
|
||||
checked="ctrl.panel.highlightNewEvents"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Newer than</label>
|
||||
<input class="gf-form-input width-8" placeholder="1h"
|
||||
ng-model="ctrl.panel.highlightNewerThan"
|
||||
ng-model-onblur ng-change="ctrl.render()">
|
||||
</div>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-16"
|
||||
label="Custom Last change format"
|
||||
checked="ctrl.panel.customLastChangeFormat"
|
||||
on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form" ng-if="ctrl.panel.customLastChangeFormat">
|
||||
<label class="gf-form-label width-3">
|
||||
<a href="http://momentjs.com/docs/#/displaying/format/" target="_blank">
|
||||
<tip>See moment.js dosc for time format.</tip>
|
||||
</a>
|
||||
</label>
|
||||
<input class="gf-form-input width-18"
|
||||
type="text"
|
||||
placeholder="dddd, MMMM Do YYYY, h:mm:ss a"
|
||||
empty-to-null
|
||||
ng-model-onblur
|
||||
ng-model="ctrl.panel.lastChangeFormat"
|
||||
ng-change="ctrl.render()">
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.panel.layout === 'table'">
|
||||
<div class="gf-form-button">
|
||||
<button class="btn btn-inverse width-16 panel-options-button" ng-click="ctrl.resetResizedColumns()">
|
||||
Reset resized columns
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Problems severity and colors</h5>
|
||||
<div class="gf-form-inline" ng-repeat="trigger in ctrl.panel.triggerSeverity">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-3">{{ trigger.priority }}</label>
|
||||
<label class="gf-form-label triggers-severity-config"
|
||||
ng-style="{color: trigger.color}">
|
||||
<i class="icon-gf" ng-class="ctrl.getAlertIconClassBySeverity(trigger)"></i>
|
||||
</label>
|
||||
<input type="text"
|
||||
class="gf-form-input width-12"
|
||||
empty-to-null
|
||||
ng-model="trigger.severity"
|
||||
ng-model-onblur
|
||||
ng-change="ctrl.render()">
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="trigger.color" ng-change="ctrl.render()"></spectrum-picker>
|
||||
</span>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-0"
|
||||
label="Show"
|
||||
checked="trigger.show"
|
||||
on-change="ctrl.reRenderProblems()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-3"> </label>
|
||||
<label class="gf-form-label triggers-severity-config"
|
||||
ng-style="{color: ctrl.panel.ackEventColor}">
|
||||
<i class="icon-gf icon-gf-online"></i>
|
||||
</label>
|
||||
<label class="gf-form-label width-12">
|
||||
Acknowledged color
|
||||
</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="ctrl.panel.ackEventColor" ng-change="ctrl.render()"></spectrum-picker>
|
||||
</span>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form"
|
||||
label-class="width-0"
|
||||
label="Show"
|
||||
checked="ctrl.panel.markAckEvents"
|
||||
on-change="ctrl.reRenderProblems()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-3"> </label>
|
||||
<label class="gf-form-label triggers-severity-config"
|
||||
ng-style="{color: ctrl.panel.okEventColor}">
|
||||
<i class="icon-gf icon-gf-online"></i>
|
||||
</label>
|
||||
<label class="gf-form-label width-12">
|
||||
OK event color
|
||||
</label>
|
||||
<span class="gf-form-label">
|
||||
<spectrum-picker ng-model="ctrl.panel.okEventColor" ng-change="ctrl.render()"></spectrum-picker>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import './matchMedia.mock';
|
||||
import { DEFAULT_SEVERITY, DEFAULT_TARGET, PANEL_DEFAULTS, TriggerPanelCtrl } from '../triggers_panel_ctrl';
|
||||
import { CURRENT_SCHEMA_VERSION } from '../migrations';
|
||||
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
return {
|
||||
getDataSourceSrv: () => ({
|
||||
getMetricSources: () => {
|
||||
return [{ meta: { id: 'alexanderzobnin-zabbix-datasource' }, value: {}, name: 'zabbix_default' }];
|
||||
},
|
||||
get: () => Promise.resolve({})
|
||||
}),
|
||||
};
|
||||
}, { virtual: true });
|
||||
|
||||
describe('Triggers Panel schema migration', () => {
|
||||
let ctx: any = {};
|
||||
let updatePanelCtrl;
|
||||
|
||||
const timeoutMock = () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = {
|
||||
scope: {
|
||||
panel: {
|
||||
datasource: 'zabbix',
|
||||
triggers: DEFAULT_TARGET,
|
||||
hostField: true,
|
||||
statusField: false,
|
||||
severityField: false,
|
||||
lastChangeField: true,
|
||||
ageField: true,
|
||||
infoField: true,
|
||||
limit: 10,
|
||||
showTriggers: 'unacknowledged',
|
||||
hideHostsInMaintenance: false,
|
||||
hostsInMaintenance: false,
|
||||
sortTriggersBy: { text: 'last change', value: 'lastchange' },
|
||||
showEvents: { text: 'Problems', value: '1' },
|
||||
triggerSeverity: DEFAULT_SEVERITY,
|
||||
okEventColor: 'rgba(0, 245, 153, 0.45)',
|
||||
ackEventColor: 'rgba(0, 0, 0, 0)',
|
||||
scroll: true,
|
||||
pageSize: 10,
|
||||
fontSize: '100%',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updatePanelCtrl = (scope) => new TriggerPanelCtrl(scope, {}, timeoutMock);
|
||||
});
|
||||
|
||||
it('should update old panel schema', () => {
|
||||
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
|
||||
|
||||
const expected = _.defaultsDeep({
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
datasource: 'zabbix',
|
||||
targets: [
|
||||
{
|
||||
...DEFAULT_TARGET,
|
||||
queryType: 5,
|
||||
showProblems: 'problems',
|
||||
options: {
|
||||
hostsInMaintenance: false,
|
||||
acknowledged: 0,
|
||||
sortProblems: 'default',
|
||||
minSeverity: 0,
|
||||
limit: 10,
|
||||
},
|
||||
}
|
||||
],
|
||||
sortProblems: 'lastchange',
|
||||
ageField: true,
|
||||
statusField: false,
|
||||
severityField: false,
|
||||
limit: 10,
|
||||
okEventColor: 'rgba(0, 245, 153, 0.45)',
|
||||
ackEventColor: 'rgba(0, 0, 0, 0)'
|
||||
}, PANEL_DEFAULTS);
|
||||
|
||||
expect(updatedPanelCtrl.panel).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should create new panel with default schema', () => {
|
||||
ctx.scope.panel = {};
|
||||
const updatedPanelCtrl = updatePanelCtrl(ctx.scope);
|
||||
|
||||
const expected = _.defaultsDeep({
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
}, PANEL_DEFAULTS);
|
||||
expect(updatedPanelCtrl.panel).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,211 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import './matchMedia.mock';
|
||||
import { DEFAULT_TARGET, PANEL_DEFAULTS, TriggerPanelCtrl } from '../triggers_panel_ctrl';
|
||||
|
||||
let datasourceSrvMock, zabbixDSMock;
|
||||
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
return {
|
||||
getDataSourceSrv: () => datasourceSrvMock,
|
||||
};
|
||||
}, { virtual: true });
|
||||
|
||||
describe('TriggerPanelCtrl', () => {
|
||||
let ctx: any = {};
|
||||
let createPanelCtrl: () => any;
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = {
|
||||
scope: {
|
||||
panel: {
|
||||
...PANEL_DEFAULTS,
|
||||
sortProblems: 'lastchange',
|
||||
}
|
||||
}
|
||||
};
|
||||
ctx.scope.panel.targets = [{
|
||||
...DEFAULT_TARGET,
|
||||
datasource: 'zabbix_default',
|
||||
}];
|
||||
|
||||
zabbixDSMock = {
|
||||
zabbix: {
|
||||
getExtendedEventData: jest.fn().mockResolvedValue([]),
|
||||
getEventAlerts: jest.fn().mockResolvedValue([]),
|
||||
}
|
||||
};
|
||||
|
||||
datasourceSrvMock = {
|
||||
get: () => Promise.resolve(zabbixDSMock)
|
||||
};
|
||||
|
||||
const timeoutMock = (fn: () => any) => Promise.resolve(fn());
|
||||
createPanelCtrl = () => new TriggerPanelCtrl(ctx.scope, {}, timeoutMock);
|
||||
|
||||
ctx.panelCtrl = createPanelCtrl();
|
||||
|
||||
ctx.dataFramesReceived = generateDataFramesResponse([
|
||||
{ id: "1", timestamp: "1510000010", severity: 5 },
|
||||
{ id: "2", timestamp: "1510000040", severity: 3 },
|
||||
{ id: "3", timestamp: "1510000020", severity: 4 },
|
||||
{ id: "4", timestamp: "1510000030", severity: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
describe('When refreshing panel', () => {
|
||||
beforeEach(() => {
|
||||
ctx.scope.panel.datasources = ['zabbix_default', 'zabbix'];
|
||||
ctx.scope.panel.targets = [
|
||||
{
|
||||
...DEFAULT_TARGET,
|
||||
datasource: 'zabbix_default'
|
||||
},
|
||||
{
|
||||
...DEFAULT_TARGET,
|
||||
datasource: 'zabbix'
|
||||
},
|
||||
];
|
||||
ctx.panelCtrl = createPanelCtrl();
|
||||
});
|
||||
|
||||
it('should format triggers', (done) => {
|
||||
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
|
||||
const formattedTrigger: any = _.find(ctx.panelCtrl.renderData, { triggerid: "1" });
|
||||
expect(formattedTrigger.host).toBe('backend01');
|
||||
expect(formattedTrigger.hostTechName).toBe('backend01_tech');
|
||||
expect(formattedTrigger.datasource).toBe('zabbix_default');
|
||||
expect(formattedTrigger.maintenance).toBe(true);
|
||||
expect(formattedTrigger.lastchange).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort triggers by time by default', (done) => {
|
||||
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
|
||||
const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid');
|
||||
expect(trigger_ids).toEqual([
|
||||
'2', '4', '3', '1'
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort triggers by severity', (done) => {
|
||||
ctx.panelCtrl.panel.sortProblems = 'priority';
|
||||
ctx.panelCtrl.onDataFramesReceived(ctx.dataFramesReceived).then(() => {
|
||||
const trigger_ids = _.map(ctx.panelCtrl.renderData, 'triggerid');
|
||||
expect(trigger_ids).toEqual([
|
||||
'1', '3', '2', '4'
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const defaultProblem: any = {
|
||||
"acknowledges": [],
|
||||
"comments": "It probably means that the systems requires\nmore physical memory.",
|
||||
"correlation_mode": "0",
|
||||
"correlation_tag": "",
|
||||
"datasource": "zabbix_default",
|
||||
"description": "Lack of free swap space on server",
|
||||
"error": "",
|
||||
"expression": "{13297}>20",
|
||||
"flags": "0",
|
||||
"groups": [
|
||||
{
|
||||
"groupid": "2",
|
||||
"name": "Linux servers"
|
||||
},
|
||||
{
|
||||
"groupid": "9",
|
||||
"name": "Backend"
|
||||
}
|
||||
],
|
||||
"hosts": [
|
||||
{
|
||||
"host": "backend01_tech",
|
||||
"hostid": "10111",
|
||||
"maintenance_status": "1",
|
||||
"name": "backend01",
|
||||
"proxy_hostid": "0"
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"itemid": "23979",
|
||||
"key_": "system.cpu.util[,iowait]",
|
||||
"lastvalue": "25.2091",
|
||||
"name": "CPU $2 time"
|
||||
}
|
||||
],
|
||||
"lastEvent": {
|
||||
"acknowledged": "0",
|
||||
"clock": "1589297010",
|
||||
"eventid": "4399289",
|
||||
"name": "Disk I/O is overloaded on backend01",
|
||||
"ns": "224779201",
|
||||
"object": "0",
|
||||
"objectid": "13682",
|
||||
"severity": "2",
|
||||
"source": "0",
|
||||
"value": "1"
|
||||
},
|
||||
"lastchange": "1440259530",
|
||||
"maintenance": true,
|
||||
"manual_close": "0",
|
||||
"severity": "2",
|
||||
"recovery_expression": "",
|
||||
"recovery_mode": "0",
|
||||
"showAckButton": true,
|
||||
"state": "0",
|
||||
"status": "0",
|
||||
"tags": [],
|
||||
"templateid": "13671",
|
||||
"triggerid": "13682",
|
||||
"type": "0",
|
||||
"url": "",
|
||||
"value": "1"
|
||||
};
|
||||
|
||||
function generateDataFramesResponse(problemDescs: any[] = [{ id: 1 }]): any {
|
||||
const problems = problemDescs.map(problem => generateProblem(problem.id, problem.timestamp, problem.severity));
|
||||
|
||||
return [
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"config": {},
|
||||
"name": "Problems",
|
||||
"state": {
|
||||
"scopedVars": {},
|
||||
"title": null
|
||||
},
|
||||
"type": "other",
|
||||
"values": problems,
|
||||
}
|
||||
],
|
||||
"length": 16,
|
||||
"name": "problems"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function generateProblem(id, timestamp?, severity?): any {
|
||||
const problem = _.cloneDeep(defaultProblem);
|
||||
problem.triggerid = id.toString();
|
||||
problem.eventid = id.toString();
|
||||
if (severity) {
|
||||
problem.severity = severity.toString();
|
||||
}
|
||||
if (timestamp) {
|
||||
problem.lastchange = timestamp;
|
||||
problem.timestamp = timestamp;
|
||||
}
|
||||
return problem;
|
||||
}
|
||||
|
||||
function getProblemById(id, ctx): any {
|
||||
return _.find(ctx.panelCtrl.renderData, { triggerid: id.toString() });
|
||||
}
|
||||
@@ -1,456 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import _ from 'lodash';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { PanelEvents } from '@grafana/data';
|
||||
import * as dateMath from 'grafana/app/core/utils/datemath';
|
||||
import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk';
|
||||
import { triggerPanelOptionsTab } from './options_tab';
|
||||
import { CURRENT_SCHEMA_VERSION, migratePanelSchema } from './migrations';
|
||||
import ProblemList from './components/Problems/Problems';
|
||||
import AlertList from './components/AlertList/AlertList';
|
||||
import { ProblemDTO } from 'datasource-zabbix/types';
|
||||
|
||||
const PROBLEM_EVENTS_LIMIT = 100;
|
||||
|
||||
export const DEFAULT_TARGET = {
|
||||
group: { filter: "" },
|
||||
host: { filter: "" },
|
||||
application: { filter: "" },
|
||||
trigger: { filter: "" },
|
||||
tags: { filter: "" },
|
||||
proxy: { filter: "" },
|
||||
showProblems: 'problems',
|
||||
};
|
||||
|
||||
export const DEFAULT_SEVERITY = [
|
||||
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true },
|
||||
{ priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true },
|
||||
{ priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true },
|
||||
{ priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true },
|
||||
{ priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true },
|
||||
{ priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true },
|
||||
];
|
||||
|
||||
export const getDefaultSeverity = () => DEFAULT_SEVERITY;
|
||||
|
||||
const DEFAULT_TIME_FORMAT = "DD MMM YYYY HH:mm:ss";
|
||||
|
||||
export const PANEL_DEFAULTS = {
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
targets: [{}],
|
||||
// Fields
|
||||
hostField: true,
|
||||
hostTechNameField: false,
|
||||
hostProxy: false,
|
||||
hostGroups: false,
|
||||
showTags: true,
|
||||
statusField: true,
|
||||
statusIcon: false,
|
||||
severityField: true,
|
||||
ackField: true,
|
||||
ageField: false,
|
||||
descriptionField: true,
|
||||
descriptionAtNewLine: false,
|
||||
// Options
|
||||
sortProblems: 'lastchange',
|
||||
limit: null,
|
||||
// View options
|
||||
layout: 'table',
|
||||
fontSize: '100%',
|
||||
pageSize: 10,
|
||||
problemTimeline: true,
|
||||
highlightBackground: false,
|
||||
highlightNewEvents: false,
|
||||
highlightNewerThan: '1h',
|
||||
customLastChangeFormat: false,
|
||||
lastChangeFormat: "",
|
||||
resizedColumns: [],
|
||||
// Triggers severity and colors
|
||||
triggerSeverity: getDefaultSeverity(),
|
||||
okEventColor: 'rgb(56, 189, 113)',
|
||||
ackEventColor: 'rgb(56, 219, 156)',
|
||||
markAckEvents: false,
|
||||
};
|
||||
|
||||
const triggerStatusMap = {
|
||||
'0': 'OK',
|
||||
'1': 'PROBLEM'
|
||||
};
|
||||
|
||||
export class TriggerPanelCtrl extends MetricsPanelCtrl {
|
||||
scope: any;
|
||||
useDataFrames: boolean;
|
||||
triggerStatusMap: any;
|
||||
defaultTimeFormat: string;
|
||||
pageIndex: number;
|
||||
renderData: any[];
|
||||
problems: any[];
|
||||
contextSrv: any;
|
||||
static templateUrl: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, $timeout) {
|
||||
super($scope, $injector);
|
||||
this.scope = $scope;
|
||||
this.$timeout = $timeout;
|
||||
|
||||
// Tell Grafana do not convert data frames to table or series
|
||||
this.useDataFrames = true;
|
||||
|
||||
this.triggerStatusMap = triggerStatusMap;
|
||||
this.defaultTimeFormat = DEFAULT_TIME_FORMAT;
|
||||
this.pageIndex = 0;
|
||||
this.range = {};
|
||||
this.renderData = [];
|
||||
|
||||
this.panel = migratePanelSchema(this.panel);
|
||||
_.defaultsDeep(this.panel, _.cloneDeep(PANEL_DEFAULTS));
|
||||
|
||||
// this.events.on(PanelEvents.render, this.onRender.bind(this));
|
||||
this.events.on('data-frames-received', this.onDataFramesReceived.bind(this));
|
||||
this.events.on(PanelEvents.dataSnapshotLoad, this.onDataSnapshotLoad.bind(this));
|
||||
this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this));
|
||||
}
|
||||
|
||||
onInitEditMode() {
|
||||
// Update schema version to prevent migration on up-to-date targets
|
||||
this.panel.schemaVersion = CURRENT_SCHEMA_VERSION;
|
||||
this.addEditorTab('Options', triggerPanelOptionsTab);
|
||||
}
|
||||
|
||||
onDataFramesReceived(data: any): Promise<any> {
|
||||
this.range = this.timeSrv.timeRange();
|
||||
let problems = [];
|
||||
|
||||
if (data && data.length) {
|
||||
for (const dataFrame of data) {
|
||||
try {
|
||||
const values = dataFrame.fields[0].values;
|
||||
if (values.toArray) {
|
||||
problems.push(values.toArray());
|
||||
} else if (values.length > 0) {
|
||||
// On snapshot mode values is a plain Array, not ArrayVector
|
||||
problems.push(values);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
problems = _.flatten(problems);
|
||||
this.problems = problems;
|
||||
return this.renderProblems(problems);
|
||||
}
|
||||
|
||||
onDataSnapshotLoad(snapshotData) {
|
||||
return this.onDataFramesReceived(snapshotData);
|
||||
}
|
||||
|
||||
reRenderProblems() {
|
||||
if (this.problems) {
|
||||
this.renderProblems(this.problems);
|
||||
}
|
||||
}
|
||||
|
||||
setPanelError(err, defaultError = "Request Error") {
|
||||
this.inspector = { error: err };
|
||||
this.error = err.message || defaultError;
|
||||
if (err.data) {
|
||||
if (err.data.message) {
|
||||
this.error = err.data.message;
|
||||
}
|
||||
if (err.data.error) {
|
||||
this.error = err.data.error;
|
||||
}
|
||||
}
|
||||
|
||||
// this.events.emit(PanelEvents.dataError, err);
|
||||
console.log('Panel data error:', err);
|
||||
}
|
||||
|
||||
renderProblems(problems) {
|
||||
let triggers = _.cloneDeep(problems);
|
||||
|
||||
triggers = _.map(triggers, this.formatTrigger.bind(this));
|
||||
triggers = this.filterProblems(triggers);
|
||||
triggers = this.sortTriggers(triggers);
|
||||
|
||||
this.renderData = triggers;
|
||||
|
||||
return this.$timeout(() => {
|
||||
return super.render(triggers);
|
||||
});
|
||||
}
|
||||
|
||||
filterProblems(problems) {
|
||||
let problemsList = _.cloneDeep(problems);
|
||||
|
||||
// Filter acknowledged triggers
|
||||
if (this.panel.showTriggers === 'unacknowledged') {
|
||||
problemsList = _.filter(problemsList, trigger => {
|
||||
return !(trigger.acknowledges && trigger.acknowledges.length);
|
||||
});
|
||||
} else if (this.panel.showTriggers === 'acknowledged') {
|
||||
problemsList = _.filter(problemsList, trigger => {
|
||||
return trigger.acknowledges && trigger.acknowledges.length;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter triggers by severity
|
||||
problemsList = _.filter(problemsList, problem => {
|
||||
if (problem.severity) {
|
||||
return this.panel.triggerSeverity[problem.severity].show;
|
||||
} else {
|
||||
return this.panel.triggerSeverity[problem.priority].show;
|
||||
}
|
||||
});
|
||||
|
||||
return problemsList;
|
||||
}
|
||||
|
||||
sortTriggers(problems: ProblemDTO[]) {
|
||||
if (this.panel.sortProblems === 'priority') {
|
||||
problems = _.orderBy(problems, ['severity', 'timestamp', 'eventid'], ['desc', 'desc', 'desc']);
|
||||
} else if (this.panel.sortProblems === 'lastchange') {
|
||||
problems = _.orderBy(problems, ['timestamp', 'severity', 'eventid'], ['desc', 'desc', 'desc']);
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
formatTrigger(zabbixTrigger) {
|
||||
const trigger = _.cloneDeep(zabbixTrigger);
|
||||
|
||||
// Set host and proxy that the trigger belongs
|
||||
if (trigger.hosts && trigger.hosts.length) {
|
||||
const host = trigger.hosts[0];
|
||||
trigger.host = host.name;
|
||||
trigger.hostTechName = host.host;
|
||||
if (host.proxy) {
|
||||
trigger.proxy = host.proxy;
|
||||
}
|
||||
}
|
||||
|
||||
// Set tags if present
|
||||
if (trigger.tags && trigger.tags.length === 0) {
|
||||
trigger.tags = null;
|
||||
}
|
||||
|
||||
// Handle multi-line description
|
||||
if (trigger.comments) {
|
||||
trigger.comments = trigger.comments.replace('\n', '<br>');
|
||||
}
|
||||
|
||||
trigger.lastchangeUnix = Number(trigger.lastchange);
|
||||
return trigger;
|
||||
}
|
||||
|
||||
parseTags(tagStr) {
|
||||
if (!tagStr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let tags = _.map(tagStr.split(','), (tag) => tag.trim());
|
||||
tags = _.map(tags, (tag) => {
|
||||
const tagParts = tag.split(':');
|
||||
return { tag: tagParts[0].trim(), value: tagParts[1].trim() };
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
tagsToString(tags) {
|
||||
return _.map(tags, (tag) => `${tag.tag}:${tag.value}`).join(', ');
|
||||
}
|
||||
|
||||
addTagFilter(tag, datasource) {
|
||||
for (const target of this.panel.targets) {
|
||||
if (target.datasource?.uid === datasource?.uid || target.datasource === datasource || this.panel.datasource === datasource) {
|
||||
const tagFilter = target.tags.filter;
|
||||
let targetTags = this.parseTags(tagFilter);
|
||||
const newTag = { tag: tag.tag, value: tag.value };
|
||||
targetTags.push(newTag);
|
||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||
const newFilter = this.tagsToString(targetTags);
|
||||
target.tags.filter = newFilter;
|
||||
}
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
removeTagFilter(tag, datasource) {
|
||||
const matchTag = t => t.tag === tag.tag && t.value === tag.value;
|
||||
for (const target of this.panel.targets) {
|
||||
if (target.datasource?.uid === datasource?.uid || target.datasource === datasource || this.panel.datasource === datasource) {
|
||||
const tagFilter = target.tags.filter;
|
||||
let targetTags = this.parseTags(tagFilter);
|
||||
_.remove(targetTags, matchTag);
|
||||
targetTags = _.uniqWith(targetTags, _.isEqual);
|
||||
const newFilter = this.tagsToString(targetTags);
|
||||
target.tags.filter = newFilter;
|
||||
}
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getProblemEvents(problem) {
|
||||
const triggerids = [problem.triggerid];
|
||||
const timeFrom = Math.ceil(dateMath.parse(this.range.from) / 1000);
|
||||
const timeTo = Math.ceil(dateMath.parse(this.range.to) / 1000);
|
||||
return getDataSourceSrv().get(problem.datasource)
|
||||
.then((datasource: any) => {
|
||||
return datasource.zabbix.getEvents(triggerids, timeFrom, timeTo, [0, 1], PROBLEM_EVENTS_LIMIT);
|
||||
});
|
||||
}
|
||||
|
||||
getProblemAlerts(problem: ProblemDTO) {
|
||||
if (!problem.eventid) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const eventids = [problem.eventid];
|
||||
return getDataSourceSrv().get(problem.datasource)
|
||||
.then((datasource: any) => {
|
||||
return datasource.zabbix.getEventAlerts(eventids);
|
||||
});
|
||||
}
|
||||
|
||||
getProblemScripts(problem: ProblemDTO) {
|
||||
const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null;
|
||||
|
||||
return getDataSourceSrv().get(problem.datasource)
|
||||
.then((datasource: any) => {
|
||||
return datasource.zabbix.getScripts([hostid]);
|
||||
});
|
||||
}
|
||||
|
||||
getAlertIconClassBySeverity(triggerSeverity) {
|
||||
let iconClass = 'icon-gf-online';
|
||||
if (triggerSeverity.priority >= 2) {
|
||||
iconClass = 'icon-gf-critical';
|
||||
}
|
||||
return iconClass;
|
||||
}
|
||||
|
||||
resetResizedColumns() {
|
||||
this.panel.resizedColumns = [];
|
||||
this.render();
|
||||
}
|
||||
|
||||
acknowledgeProblem(problem: ProblemDTO, message, action, severity) {
|
||||
const eventid = problem.eventid;
|
||||
const grafana_user = this.contextSrv.user.name;
|
||||
const ack_message = grafana_user + ' (Grafana): ' + message;
|
||||
return getDataSourceSrv().get(problem.datasource)
|
||||
.then((datasource: any) => {
|
||||
const userIsEditor = this.contextSrv.isEditor || this.contextSrv.isGrafanaAdmin;
|
||||
if (datasource.disableReadOnlyUsersAck && !userIsEditor) {
|
||||
return Promise.reject({ message: 'You have no permissions to acknowledge events.' });
|
||||
}
|
||||
if (eventid) {
|
||||
return datasource.zabbix.acknowledgeEvent(eventid, ack_message, action, severity);
|
||||
} else {
|
||||
return Promise.reject({ message: 'Trigger has no events. Nothing to acknowledge.' });
|
||||
}
|
||||
})
|
||||
.then(this.refresh.bind(this))
|
||||
.catch((err) => {
|
||||
this.setPanelError(err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
executeScript(problem: ProblemDTO, scriptid: string) {
|
||||
const hostid = problem.hosts?.length ? problem.hosts[0].hostid : null;
|
||||
|
||||
return getDataSourceSrv().get(problem.datasource)
|
||||
.then((datasource: any) => {
|
||||
return datasource.zabbix.executeScript(hostid, scriptid);
|
||||
});
|
||||
}
|
||||
|
||||
handlePageSizeChange(pageSize, pageIndex) {
|
||||
this.panel.pageSize = pageSize;
|
||||
this.pageIndex = pageIndex;
|
||||
this.scope.$apply(() => {
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
handleColumnResize(newResized) {
|
||||
this.panel.resizedColumns = newResized;
|
||||
this.scope.$apply(() => {
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
link(scope, elem, attrs, ctrl) {
|
||||
const panel = ctrl.panel;
|
||||
|
||||
ctrl.events.on(PanelEvents.render, (renderData) => {
|
||||
renderData = renderData || this.renderData;
|
||||
renderPanel(renderData);
|
||||
ctrl.renderingCompleted();
|
||||
});
|
||||
|
||||
function renderPanel(problems) {
|
||||
const timeFrom = Math.ceil(dateMath.parse(ctrl.range.from) / 1000);
|
||||
const timeTo = Math.ceil(dateMath.parse(ctrl.range.to) / 1000);
|
||||
|
||||
const fontSize = parseInt(panel.fontSize.slice(0, panel.fontSize.length - 1), 10);
|
||||
const fontSizeProp = fontSize && fontSize !== 100 ? fontSize : null;
|
||||
|
||||
const pageSize = panel.pageSize || 10;
|
||||
const loading = ctrl.loading && (!problems || !problems.length);
|
||||
|
||||
const panelOptions = {};
|
||||
for (const prop in PANEL_DEFAULTS) {
|
||||
panelOptions[prop] = ctrl.panel[prop];
|
||||
}
|
||||
const problemsListProps = {
|
||||
problems,
|
||||
panelOptions,
|
||||
timeRange: { timeFrom, timeTo },
|
||||
range: ctrl.range,
|
||||
loading,
|
||||
pageSize,
|
||||
fontSize: fontSizeProp,
|
||||
panelId: ctrl.panel.id,
|
||||
getProblemEvents: ctrl.getProblemEvents.bind(ctrl),
|
||||
getProblemAlerts: ctrl.getProblemAlerts.bind(ctrl),
|
||||
getScripts: ctrl.getProblemScripts.bind(ctrl),
|
||||
onPageSizeChange: ctrl.handlePageSizeChange.bind(ctrl),
|
||||
onColumnResize: ctrl.handleColumnResize.bind(ctrl),
|
||||
onProblemAck: (trigger, data) => {
|
||||
const { message, action, severity } = data;
|
||||
return ctrl.acknowledgeProblem(trigger, message, action, severity);
|
||||
},
|
||||
onExecuteScript: ctrl.executeScript.bind(ctrl),
|
||||
onTagClick: (tag, datasource, ctrlKey, shiftKey) => {
|
||||
if (ctrlKey || shiftKey) {
|
||||
ctrl.removeTagFilter(tag, datasource);
|
||||
} else {
|
||||
ctrl.addTagFilter(tag, datasource);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let problemsReactElem;
|
||||
if (panel.layout === 'list') {
|
||||
problemsReactElem = React.createElement(AlertList, problemsListProps);
|
||||
} else {
|
||||
problemsReactElem = React.createElement(ProblemList, problemsListProps);
|
||||
}
|
||||
|
||||
const panelContainerElem = elem.find('.panel-content');
|
||||
if (panelContainerElem && panelContainerElem.length) {
|
||||
ReactDOM.render(problemsReactElem, panelContainerElem[0]);
|
||||
} else {
|
||||
ReactDOM.render(problemsReactElem, elem[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TriggerPanelCtrl.templateUrl = 'public/plugins/alexanderzobnin-zabbix-app/panel-triggers/partials/module.html';
|
||||
@@ -1,9 +1,11 @@
|
||||
import { DataSourceRef } from "@grafana/data";
|
||||
import { DataSourceRef } from '@grafana/data';
|
||||
import { CURRENT_SCHEMA_VERSION } from './migrations';
|
||||
|
||||
export interface ProblemsPanelOptions {
|
||||
schemaVersion: number;
|
||||
datasources: any[];
|
||||
targets: ProblemsPanelTarget[];
|
||||
layout: 'table' | 'list';
|
||||
// Fields
|
||||
hostField?: boolean;
|
||||
hostTechNameField?: boolean;
|
||||
@@ -19,15 +21,9 @@ export interface ProblemsPanelOptions {
|
||||
descriptionAtNewLine?: boolean;
|
||||
// Options
|
||||
hostsInMaintenance?: boolean;
|
||||
showTriggers?: 'all triggers' | 'unacknowledged' | 'acknowledges';
|
||||
sortTriggersBy?: {
|
||||
text: string;
|
||||
value: 'lastchange' | 'priority';
|
||||
};
|
||||
showEvents?: {
|
||||
text: 'All' | 'OK' | 'Problems';
|
||||
value: 1 | Array<0 | 1>;
|
||||
};
|
||||
showTriggers?: 'all triggers' | 'unacknowledged' | 'acknowledged';
|
||||
sortProblems?: 'default' | 'lastchange' | 'priority';
|
||||
showEvents?: Number[];
|
||||
limit?: number;
|
||||
// View options
|
||||
fontSize?: string;
|
||||
@@ -46,24 +42,71 @@ export interface ProblemsPanelOptions {
|
||||
markAckEvents?: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SEVERITY = [
|
||||
{ priority: 0, severity: 'Not classified', color: 'rgb(108, 108, 108)', show: true },
|
||||
{ priority: 1, severity: 'Information', color: 'rgb(120, 158, 183)', show: true },
|
||||
{ priority: 2, severity: 'Warning', color: 'rgb(175, 180, 36)', show: true },
|
||||
{ priority: 3, severity: 'Average', color: 'rgb(255, 137, 30)', show: true },
|
||||
{ priority: 4, severity: 'High', color: 'rgb(255, 101, 72)', show: true },
|
||||
{ priority: 5, severity: 'Disaster', color: 'rgb(215, 0, 0)', show: true },
|
||||
];
|
||||
|
||||
export const getDefaultSeverity = () => DEFAULT_SEVERITY;
|
||||
|
||||
export const defaultPanelOptions: Partial<ProblemsPanelOptions> = {
|
||||
schemaVersion: CURRENT_SCHEMA_VERSION,
|
||||
// Fields
|
||||
hostField: true,
|
||||
hostTechNameField: false,
|
||||
hostProxy: false,
|
||||
hostGroups: false,
|
||||
showTags: true,
|
||||
statusField: true,
|
||||
statusIcon: false,
|
||||
severityField: true,
|
||||
ackField: true,
|
||||
ageField: false,
|
||||
descriptionField: true,
|
||||
descriptionAtNewLine: false,
|
||||
// Options
|
||||
sortProblems: 'lastchange',
|
||||
limit: null,
|
||||
// View options
|
||||
layout: 'table',
|
||||
fontSize: '100%',
|
||||
pageSize: 10,
|
||||
problemTimeline: true,
|
||||
highlightBackground: false,
|
||||
highlightNewEvents: false,
|
||||
highlightNewerThan: '1h',
|
||||
customLastChangeFormat: false,
|
||||
lastChangeFormat: '',
|
||||
resizedColumns: [],
|
||||
// Triggers severity and colors
|
||||
triggerSeverity: getDefaultSeverity(),
|
||||
okEventColor: 'rgb(56, 189, 113)',
|
||||
ackEventColor: 'rgb(56, 219, 156)',
|
||||
markAckEvents: false,
|
||||
};
|
||||
|
||||
export interface ProblemsPanelTarget {
|
||||
group: {
|
||||
filter: string
|
||||
filter: string;
|
||||
};
|
||||
host: {
|
||||
filter: string
|
||||
filter: string;
|
||||
};
|
||||
application: {
|
||||
filter: string
|
||||
filter: string;
|
||||
};
|
||||
trigger: {
|
||||
filter: string
|
||||
filter: string;
|
||||
};
|
||||
tags: {
|
||||
filter: string
|
||||
filter: string;
|
||||
};
|
||||
proxy: {
|
||||
filter: string
|
||||
filter: string;
|
||||
};
|
||||
datasource: string;
|
||||
}
|
||||
@@ -77,108 +120,6 @@ export interface TriggerSeverity {
|
||||
|
||||
export type TriggerColor = string;
|
||||
|
||||
export interface ZBXTrigger {
|
||||
acknowledges?: ZBXAcknowledge[];
|
||||
showAckButton?: boolean;
|
||||
alerts?: ZBXAlert[];
|
||||
age?: string;
|
||||
color?: TriggerColor;
|
||||
comments?: string;
|
||||
correlation_mode?: string;
|
||||
correlation_tag?: string;
|
||||
datasource?: DataSourceRef | string;
|
||||
description?: string;
|
||||
error?: string;
|
||||
expression?: string;
|
||||
flags?: string;
|
||||
groups?: ZBXGroup[];
|
||||
host?: string;
|
||||
hostTechName?: string;
|
||||
hosts?: ZBXHost[];
|
||||
items?: ZBXItem[];
|
||||
lastEvent?: ZBXEvent;
|
||||
lastchange?: string;
|
||||
lastchangeUnix?: number;
|
||||
maintenance?: boolean;
|
||||
manual_close?: string;
|
||||
priority?: string;
|
||||
proxy?: string;
|
||||
recovery_expression?: string;
|
||||
recovery_mode?: string;
|
||||
severity?: string;
|
||||
state?: string;
|
||||
status?: string;
|
||||
tags?: ZBXTag[];
|
||||
templateid?: string;
|
||||
triggerid?: string;
|
||||
/** Whether the trigger can generate multiple problem events. */
|
||||
type?: string;
|
||||
url?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface ZBXGroup {
|
||||
groupid: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ZBXHost {
|
||||
hostid: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ZBXItem {
|
||||
itemid: string;
|
||||
name: string;
|
||||
key_: string;
|
||||
lastvalue?: string;
|
||||
}
|
||||
|
||||
export interface ZBXEvent {
|
||||
eventid: string;
|
||||
clock: string;
|
||||
ns?: string;
|
||||
value?: string;
|
||||
source?: string;
|
||||
object?: string;
|
||||
objectid?: string;
|
||||
acknowledged?: string;
|
||||
severity?: string;
|
||||
hosts?: ZBXHost[];
|
||||
acknowledges?: ZBXAcknowledge[];
|
||||
}
|
||||
|
||||
export interface ZBXTag {
|
||||
tag: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface ZBXAcknowledge {
|
||||
acknowledgeid: string;
|
||||
eventid: string;
|
||||
userid: string;
|
||||
action: string;
|
||||
clock: string;
|
||||
time: string;
|
||||
message?: string;
|
||||
user: string;
|
||||
alias: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
}
|
||||
|
||||
export interface ZBXAlert {
|
||||
eventid: string;
|
||||
clock: string;
|
||||
message: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface GFTimeRange {
|
||||
timeFrom: number;
|
||||
timeTo: number;
|
||||
}
|
||||
|
||||
export interface RTRow<T> {
|
||||
/** the materialized row of data */
|
||||
row: any;
|
||||
|
||||
Reference in New Issue
Block a user