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:
Alexander Zobnin
2022-11-30 14:01:21 +03:00
committed by GitHub
parent 504c9af226
commit 9b2079c1da
35 changed files with 1188 additions and 1630 deletions

View File

@@ -85,7 +85,7 @@
"ts-jest": "24.1.0", "ts-jest": "24.1.0",
"ts-loader": "4.4.1", "ts-loader": "4.4.1",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"typescript": "4.4.4", "typescript": "4.8.2",
"webpack": "4.41.5", "webpack": "4.41.5",
"webpack-cli": "3.3.10" "webpack-cli": "3.3.10"
}, },

View File

@@ -1 +0,0 @@
<h3 class="page-heading">Zabbix Plugin Config</h3>

View File

@@ -1,4 +0,0 @@
export class ZabbixAppConfigCtrl {
constructor() { }
}
ZabbixAppConfigCtrl.templateUrl = 'app_config_ctrl/config.html';

View File

@@ -1,6 +1,6 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { locationService } from '@grafana/runtime'; 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 { MODE_ITEMID, MODE_METRICS } from '../../datasource-zabbix/constants';
import { ActionButton } from '../ActionButton/ActionButton'; import { ActionButton } from '../ActionButton/ActionButton';
import { expandItemName } from '../../datasource-zabbix/utils'; import { expandItemName } from '../../datasource-zabbix/utils';
@@ -35,7 +35,7 @@ const openInExplore = (problem: ProblemDTO, panelId: number, range: TimeRange) =
item: { filter: itemName }, item: { filter: itemName },
}; };
} else { } else {
const itemids = problem.items?.map(p => p.itemid).join(','); const itemids = problem.items?.map((p) => p.itemid).join(',');
query = { query = {
queryType: MODE_ITEMID, queryType: MODE_ITEMID,
itemids: itemids, itemids: itemids,
@@ -54,4 +54,3 @@ const openInExplore = (problem: ProblemDTO, panelId: number, range: TimeRange) =
const url = urlUtil.renderUrl('/explore', { left: exploreState }); const url = urlUtil.renderUrl('/explore', { left: exploreState });
locationService.push(url); locationService.push(url);
}; };

View File

@@ -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 { export interface ZabbixDSOptions extends DataSourceJsonData {
username: string; username: string;
@@ -207,6 +207,8 @@ export interface ProblemDTO {
triggerid?: string; triggerid?: string;
eventid?: string; eventid?: string;
timestamp: number; timestamp: number;
lastchange?: string;
lastchangeUnix?: number;
/** Name of the trigger. */ /** Name of the trigger. */
name?: string; name?: string;
@@ -223,6 +225,7 @@ export interface ProblemDTO {
hostTechName?: string; hostTechName?: string;
proxy?: string; proxy?: string;
severity?: string; severity?: string;
priority?: string;
acknowledged?: '1' | '0'; acknowledged?: '1' | '0';
acknowledges?: ZBXAcknowledge[]; acknowledges?: ZBXAcknowledge[];
@@ -245,6 +248,7 @@ export interface ProblemDTO {
error?: string; error?: string;
showAckButton?: boolean; showAckButton?: boolean;
type?: string;
} }
export interface ZBXProblem { export interface ZBXProblem {
@@ -323,6 +327,7 @@ export interface ZBXHost {
host: string; host: string;
maintenance_status?: string; maintenance_status?: string;
proxy_hostid?: string; proxy_hostid?: string;
proxy?: any;
} }
export interface ZBXItem { export interface ZBXItem {
@@ -380,3 +385,7 @@ export interface ZBXAlert {
message: string; message: string;
error: string; error: string;
} }
export class ZBXQueryUpdatedEvent extends BusEventWithPayload<any> {
static type = 'zbx-query-updated';
}

View File

@@ -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.dark.scss';
import './sass/grafana-zabbix.light.scss'; import './sass/grafana-zabbix.light.scss';
import {ZabbixAppConfigCtrl} from './app_config_ctrl/config';
import {loadPluginCss} from 'grafana/app/plugins/sdk';
loadPluginCss({ loadPluginCss({
dark: 'plugins/alexanderzobnin-zabbix-app/css/grafana-zabbix.dark.css', 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 { export const plugin = new AppPlugin<{}>();
ZabbixAppConfigCtrl as ConfigCtrl
};

View 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()}
</>
);
};

View File

@@ -1,27 +1,29 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { ZBXTrigger } from '../../types'; import { ProblemDTO } from '../../../datasource-zabbix/types';
interface AlertAcknowledgesProps { interface AlertAcknowledgesProps {
problem: ZBXTrigger; problem: ProblemDTO;
onClick: (event?) => void; onClick: (event?) => void;
} }
export default class AlertAcknowledges extends PureComponent<AlertAcknowledgesProps> { export default class AlertAcknowledges extends PureComponent<AlertAcknowledgesProps> {
handleClick = (event) => { handleClick = (event) => {
this.props.onClick(event); this.props.onClick(event);
} };
render() { render() {
const { problem } = this.props; const { problem } = this.props;
const ackRows = problem.acknowledges && problem.acknowledges.map(ack => { const ackRows =
return ( problem.acknowledges &&
<tr key={ack.acknowledgeid}> problem.acknowledges.map((ack) => {
<td>{ack.time}</td> return (
<td>{ack.user}</td> <tr key={ack.acknowledgeid}>
<td>{ack.message}</td> <td>{ack.time}</td>
</tr> <td>{ack.user}</td>
); <td>{ack.message}</td>
}); </tr>
);
});
return ( return (
<div className="ack-tooltip"> <div className="ack-tooltip">
<table className="table"> <table className="table">
@@ -32,17 +34,19 @@ export default class AlertAcknowledges extends PureComponent<AlertAcknowledgesPr
<th className="ack-comments">Comments</th> <th className="ack-comments">Comments</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>{ackRows}</tbody>
{ackRows}
</tbody>
</table> </table>
{problem.showAckButton && {problem.showAckButton && (
<div className="ack-add-button"> <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> <i className="fa fa-plus"></i>
</button> </button>
</div> </div>
} )}
</div> </div>
); );
} }

View File

@@ -31,25 +31,32 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
ackProblem = (data: AckProblemData) => { ackProblem = (data: AckProblemData) => {
const problem = this.props.problem; const problem = this.props.problem;
return this.props.onProblemAck(problem, data); return this.props.onProblemAck(problem, data);
} };
render() { render() {
const { problem, panelOptions } = this.props; const { problem, panelOptions } = this.props;
const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1; const showDatasourceName = panelOptions.targets && panelOptions.targets.length > 1;
const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', { 'zbx-trigger-highlighted': panelOptions.highlightBackground }); const cardClass = classNames('alert-rule-item', 'zbx-trigger-card', {
const descriptionClass = classNames('alert-rule-item__text', { 'zbx-description--newline': panelOptions.descriptionAtNewLine }); 'zbx-trigger-highlighted': panelOptions.highlightBackground,
});
const descriptionClass = classNames('alert-rule-item__text', {
'zbx-description--newline': panelOptions.descriptionAtNewLine,
});
const problemSeverity = Number(problem.severity); const problemSeverity = Number(problem.severity);
let severityDesc: TriggerSeverity; let severityDesc: TriggerSeverity;
severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity); severityDesc = _.find(panelOptions.triggerSeverity, (s) => s.priority === problemSeverity);
if (problem.severity) { 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); 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) { if ((problem.datasource as DataSourceRef)?.uid) {
const dsInstance = getDataSourceSrv().getInstanceSettings((problem.datasource as DataSourceRef).uid); const dsInstance = getDataSourceSrv().getInstanceSettings((problem.datasource as DataSourceRef).uid);
dsName = dsInstance.name; dsName = dsInstance.name;
@@ -64,7 +71,7 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
let problemColor: string; let problemColor: string;
if (problem.value === '0') { if (problem.value === '0') {
problemColor = panelOptions.okEventColor; problemColor = panelOptions.okEventColor;
} else if (panelOptions.markAckEvents && problem.acknowledged === "1") { } else if (panelOptions.markAckEvents && problem.acknowledged === '1') {
problemColor = panelOptions.ackEventColor; problemColor = panelOptions.ackEventColor;
} else { } else {
problemColor = severityDesc.color; problemColor = severityDesc.color;
@@ -77,7 +84,12 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
return ( return (
<li className={cardClass} style={cardStyle}> <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__body">
<div className="alert-rule-item__header"> <div className="alert-rule-item__header">
@@ -90,15 +102,16 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
{panelOptions.showTags && ( {panelOptions.showTags && (
<span className="zbx-trigger-tags"> <span className="zbx-trigger-tags">
{problem.tags && problem.tags.map(tag => {problem.tags &&
<EventTag problem.tags.map((tag) => (
key={tag.tag + tag.value} <EventTag
tag={tag} key={tag.tag + tag.value}
datasource={dsName} tag={tag}
highlight={tag.tag === problem.correlation_tag} datasource={dsName}
onClick={this.handleTagClick} highlight={tag.tag === problem.correlation_tag}
/> onClick={this.handleTagClick}
)} />
))}
</span> </span>
)} )}
</div> </div>
@@ -106,25 +119,26 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
<div className={descriptionClass}> <div className={descriptionClass}>
{panelOptions.statusField && <AlertStatus problem={problem} blink={blink} />} {panelOptions.statusField && <AlertStatus problem={problem} blink={blink} />}
{panelOptions.severityField && ( {panelOptions.severityField && (
<AlertSeverity severityDesc={severityDesc} blink={blink} highlightBackground={panelOptions.highlightBackground} /> <AlertSeverity
severityDesc={severityDesc}
blink={blink}
highlightBackground={panelOptions.highlightBackground}
/>
)} )}
<span className="alert-rule-item__time"> <span className="alert-rule-item__time">{panelOptions.ageField && 'for ' + age}</span>
{panelOptions.ageField && "for " + age}
</span>
{panelOptions.descriptionField && !panelOptions.descriptionAtNewLine && ( {panelOptions.descriptionField && !panelOptions.descriptionAtNewLine && (
<span className="zbx-description" dangerouslySetInnerHTML={{ __html: problem.comments }} /> <span className="zbx-description" dangerouslySetInnerHTML={{ __html: problem.comments }} />
)} )}
</div> </div>
{panelOptions.descriptionField && panelOptions.descriptionAtNewLine && ( {panelOptions.descriptionField && panelOptions.descriptionAtNewLine && (
<div className="alert-rule-item__text zbx-description--newline" > <div className="alert-rule-item__text zbx-description--newline">
<span <span
className="alert-rule-item__info zbx-description" className="alert-rule-item__info zbx-description"
dangerouslySetInnerHTML={{ __html: problem.comments }} dangerouslySetInnerHTML={{ __html: problem.comments }}
/> />
</div> </div>
)} )}
</div> </div>
</div> </div>
@@ -138,30 +152,36 @@ export default class AlertCard extends PureComponent<AlertCardProps> {
)} )}
<div className="alert-rule-item__time zbx-trigger-lastchange"> <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"> <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' && ( {problem.state === '1' && (
<Tooltip placement="bottom" content={problem.error}> <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> </Tooltip>
)} )}
{problem.eventid && ( {problem.eventid && (
<ModalController> <ModalController>
{({ showModal, hideModal }) => ( {({ showModal, hideModal }) => (
<AlertAcknowledgesButton <AlertAcknowledgesButton
problem={problem} problem={problem}
onClick={() => { onClick={() => {
showModal(AckModal, { showModal(AckModal, {
canClose: problem.manual_close === '1', canClose: problem.manual_close === '1',
severity: problemSeverity, severity: problemSeverity,
onSubmit: this.ackProblem, onSubmit: this.ackProblem,
onDismiss: hideModal, onDismiss: hideModal,
}); });
}} }}
/> />
)} )}
</ModalController> </ModalController>
)} )}
</div> </div>
</div> </div>
@@ -178,7 +198,7 @@ interface AlertHostProps {
function AlertHost(props: AlertHostProps) { function AlertHost(props: AlertHostProps) {
const problem = props.problem; const problem = props.problem;
const panel = props.panelOptions; const panel = props.panelOptions;
let host = ""; let host = '';
if (panel.hostField && panel.hostTechNameField) { if (panel.hostField && panel.hostTechNameField) {
host = `${problem.host} (${problem.hostTechName})`; host = `${problem.host} (${problem.hostTechName})`;
} else if (panel.hostField || panel.hostTechNameField) { } else if (panel.hostField || panel.hostTechNameField) {
@@ -204,15 +224,13 @@ interface AlertGroupProps {
function AlertGroup(props: AlertGroupProps) { function AlertGroup(props: AlertGroupProps) {
const problem = props.problem; const problem = props.problem;
const panel = props.panelOptions; const panel = props.panelOptions;
let groupNames = ""; let groupNames = '';
if (panel.hostGroups) { if (panel.hostGroups) {
const groups = _.map(problem.groups, 'name').join(', '); const groups = _.map(problem.groups, 'name').join(', ');
groupNames += `[ ${groups} ]`; groupNames += `[ ${groups} ]`;
} }
return ( return <span className="zabbix-hostname">{groupNames}</span>;
<span className="zabbix-hostname">{groupNames}</span>
);
} }
const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)'; const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)';
@@ -228,11 +246,7 @@ function AlertStatus(props) {
{ 'alert-state-ok': problem.value === '0' }, { 'alert-state-ok': problem.value === '0' },
{ 'zabbix-trigger--blinked': blink } { 'zabbix-trigger--blinked': blink }
); );
return ( return <span className={className}>{status}</span>;
<span className={className}>
{status}
</span>
);
} }
function AlertSeverity(props) { function AlertSeverity(props) {
@@ -257,25 +271,29 @@ interface AlertAcknowledgesButtonProps {
class AlertAcknowledgesButton extends PureComponent<AlertAcknowledgesButtonProps> { class AlertAcknowledgesButton extends PureComponent<AlertAcknowledgesButtonProps> {
handleClick = (event) => { handleClick = (event) => {
this.props.onClick(event); this.props.onClick(event);
} };
renderTooltipContent = () => { renderTooltipContent = () => {
return <AlertAcknowledges problem={this.props.problem} onClick={this.handleClick} />; return <AlertAcknowledges problem={this.props.problem} onClick={this.handleClick} />;
} };
render() { render() {
const { problem } = this.props; const { problem } = this.props;
let content = null; let content = null;
if (problem.acknowledges && problem.acknowledges.length) { if (problem.acknowledges && problem.acknowledges.length) {
content = ( content = (
<Tooltip placement="bottom" content={this.renderTooltipContent}> <Tooltip placement="auto" content={this.renderTooltipContent} interactive>
<span><i className="fa fa-comments"></i></span> <span>
<i className="fa fa-comments"></i>
</span>
</Tooltip> </Tooltip>
); );
} else if (problem.showAckButton) { } else if (problem.showAckButton) {
content = ( content = (
<Tooltip placement="bottom" content="Acknowledge problem"> <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> </Tooltip>
); );
} }

View File

@@ -14,14 +14,14 @@ export const AlertIcon: FC<Props> = ({ problem, color, blink, highlightBackgroun
const severity = Number(problem.severity); const severity = Number(problem.severity);
const status = problem.value === '1' && severity >= 2 ? 'critical' : 'online'; const status = problem.value === '1' && severity >= 2 ? 'critical' : 'online';
const iconClass = cx( const iconClass = cx('icon-gf', blink && 'zabbix-trigger--blinked');
'icon-gf',
blink && 'zabbix-trigger--blinked',
);
const wrapperClass = cx( const wrapperClass = cx(
'alert-rule-item__icon', 'alert-rule-item__icon',
!highlightBackground && css`color: ${color}` !highlightBackground &&
css`
color: ${color};
`
); );
return ( return (

View File

@@ -1,6 +1,6 @@
import React, { PureComponent, CSSProperties } from 'react'; import React, { PureComponent, CSSProperties } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { ProblemsPanelOptions, GFTimeRange } from '../../types'; import { ProblemsPanelOptions } from '../../types';
import { AckProblemData } from '../AckModal'; import { AckProblemData } from '../AckModal';
import AlertCard from './AlertCard'; import AlertCard from './AlertCard';
import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types'; import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types';
@@ -10,7 +10,6 @@ export interface AlertListProps {
problems: ProblemDTO[]; problems: ProblemDTO[];
panelOptions: ProblemsPanelOptions; panelOptions: ProblemsPanelOptions;
loading?: boolean; loading?: boolean;
timeRange?: GFTimeRange;
pageSize?: number; pageSize?: number;
fontSize?: number; fontSize?: number;
onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void; onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void;
@@ -44,18 +43,17 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
page: newPage, page: newPage,
currentProblems: items, currentProblems: items,
}); });
} };
handleTagClick = (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => { handleTagClick = (tag: ZBXTag, datasource: string, ctrlKey?: boolean, shiftKey?: boolean) => {
if (this.props.onTagClick) { if (this.props.onTagClick) {
this.props.onTagClick(tag, datasource, ctrlKey, shiftKey); this.props.onTagClick(tag, datasource, ctrlKey, shiftKey);
} }
} };
handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => { handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => {
return this.props.onProblemAck(problem, data); return this.props.onProblemAck(problem, data);
} };
render() { render() {
const { problems, panelOptions } = this.props; const { problems, panelOptions } = this.props;
@@ -68,15 +66,17 @@ export default class AlertList extends PureComponent<AlertListProps, AlertListSt
<div className="triggers-panel-container" key="alertListContainer"> <div className="triggers-panel-container" key="alertListContainer">
<section className="card-section card-list-layout-list"> <section className="card-section card-list-layout-list">
<ol className={alertListClass}> <ol className={alertListClass}>
{currentProblems.map((problem, index) => {currentProblems.map((problem, index) => (
<AlertCard <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} problem={problem}
panelOptions={panelOptions} panelOptions={panelOptions}
onTagClick={this.handleTagClick} onTagClick={this.handleTagClick}
onProblemAck={this.handleProblemAck} onProblemAck={this.handleProblemAck}
/> />
)} ))}
</ol> </ol>
</section> </section>
@@ -101,10 +101,9 @@ interface PaginationControlProps {
} }
class PaginationControl extends PureComponent<PaginationControlProps> { class PaginationControl extends PureComponent<PaginationControlProps> {
handlePageChange = (index: number) => () => { handlePageChange = (index: number) => () => {
this.props.onPageChange(index); this.props.onPageChange(index);
} };
render() { render() {
const { itemsLength, pageIndex, pageSize } = this.props; const { itemsLength, pageIndex, pageSize } = this.props;
@@ -116,23 +115,20 @@ class PaginationControl extends PureComponent<PaginationControlProps> {
const startPage = Math.max(pageIndex - 3, 0); const startPage = Math.max(pageIndex - 3, 0);
const endPage = Math.min(pageCount, startPage + 9); const endPage = Math.min(pageCount, startPage + 9);
const pageLinks = []; const pageLinks = [];
for (let i = startPage; i < endPage; i++) { 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 value = i + 1;
const pageLinkElem = ( const pageLinkElem = (
<li key={value.toString()}> <li key={value.toString()}>
<a className={pageLinkClass} onClick={this.handlePageChange(i)}>{value}</a> <a className={pageLinkClass} onClick={this.handlePageChange(i)}>
{value}
</a>
</li> </li>
); );
pageLinks.push(pageLinkElem); pageLinks.push(pageLinkElem);
} }
return ( return <ul>{pageLinks}</ul>;
<ul>
{pageLinks}
</ul>
);
} }
} }

View File

@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { ZBXTag } from '../types';
import { DataSourceRef } from '@grafana/data'; import { DataSourceRef } from '@grafana/data';
import { Tooltip } from '@grafana/ui'; import { Tooltip } from '@grafana/ui';
import { ZBXTag } from '../../datasource-zabbix/types';
const TAG_COLORS = [ const TAG_COLORS = [
'#E24D42', '#E24D42',
@@ -96,10 +96,10 @@ interface EventTagProps {
export default class EventTag extends PureComponent<EventTagProps> { export default class EventTag extends PureComponent<EventTagProps> {
handleClick = (event) => { handleClick = (event) => {
if (this.props.onClick) { if (this.props.onClick) {
const { tag, datasource} = this.props; const { tag, datasource } = this.props;
this.props.onClick(tag, datasource, event.ctrlKey, event.shiftKey); this.props.onClick(tag, datasource, event.ctrlKey, event.shiftKey);
} }
} };
render() { render() {
const { tag, highlight } = this.props; const { tag, highlight } = this.props;
@@ -110,13 +110,12 @@ export default class EventTag extends PureComponent<EventTagProps> {
}; };
return ( return (
<Tooltip placement="bottom" content="Click to add tag filter or Ctrl/Shift+click to remove"> <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} style={style}
onClick={this.handleClick}> onClick={this.handleClick}
{tag.value ? >
`${tag.tag}: ${tag.value}` : {tag.value ? `${tag.tag}: ${tag.value}` : `${tag.tag}`}
`${tag.tag}`
}
</span> </span>
</Tooltip> </Tooltip>
); );

View File

@@ -46,7 +46,7 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
async componentDidMount() { async componentDidMount() {
const scripts = await this.props.getScripts(); const scripts = await this.props.getScripts();
this.scripts = scripts; this.scripts = scripts;
const scriptOptions: Array<SelectableValue<string>> = scripts.map(s => { const scriptOptions: Array<SelectableValue<string>> = scripts.map((s) => {
return { return {
value: s.scriptid, value: s.scriptid,
label: s.name, label: s.name,
@@ -55,27 +55,27 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
}); });
const selectedScript = scriptOptions?.length ? scriptOptions[0] : null; 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 }); this.setState({ scriptOptions, selectedScript, script });
} }
onChangeSelectedScript = (v: SelectableValue<string>) => { 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: '' }); this.setState({ selectedScript: v, script, errorMessage: '', loading: false, result: '' });
}; };
dismiss = () => { dismiss = () => {
this.setState({ selectedScript: null, error: false, errorMessage: '', selectError: '', loading: false }); this.setState({ selectedScript: null, error: false, errorMessage: '', selectError: '', loading: false });
this.props.onDismiss(); this.props.onDismiss();
} };
submit = () => { submit = () => {
const { selectedScript } = this.state; const { selectedScript } = this.state;
if (!selectedScript) { if (!selectedScript) {
return this.setState({ 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, scriptid: selectedScript.value,
}; };
this.props.onSubmit(data).then((result: APIExecuteScriptResponse) => { this.props
const message = this.formatResult(result?.value || ''); .onSubmit(data)
if (result?.response === 'success') { .then((result: APIExecuteScriptResponse) => {
this.setState({ result: message, loading: false }); const message = this.formatResult(result?.value || '');
} else { if (result?.response === 'success') {
this.setState({ error: true, errorMessage: message, loading: false }); this.setState({ result: message, loading: false });
} } else {
}).catch(err => { this.setState({ error: true, errorMessage: message, loading: false });
let errorMessage = err.data?.message || err.data?.error || err.data || err.statusText || ''; }
errorMessage = this.formatResult(errorMessage); })
this.setState({ .catch((err) => {
error: true, let errorMessage = err.data?.message || err.data?.error || err.data || err.statusText || '';
loading: false, errorMessage = this.formatResult(errorMessage);
errorMessage, this.setState({
error: true,
loading: false,
errorMessage,
});
}); });
}); };
}
formatResult = (result: string) => { formatResult = (result: string) => {
const formatted = result.split('\n').map((p, i) => { const formatted = result.split('\n').map((p, i) => {
return <p key={i}>{p}</p>; return <p key={i}>{p}</p>;
}); });
return <>{formatted}</>; return <>{formatted}</>;
} };
render() { render() {
const { theme } = this.props; const { theme } = this.props;
@@ -135,14 +138,8 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
> >
<div className="gf-form"> <div className="gf-form">
<label className="gf-form-hint"> <label className="gf-form-hint">
<Select <Select options={scriptOptions} value={selectedScript} onChange={this.onChangeSelectedScript} />
options={scriptOptions} {selectError && <small className={selectErrorClass}>{selectError}</small>}
value={selectedScript}
onChange={this.onChangeSelectedScript}
/>
{selectError &&
<small className={selectErrorClass}>{selectError}</small>
}
</label> </label>
</div> </div>
<div className={scriptCommandContainerClass}> <div className={scriptCommandContainerClass}>
@@ -150,17 +147,17 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
</div> </div>
<div className={styles.resultContainer}> <div className={styles.resultContainer}>
{result && {result && <span className={styles.execResult}>{result}</span>}
<span className={styles.execResult}>{result}</span> {error && <span className={styles.execError}>{errorMessage}</span>}
}
{error &&
<span className={styles.execError}>{errorMessage}</span>
}
</div> </div>
<div className="gf-form-button-row text-center"> <div className="gf-form-button-row text-center">
<Button variant="primary" onClick={this.submit}>Execute</Button> <Button variant="primary" onClick={this.submit}>
<Button variant="secondary" onClick={this.dismiss}>Cancel</Button> Execute
</Button>
<Button variant="secondary" onClick={this.dismiss}>
Cancel
</Button>
</div> </div>
</Modal> </Modal>
); );
@@ -211,8 +208,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
margin-bottom: 0px; margin-bottom: 0px;
} }
`, `,
execResult: css` execResult: css``,
`,
execError: css` execError: css`
color: ${red}; color: ${red};
`, `,

View 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>
);
};

View File

@@ -21,12 +21,12 @@ export const AckCell: React.FC<RTCell<ProblemDTO>> = (props: RTCell<ProblemDTO>)
return ( return (
<div> <div>
{problem.acknowledges?.length > 0 && {problem.acknowledges?.length > 0 && (
<> <>
<FAIcon icon="comments" /> <FAIcon icon="comments" />
<span className={styles.countLabel}> ({problem.acknowledges?.length})</span> <span className={styles.countLabel}> ({problem.acknowledges?.length})</span>
</> </>
} )}
</div> </div>
); );
}; };

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { ZBXAcknowledge } from '../../types'; import { ZBXAcknowledge } from '../../../datasource-zabbix/types';
interface AcknowledgesListProps { interface AcknowledgesListProps {
acknowledges: ZBXAcknowledge[]; acknowledges: ZBXAcknowledge[];
@@ -10,13 +10,25 @@ export default function AcknowledgesList(props: AcknowledgesListProps) {
return ( return (
<div className="problem-ack-list"> <div className="problem-ack-list">
<div className="problem-ack-col problem-ack-time"> <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>
<div className="problem-ack-col problem-ack-user"> <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>
<div className="problem-ack-col problem-ack-message"> <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>
</div> </div>
); );

View File

@@ -1,24 +1,24 @@
import React, { FC, PureComponent } from 'react'; import React, { FC, PureComponent } from 'react';
import moment from 'moment'; import moment from 'moment';
import { TimeRange, DataSourceRef } from "@grafana/data"; import { TimeRange, DataSourceRef } from '@grafana/data';
import { Tooltip } from '@grafana/ui'; import { Tooltip } from '@grafana/ui';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import * as utils from '../../../datasource-zabbix/utils'; import * as utils from '../../../datasource-zabbix/utils';
import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXGroup, ZBXHost, ZBXTag } from '../../../datasource-zabbix/types'; import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXGroup, ZBXHost, ZBXTag } from '../../../datasource-zabbix/types';
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types';
import { GFTimeRange, RTRow, ZBXItem } from '../../types';
import { AckModal, AckProblemData } from '../AckModal'; import { AckModal, AckProblemData } from '../AckModal';
import EventTag from '../EventTag'; import EventTag from '../EventTag';
import AcknowledgesList from './AcknowledgesList'; import AcknowledgesList from './AcknowledgesList';
import ProblemTimeline from './ProblemTimeline'; import ProblemTimeline from './ProblemTimeline';
import { AckButton, ExecScriptButton, ExploreButton, FAIcon, ModalController } from '../../../components'; import { AckButton, ExecScriptButton, ExploreButton, FAIcon, ModalController } from '../../../components';
import { ExecScriptData, ExecScriptModal } from '../ExecScriptModal'; 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> { interface ProblemDetailsProps extends RTRow<ProblemDTO> {
rootWidth: number; rootWidth: number;
timeRange: GFTimeRange; timeRange: TimeRange;
range: TimeRange;
showTimeline?: boolean; showTimeline?: boolean;
panelId?: number; panelId?: number;
getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>; getProblemEvents: (problem: ProblemDTO) => Promise<ZBXEvent[]>;
@@ -65,16 +65,14 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
fetchProblemEvents() { fetchProblemEvents() {
const problem = this.props.original; const problem = this.props.original;
this.props.getProblemEvents(problem) this.props.getProblemEvents(problem).then((events) => {
.then(events => {
this.setState({ events }); this.setState({ events });
}); });
} }
fetchProblemAlerts() { fetchProblemAlerts() {
const problem = this.props.original; const problem = this.props.original;
this.props.getProblemAlerts(problem) this.props.getProblemAlerts(problem).then((alerts) => {
.then(alerts => {
this.setState({ alerts }); this.setState({ alerts });
}); });
} }
@@ -97,15 +95,15 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
render() { render() {
const problem = this.props.original as ProblemDTO; const problem = this.props.original as ProblemDTO;
const alerts = this.state.alerts; const alerts = this.state.alerts;
const { rootWidth, panelId, range } = this.props; const { rootWidth, panelId, timeRange } = this.props;
const displayClass = this.state.show ? 'show' : ''; const displayClass = this.state.show ? 'show' : '';
const wideLayout = rootWidth > 1200; 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 age = moment.unix(problem.timestamp).fromNow(true);
const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0; const showAcknowledges = problem.acknowledges && problem.acknowledges.length !== 0;
const problemSeverity = Number(problem.severity); const problemSeverity = Number(problem.severity);
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) { if ((this.props.original.datasource as DataSourceRef)?.uid) {
const dsInstance = getDataSourceSrv().getInstanceSettings((this.props.original.datasource as DataSourceRef).uid); const dsInstance = getDataSourceSrv().getInstanceSettings((this.props.original.datasource as DataSourceRef).uid);
dsName = dsInstance.name; dsName = dsInstance.name;
@@ -117,106 +115,111 @@ export class ProblemDetails extends PureComponent<ProblemDetailsProps, ProblemDe
<div className="problem-details"> <div className="problem-details">
<div className="problem-details-head"> <div className="problem-details-head">
<div className="problem-actions-left"> <div className="problem-actions-left">
<ExploreButton problem={problem} panelId={panelId} range={range}/> <ExploreButton problem={problem} panelId={panelId} range={timeRange} />
</div> </div>
{problem.showAckButton && {problem.showAckButton && (
<div className="problem-actions"> <div className="problem-actions">
<ModalController> <ModalController>
{({ showModal, hideModal }) => ( {({ showModal, hideModal }) => (
<ExecScriptButton <ExecScriptButton
className="problem-action-button" className="problem-action-button"
onClick={() => { onClick={() => {
showModal(ExecScriptModal, { showModal(ExecScriptModal, {
getScripts: this.getScripts, getScripts: this.getScripts,
onSubmit: this.onExecuteScript, onSubmit: this.onExecuteScript,
onDismiss: hideModal, onDismiss: hideModal,
}); });
}} }}
/> />
)} )}
</ModalController> </ModalController>
<ModalController> <ModalController>
{({ showModal, hideModal }) => ( {({ showModal, hideModal }) => (
<AckButton <AckButton
className="problem-action-button" className="problem-action-button"
onClick={() => { onClick={() => {
showModal(AckModal, { showModal(AckModal, {
canClose: problem.manual_close === '1', canClose: problem.manual_close === '1',
severity: problemSeverity, severity: problemSeverity,
onSubmit: this.ackProblem, onSubmit: this.ackProblem,
onDismiss: hideModal, onDismiss: hideModal,
}); });
}} }}
/> />
)} )}
</ModalController> </ModalController>
</div> </div>
} )}
<ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'}/> <ProblemStatusBar problem={problem} alerts={alerts} className={compactStatusBar && 'compact'} />
</div> </div>
<div className="problem-details-row"> <div className="problem-details-row">
<div className="problem-value-container"> <div className="problem-value-container">
<div className="problem-age"> <div className="problem-age">
<FAIcon icon="clock-o"/> <FAIcon icon="clock-o" />
<span>{age}</span> <span>{age}</span>
</div> </div>
{problem.items && <ProblemItems items={problem.items}/>} {problem.items && <ProblemItems items={problem.items} />}
</div> </div>
</div> </div>
{problem.comments && {problem.comments && (
<div className="problem-description-row"> <div className="problem-description-row">
<div className="problem-description"> <div className="problem-description">
<Tooltip placement="right" content={problem.comments}> <Tooltip placement="right" content={problem.comments}>
<span className="description-label">Description:&nbsp;</span> <span className="description-label">Description:&nbsp;</span>
</Tooltip> </Tooltip>
<span>{problem.comments}</span> <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>
</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">
<div className="problem-details-right-item"> <div className="problem-details-right-item">
<FAIcon icon="database"/> <FAIcon icon="database" />
<span>{dsName}</span> <span>{dsName}</span>
</div> </div>
{problem.proxy && {problem.proxy && (
<div className="problem-details-right-item"> <div className="problem-details-right-item">
<FAIcon icon="cloud"/> <FAIcon icon="cloud" />
<span>{problem.proxy}</span> <span>{problem.proxy}</span>
</div> </div>
} )}
{problem.groups && <ProblemGroups groups={problem.groups} className="problem-details-right-item"/>} {problem.groups && <ProblemGroups groups={problem.groups} className="problem-details-right-item" />}
{problem.hosts && <ProblemHosts hosts={problem.hosts} className="problem-details-right-item"/>} {problem.hosts && <ProblemHosts hosts={problem.hosts} className="problem-details-right-item" />}
</div> </div>
</div> </div>
</div> </div>
@@ -232,11 +235,17 @@ interface ProblemItemProps {
function ProblemItem(props: ProblemItemProps) { function ProblemItem(props: ProblemItemProps) {
const { item, showName } = props; const { item, showName } = props;
const itemName = utils.expandItemName(item.name, item.key_); const itemName = utils.expandItemName(item.name, item.key_);
const tooltipContent = () => <>{itemName}<br/>{item.lastvalue}</>; const tooltipContent = () => (
<>
{itemName}
<br />
{item.lastvalue}
</>
);
return ( return (
<div className="problem-item"> <div className="problem-item">
<FAIcon icon="thermometer-three-quarters"/> <FAIcon icon="thermometer-three-quarters" />
{showName && <span className="problem-item-name">{item.name}: </span>} {showName && <span className="problem-item-name">{item.name}: </span>}
<Tooltip placement="top-start" content={tooltipContent}> <Tooltip placement="top-start" content={tooltipContent}>
<span className="problem-item-value">{item.lastvalue}</span> <span className="problem-item-value">{item.lastvalue}</span>
@@ -252,10 +261,11 @@ interface ProblemItemsProps {
const ProblemItems: FC<ProblemItemsProps> = ({ items }) => { const ProblemItems: FC<ProblemItemsProps> = ({ items }) => {
return ( return (
<div className="problem-items-row"> <div className="problem-items-row">
{items.length > 1 ? {items.length > 1 ? (
items.map(item => <ProblemItem item={item} key={item.itemid} showName={true}/>) : items.map((item) => <ProblemItem item={item} key={item.itemid} showName={true} />)
<ProblemItem item={items[0]}/> ) : (
} <ProblemItem item={items[0]} />
)}
</div> </div>
); );
}; };
@@ -267,9 +277,9 @@ interface ProblemGroupsProps {
class ProblemGroups extends PureComponent<ProblemGroupsProps> { class ProblemGroups extends PureComponent<ProblemGroupsProps> {
render() { render() {
return this.props.groups.map(g => ( return this.props.groups.map((g) => (
<div className={this.props.className || ''} key={g.groupid}> <div className={this.props.className || ''} key={g.groupid}>
<FAIcon icon="folder"/> <FAIcon icon="folder" />
<span>{g.name}</span> <span>{g.name}</span>
</div> </div>
)); ));
@@ -283,9 +293,9 @@ interface ProblemHostsProps {
class ProblemHosts extends PureComponent<ProblemHostsProps> { class ProblemHosts extends PureComponent<ProblemHostsProps> {
render() { render() {
return this.props.hosts.map(h => ( return this.props.hosts.map((h) => (
<div className={this.props.className || ''} key={h.hostid}> <div className={this.props.className || ''} key={h.hostid}>
<FAIcon icon="server"/> <FAIcon icon="server" />
<span>{h.name}</span> <span>{h.name}</span>
</div> </div>
)); ));

View File

@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { Tooltip } from '@grafana/ui'; import { Tooltip } from '@grafana/ui';
import FAIcon from '../../../components/FAIcon/FAIcon'; import FAIcon from '../../../components/FAIcon/FAIcon';
import { ZBXTrigger, ZBXAlert } from '../../types'; import { ZBXAlert, ProblemDTO } from '../../../datasource-zabbix/types';
export interface ProblemStatusBarProps { export interface ProblemStatusBarProps {
problem: ZBXTrigger; problem: ProblemDTO;
alerts?: ZBXAlert[]; alerts?: ZBXAlert[];
className?: string; className?: string;
} }
@@ -26,7 +26,11 @@ export default function ProblemStatusBar(props: ProblemStatusBarProps) {
<ProblemStatusBarItem icon="wrench" fired={maintenance} tooltip="Host maintenance" /> <ProblemStatusBarItem icon="wrench" fired={maintenance} tooltip="Host maintenance" />
<ProblemStatusBarItem icon="globe" fired={link} link={link && problem.url} tooltip="External link" /> <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="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="circle-o-notch" fired={actions} tooltip={actionMessage} />
<ProblemStatusBarItem icon="question-circle" fired={stateUnknown} tooltip="Current trigger state is unknown" /> <ProblemStatusBarItem icon="question-circle" fired={stateUnknown} tooltip="Current trigger state is unknown" />
<ProblemStatusBarItem icon="warning" fired={error} tooltip={problem.error} /> <ProblemStatusBarItem icon="warning" fired={error} tooltip={problem.error} />
@@ -56,5 +60,11 @@ function ProblemStatusBarItem(props: ProblemStatusBarItemProps) {
</Tooltip> </Tooltip>
); );
} }
return link ? <a href={link} target="_blank">{item}</a> : item; return link ? (
<a href={link} target="_blank">
{item}
</a>
) : (
item
);
} }

View File

@@ -1,7 +1,8 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; 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_OK_COLOR = 'rgb(56, 189, 113)';
const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)'; 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 { export interface ProblemTimelineProps {
events: ZBXEvent[]; events: ZBXEvent[];
timeRange: GFTimeRange; timeRange: TimeRange;
okColor?: string; okColor?: string;
problemColor?: string; problemColor?: string;
eventRegionHeight?: number; eventRegionHeight?: number;
@@ -51,7 +52,7 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
highlightedEvent: null, highlightedEvent: null,
highlightedRegion: null, highlightedRegion: null,
showEventInfo: false, showEventInfo: false,
eventInfo: {} eventInfo: {},
}; };
} }
@@ -62,11 +63,11 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
} }
} }
setRootRef = ref => { setRootRef = (ref) => {
this.rootRef = ref; this.rootRef = ref;
const width = ref && ref.clientWidth || 0; const width = (ref && ref.clientWidth) || 0;
this.setState({ width }); this.setState({ width });
} };
handlePointHighlight = (index: number, secondIndex?: number) => { handlePointHighlight = (index: number, secondIndex?: number) => {
const event: ZBXEvent = this.sortedEvents[index]; const event: ZBXEvent = this.sortedEvents[index];
@@ -80,15 +81,15 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
showEventInfo: true, showEventInfo: true,
highlightedRegion: regionToHighlight, highlightedRegion: regionToHighlight,
eventInfo: { eventInfo: {
duration duration,
} },
}); });
// this.showEventInfo(event); // this.showEventInfo(event);
} };
handlePointUnHighlight = () => { handlePointUnHighlight = () => {
this.setState({ showEventInfo: false, highlightedRegion: null }); this.setState({ showEventInfo: false, highlightedRegion: null });
} };
handleAckHighlight = (ack: ZBXAcknowledge, index: number) => { handleAckHighlight = (ack: ZBXAcknowledge, index: number) => {
this.setState({ this.setState({
@@ -96,34 +97,34 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
eventInfo: { eventInfo: {
timestamp: Number(ack.clock), timestamp: Number(ack.clock),
message: ack.message, message: ack.message,
} },
}); });
} };
handleAckUnHighlight = () => { handleAckUnHighlight = () => {
this.setState({ showEventInfo: false }); this.setState({ showEventInfo: false });
} };
showEventInfo = (event: ZBXEvent) => { showEventInfo = (event: ZBXEvent) => {
this.setState({ highlightedEvent: event, showEventInfo: true }); this.setState({ highlightedEvent: event, showEventInfo: true });
} };
hideEventInfo = () => { hideEventInfo = () => {
this.setState({ showEventInfo: false }); this.setState({ showEventInfo: false });
} };
getRegionToHighlight = (index: number): number => { getRegionToHighlight = (index: number): number => {
const event = this.sortedEvents[index]; const event = this.sortedEvents[index];
const regionToHighlight = event.value === '1' ? index + 1 : index; const regionToHighlight = event.value === '1' ? index + 1 : index;
return regionToHighlight; return regionToHighlight;
} };
getEventDuration(firstIndex: number, secondIndex: number): number { getEventDuration(firstIndex: number, secondIndex: number): number {
return Math.abs(Number(this.sortedEvents[firstIndex].clock) - Number(this.sortedEvents[secondIndex].clock)) * 1000; return Math.abs(Number(this.sortedEvents[firstIndex].clock) - Number(this.sortedEvents[secondIndex].clock)) * 1000;
} }
sortEvents() { sortEvents() {
const events = _.sortBy(this.props.events, e => Number(e.clock)); const events = _.sortBy(this.props.events, (e) => Number(e.clock));
this.sortedEvents = events; this.sortedEvents = events;
return 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() { render() {
@@ -156,13 +157,14 @@ export default class ProblemTimeline extends PureComponent<ProblemTimelineProps,
const timelineYpos = Math.round(boxHeight / 2 - eventPointSize / 2); const timelineYpos = Math.round(boxHeight / 2 - eventPointSize / 2);
return ( return (
<div className="event-timeline" ref={this.setRootRef} style={{ transform: `translate(${-padding}px, 0)`}}> <div className="event-timeline" ref={this.setRootRef} style={{ transform: `translate(${-padding}px, 0)` }}>
<TimelineInfoContainer className="timeline-info-container" <TimelineInfoContainer
className="timeline-info-container"
event={this.state.highlightedEvent} event={this.state.highlightedEvent}
eventInfo={this.state.eventInfo} eventInfo={this.state.eventInfo}
show={this.state.showEventInfo} show={this.state.showEventInfo}
left={padding} left={padding}
/> />
<svg className="event-timeline-canvas" viewBox={`0 0 ${boxWidth} ${boxHeight}`}> <svg className="event-timeline-canvas" viewBox={`0 0 ${boxWidth} ${boxHeight}`}>
<defs> <defs>
<TimelineSVGFilters /> <TimelineSVGFilters />
@@ -265,7 +267,7 @@ class TimelineInfoContainer extends PureComponent<TimelineInfoContainerProps> {
<span key="ts" className="event-timestamp"> <span key="ts" className="event-timestamp">
<span className="event-timestamp-label">Time:&nbsp;</span> <span className="event-timestamp-label">Time:&nbsp;</span>
<span className="event-timestamp-value">{tsFormatted}</span> <span className="event-timestamp-value">{tsFormatted}</span>
</span> </span>,
]; ];
} }
if (eventInfo && eventInfo.duration) { if (eventInfo && eventInfo.duration) {
@@ -285,7 +287,7 @@ class TimelineInfoContainer extends PureComponent<TimelineInfoContainerProps> {
<span key="ts" className="event-timestamp"> <span key="ts" className="event-timestamp">
<span className="event-timestamp-label">Time:&nbsp;</span> <span className="event-timestamp-label">Time:&nbsp;</span>
<span className="event-timestamp-value">{tsFormatted}</span> <span className="event-timestamp-value">{tsFormatted}</span>
</span> </span>,
]; ];
} }
@@ -296,20 +298,16 @@ class TimelineInfoContainer extends PureComponent<TimelineInfoContainerProps> {
return ( return (
<div className={className} style={containerStyle}> <div className={className} style={containerStyle}>
<div> <div>{infoItems}</div>
{infoItems} <div>{durationItem}</div>
</div> {eventInfo && eventInfo.message && (
<div>
{durationItem}
</div>
{eventInfo && eventInfo.message &&
<div> <div>
<span key="duration" className="event-timestamp"> <span key="duration" className="event-timestamp">
<span className="event-timestamp-label">Message:&nbsp;</span> <span className="event-timestamp-label">Message:&nbsp;</span>
<span className="event-timestamp-value">{eventInfo.message}</span> <span className="event-timestamp-value">{eventInfo.message}</span>
</span> </span>
</div> </div>
} )}
</div> </div>
); );
} }
@@ -317,7 +315,7 @@ class TimelineInfoContainer extends PureComponent<TimelineInfoContainerProps> {
interface TimelineRegionsProps { interface TimelineRegionsProps {
events: ZBXEvent[]; events: ZBXEvent[];
timeRange: GFTimeRange; timeRange: TimeRange;
width: number; width: number;
height: number; height: number;
okColor?: string; okColor?: string;
@@ -332,7 +330,8 @@ class TimelineRegions extends PureComponent<TimelineRegionsProps> {
render() { render() {
const { events, timeRange, width, height, highlightedRegion } = this.props; 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; const range = timeTo - timeFrom;
let firstItem: React.ReactNode; let firstItem: React.ReactNode;
@@ -349,9 +348,7 @@ class TimelineRegions extends PureComponent<TimelineRegionsProps> {
width: regionWidth, width: regionWidth,
height: height, height: height,
}; };
firstItem = ( firstItem = <rect key="0" className={className} {...firstEventAttributes}></rect>;
<rect key='0' className={className} {...firstEventAttributes}></rect>
);
} }
const eventsIntervalItems = events.map((event, index) => { 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 nextTs = index < events.length - 1 ? Number(events[index + 1].clock) : timeTo;
const duration = (nextTs - ts) / range; const duration = (nextTs - ts) / range;
const regionWidth = Math.round(duration * width); 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 highlighted = highlightedRegion && highlightedRegion - 1 === index;
const valueClass = `problem-event--${event.value === '1' ? 'problem' : 'ok'}`; const valueClass = `problem-event--${event.value === '1' ? 'problem' : 'ok'}`;
const className = `problem-event-region ${valueClass} ${highlighted ? 'highlighted' : ''}`; const className = `problem-event-region ${valueClass} ${highlighted ? 'highlighted' : ''}`;
@@ -370,21 +367,16 @@ class TimelineRegions extends PureComponent<TimelineRegionsProps> {
height: height, height: height,
}; };
return ( return <rect key={`${event.eventid}-${index}`} className={className} {...attributes} />;
<rect key={`${event.eventid}-${index}`} className={className} {...attributes} />
);
}); });
return [ return [firstItem, eventsIntervalItems];
firstItem,
eventsIntervalItems
];
} }
} }
interface TimelinePointsProps { interface TimelinePointsProps {
events: ZBXEvent[]; events: ZBXEvent[];
timeRange: GFTimeRange; timeRange: TimeRange;
width: number; width: number;
pointSize: number; pointSize: number;
okColor?: string; okColor?: string;
@@ -415,7 +407,7 @@ class TimelinePoints extends PureComponent<TimelinePointsProps, TimelinePointsSt
order = moveToEnd(order, indexes); order = moveToEnd(order, indexes);
const highlighted = highlight ? indexes : null; const highlighted = highlight ? indexes : null;
this.setState({ order, highlighted }); this.setState({ order, highlighted });
} };
highlightPoint = (index: number) => () => { highlightPoint = (index: number) => () => {
let pointsToHighlight = [index]; let pointsToHighlight = [index];
@@ -429,12 +421,12 @@ class TimelinePoints extends PureComponent<TimelinePointsProps, TimelinePointsSt
} }
} }
this.bringToFront(pointsToHighlight, true); this.bringToFront(pointsToHighlight, true);
} };
getRegionEvents(index: number) { getRegionEvents(index: number) {
const events = this.props.events; const events = this.props.events;
const event = events[index]; const event = events[index];
if (event.value === '1' && index < events.length ) { if (event.value === '1' && index < events.length) {
// Problem event // Problem event
for (let i = index; i < events.length; i++) { for (let i = index; i < events.length; i++) {
if (events[i].value === '0') { if (events[i].value === '0') {
@@ -459,22 +451,23 @@ class TimelinePoints extends PureComponent<TimelinePointsProps, TimelinePointsSt
return [index]; return [index];
} }
unHighlightPoint = index => () => { unHighlightPoint = (index) => () => {
if (this.props.onPointUnHighlight) { if (this.props.onPointUnHighlight) {
this.props.onPointUnHighlight(); this.props.onPointUnHighlight();
} }
const order = this.props.events.map((v, i) => i); const order = this.props.events.map((v, i) => i);
this.setState({ order, highlighted: [] }); this.setState({ order, highlighted: [] });
} };
render() { render() {
const { events, timeRange, width, pointSize } = this.props; 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 range = timeTo - timeFrom;
const pointR = pointSize / 2; const pointR = pointSize / 2;
const eventsItems = events.map((event, i) => { const eventsItems = events.map((event, i) => {
const ts = Number(event.clock); 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 className = `problem-event-item problem-event--${event.value === '1' ? 'problem' : 'ok'}`;
const highlighted = this.state.highlighted.indexOf(i) !== -1; const highlighted = this.state.highlighted.indexOf(i) !== -1;
@@ -491,7 +484,7 @@ class TimelinePoints extends PureComponent<TimelinePointsProps, TimelinePointsSt
); );
}); });
if (this.state.order.length) { if (this.state.order.length) {
return this.state.order.map(i => eventsItems[i]); return this.state.order.map((i) => eventsItems[i]);
} }
return eventsItems; return eventsItems;
} }
@@ -528,13 +521,13 @@ class TimelinePoint extends PureComponent<TimelinePointProps, TimelinePointState
if (this.props.onPointHighlight) { if (this.props.onPointHighlight) {
this.props.onPointHighlight(); this.props.onPointHighlight();
} }
} };
handleMouseLeave = () => { handleMouseLeave = () => {
if (this.props.onPointUnHighlight) { if (this.props.onPointUnHighlight) {
this.props.onPointUnHighlight(); this.props.onPointUnHighlight();
} }
} };
render() { render() {
const { x } = this.props; const { x } = this.props;
@@ -543,11 +536,13 @@ class TimelinePoint extends PureComponent<TimelinePointProps, TimelinePointState
const rInner = Math.round(r * INNER_POINT_SIZE); const rInner = Math.round(r * INNER_POINT_SIZE);
const className = `${this.props.className || ''} ${this.state.highlighted ? 'highlighted' : ''}`; const className = `${this.props.className || ''} ${this.state.highlighted ? 'highlighted' : ''}`;
return ( return (
<g className={className} <g
className={className}
transform={`translate(${cx}, 0)`} transform={`translate(${cx}, 0)`}
filter="url(#dropShadow)" filter="url(#dropShadow)"
onMouseOver={this.handleMouseOver} 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={r} className="point-border" />
<circle cx={0} cy={0} r={rInner} className="point-core" /> <circle cx={0} cy={0} r={rInner} className="point-core" />
</g> </g>
@@ -557,7 +552,7 @@ class TimelinePoint extends PureComponent<TimelinePointProps, TimelinePointState
interface TimelineAcksProps { interface TimelineAcksProps {
acknowledges: ZBXAcknowledge[]; acknowledges: ZBXAcknowledge[];
timeRange: GFTimeRange; timeRange: TimeRange;
width: number; width: number;
size: number; size: number;
onHighlight?: (ack: ZBXAcknowledge, index: number) => void; onHighlight?: (ack: ZBXAcknowledge, index: number) => void;
@@ -581,7 +576,7 @@ class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
this.props.onHighlight(ack, index); this.props.onHighlight(ack, index);
} }
this.bringToFront(index, true); this.bringToFront(index, true);
} };
handleUnHighlight = () => { handleUnHighlight = () => {
if (this.props.onUnHighlight) { if (this.props.onUnHighlight) {
@@ -589,7 +584,7 @@ class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
} }
const order = this.props.acknowledges.map((v, i) => i); const order = this.props.acknowledges.map((v, i) => i);
this.setState({ order, highlighted: null }); this.setState({ order, highlighted: null });
} };
bringToFront = (index: number, highlight = false) => { bringToFront = (index: number, highlight = false) => {
const { acknowledges } = this.props; const { acknowledges } = this.props;
@@ -597,16 +592,17 @@ class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
order = moveToEnd(order, [index]); order = moveToEnd(order, [index]);
const highlighted = highlight ? index : null; const highlighted = highlight ? index : null;
this.setState({ order, highlighted }); this.setState({ order, highlighted });
} };
render() { render() {
const { acknowledges, timeRange, width, size } = this.props; 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 range = timeTo - timeFrom;
const pointR = size / 2; const pointR = size / 2;
const eventsItems = acknowledges.map((ack, i) => { const eventsItems = acknowledges.map((ack, i) => {
const ts = Number(ack.clock); 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; const highlighted = this.state.highlighted === i;
return ( return (
@@ -621,7 +617,7 @@ class TimelineAcks extends PureComponent<TimelineAcksProps, TimelineAcksState> {
); );
}); });
if (this.state.order.length) { if (this.state.order.length) {
return this.state.order.map(i => eventsItems[i]); return this.state.order.map((i) => eventsItems[i]);
} }
return eventsItems; return eventsItems;
} }
@@ -656,13 +652,13 @@ class TimelineAck extends PureComponent<TimelineAckProps, TimelineAckState> {
if (this.props.onHighlight) { if (this.props.onHighlight) {
this.props.onHighlight(); this.props.onHighlight();
} }
} };
handleUnHighlight = () => { handleUnHighlight = () => {
if (this.props.onUnHighlight) { if (this.props.onUnHighlight) {
this.props.onUnHighlight(); this.props.onUnHighlight();
} }
} };
render() { render() {
const { x } = this.props; const { x } = this.props;
@@ -671,11 +667,13 @@ class TimelineAck extends PureComponent<TimelineAckProps, TimelineAckState> {
const rInner = Math.round(r * INNER_POINT_SIZE); const rInner = Math.round(r * INNER_POINT_SIZE);
const className = `problem-event-ack ${this.state.highlighted ? 'highlighted' : ''}`; const className = `problem-event-ack ${this.state.highlighted ? 'highlighted' : ''}`;
return ( return (
<g className={className} <g
className={className}
transform={`translate(${cx}, 0)`} transform={`translate(${cx}, 0)`}
filter="url(#dropShadow)" filter="url(#dropShadow)"
onMouseOver={this.handleHighlight} 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={r} className="point-border" />
<circle cx={0} cy={0} r={rInner} className="point-core" /> <circle cx={0} cy={0} r={rInner} className="point-core" />
</g> </g>

View File

@@ -8,17 +8,17 @@ import EventTag from '../EventTag';
import { ProblemDetails } from './ProblemDetails'; import { ProblemDetails } from './ProblemDetails';
import { AckProblemData } from '../AckModal'; import { AckProblemData } from '../AckModal';
import { FAIcon, GFHeartIcon } from '../../../components'; 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 { ProblemDTO, ZBXAlert, ZBXEvent, ZBXTag } from '../../../datasource-zabbix/types';
import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types'; import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource-zabbix/zabbix/connectors/zabbix_api/types';
import { AckCell } from './AckCell'; import { AckCell } from './AckCell';
import { DataSourceRef, TimeRange } from "@grafana/data"; import { DataSourceRef, TimeRange } from '@grafana/data';
export interface ProblemListProps { export interface ProblemListProps {
problems: ProblemDTO[]; problems: ProblemDTO[];
panelOptions: ProblemsPanelOptions; panelOptions: ProblemsPanelOptions;
loading?: boolean; loading?: boolean;
timeRange?: GFTimeRange; timeRange?: TimeRange;
range?: TimeRange; range?: TimeRange;
pageSize?: number; pageSize?: number;
fontSize?: number; fontSize?: number;
@@ -52,7 +52,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
}; };
} }
setRootRef = ref => { setRootRef = (ref) => {
this.rootRef = ref; this.rootRef = ref;
}; };
@@ -60,8 +60,7 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
return this.props.onProblemAck(problem, data); return this.props.onProblemAck(problem, data);
}; };
onExecuteScript = (problem: ProblemDTO, data: AckProblemData) => { onExecuteScript = (problem: ProblemDTO, data: AckProblemData) => {};
};
handlePageSizeChange = (pageSize, pageIndex) => { handlePageSizeChange = (pageSize, pageIndex) => {
if (this.props.onPageSizeChange) { if (this.props.onPageSizeChange) {
@@ -132,10 +131,12 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
const result = []; const result = [];
const options = this.props.panelOptions; const options = this.props.panelOptions;
const highlightNewerThan = options.highlightNewEvents && options.highlightNewerThan; const highlightNewerThan = options.highlightNewEvents && options.highlightNewerThan;
const statusCell = props => StatusCell(props, highlightNewerThan); const statusCell = (props) => StatusCell(props, highlightNewerThan);
const statusIconCell = props => StatusIconCell(props, highlightNewerThan); const statusIconCell = (props) => StatusIconCell(props, highlightNewerThan);
const hostNameCell = props => <HostCell name={props.original.host} maintenance={props.original.maintenance}/>; 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 hostTechNameCell = (props) => (
<HostCell name={props.original.hostTechName} maintenance={props.original.maintenance} />
);
const columns = [ const columns = [
{ Header: 'Host', id: 'host', show: options.hostField, Cell: hostNameCell }, { 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: 'Host Groups', accessor: 'groups', show: options.hostGroups, Cell: GroupCell },
{ Header: 'Proxy', accessor: 'proxy', show: options.hostProxy }, { Header: 'Proxy', accessor: 'proxy', show: options.hostProxy },
{ {
Header: 'Severity', show: options.severityField, className: 'problem-severity', width: 120, Header: 'Severity',
accessor: problem => problem.priority, show: options.severityField,
className: 'problem-severity',
width: 120,
accessor: (problem) => problem.priority,
id: 'severity', 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', accessor: 'value',
Cell: statusIconCell, Cell: statusIconCell,
}, },
{ Header: 'Status', accessor: 'value', show: options.statusField, width: 100, Cell: statusCell }, { Header: 'Status', accessor: 'value', show: options.statusField, width: 100, Cell: statusCell },
{ Header: 'Problem', accessor: 'description', minWidth: 200, Cell: ProblemCell }, { Header: 'Problem', accessor: 'description', minWidth: 200, Cell: ProblemCell },
{ {
Header: 'Ack', id: 'ack', show: options.ackField, width: 70, Header: 'Ack',
Cell: props => <AckCell {...props} /> id: 'ack',
show: options.ackField,
width: 70,
Cell: (props) => <AckCell {...props} />,
}, },
{ {
Header: 'Tags', accessor: 'tags', show: options.showTags, className: 'problem-tags', Header: 'Tags',
Cell: props => <TagCell {...props} onTagClick={this.handleTagClick}/> 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', id: 'age',
Cell: AgeCell, Cell: AgeCell,
}, },
{ {
Header: 'Time', className: 'last-change', width: 150, accessor: 'timestamp', Header: 'Time',
className: 'last-change',
width: 150,
accessor: 'timestamp',
id: 'lastchange', 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 }, { Header: '', className: 'custom-expander', width: 60, expander: true, Expander: CustomExpander },
]; ];
@@ -207,25 +235,25 @@ export default class ProblemList extends PureComponent<ProblemListProps, Problem
minRows={0} minRows={0}
loading={this.props.loading} loading={this.props.loading}
noDataText="No problems found" noDataText="No problems found"
SubComponent={props => SubComponent={(props) => (
<ProblemDetails {...props} <ProblemDetails
rootWidth={this.rootWidth} {...props}
timeRange={this.props.timeRange} rootWidth={this.rootWidth}
range={this.props.range} timeRange={this.props.timeRange}
showTimeline={panelOptions.problemTimeline} showTimeline={panelOptions.problemTimeline}
panelId={this.props.panelId} panelId={this.props.panelId}
getProblemEvents={this.props.getProblemEvents} getProblemEvents={this.props.getProblemEvents}
getProblemAlerts={this.props.getProblemAlerts} getProblemAlerts={this.props.getProblemAlerts}
getScripts={this.props.getScripts} getScripts={this.props.getScripts}
onProblemAck={this.handleProblemAck} onProblemAck={this.handleProblemAck}
onExecuteScript={this.props.onExecuteScript} onExecuteScript={this.props.onExecuteScript}
onTagClick={this.handleTagClick} onTagClick={this.handleTagClick}
subRows={false} subRows={false}
/> />
} )}
expanded={this.getExpandedPage(this.state.page)} expanded={this.getExpandedPage(this.state.page)}
onExpandedChange={this.handleExpandedChange} onExpandedChange={this.handleExpandedChange}
onPageChange={page => this.setState({ page })} onPageChange={(page) => this.setState({ page })}
onPageSizeChange={this.handlePageSizeChange} onPageSizeChange={this.handlePageSizeChange}
onResizedChange={this.handleResizedChange} onResizedChange={this.handleResizedChange}
/> />
@@ -243,7 +271,7 @@ const HostCell: React.FC<HostCellProps> = ({ name, maintenance }) => {
return ( return (
<div> <div>
<span style={{ paddingRight: '0.4rem' }}>{name}</span> <span style={{ paddingRight: '0.4rem' }}>{name}</span>
{maintenance && <FAIcon customClass="fired" icon="wrench"/>} {maintenance && <FAIcon customClass="fired" icon="wrench" />}
</div> </div>
); );
}; };
@@ -260,15 +288,15 @@ function SeverityCell(
let severityDesc: TriggerSeverity; let severityDesc: TriggerSeverity;
const severity = Number(problem.severity); 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') { 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; color = problem.value === '0' ? okColor : severityDesc.color;
// Mark acknowledged triggers with different color // Mark acknowledged triggers with different color
if (markAckEvents && problem.acknowledged === "1") { if (markAckEvents && problem.acknowledged === '1') {
color = ackEventColor; color = ackEventColor;
} }
@@ -290,7 +318,9 @@ function StatusCell(props: RTCell<ProblemDTO>, highlightNewerThan?: string) {
newProblem = isNewProblem(props.original, highlightNewerThan); newProblem = isNewProblem(props.original, highlightNewerThan);
} }
return ( 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) { if (highlightNewerThan) {
newProblem = isNewProblem(props.original, highlightNewerThan); newProblem = isNewProblem(props.original, highlightNewerThan);
} }
const className = classNames('zbx-problem-status-icon', const className = classNames(
'zbx-problem-status-icon',
{ 'problem-status--new': newProblem }, { 'problem-status--new': newProblem },
{ 'zbx-problem': props.value === '1' }, { '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>) { function GroupCell(props: RTCell<ProblemDTO>) {
let groups = ""; let groups = '';
if (props.value && props.value.length) { if (props.value && props.value.length) {
groups = props.value.map(g => g.name).join(', '); groups = props.value.map((g) => g.name).join(', ');
} }
return ( return <span>{groups}</span>;
<span>{groups}</span>
);
} }
function ProblemCell(props: RTCell<ProblemDTO>) { function ProblemCell(props: RTCell<ProblemDTO>) {
@@ -336,7 +365,7 @@ function AgeCell(props: RTCell<ProblemDTO>) {
} }
function LastChangeCell(props: RTCell<ProblemDTO>, customFormat?: string) { 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 problem = props.original;
const timestamp = moment.unix(problem.timestamp); const timestamp = moment.unix(problem.timestamp);
const format = customFormat || DEFAULT_TIME_FORMAT; const format = customFormat || DEFAULT_TIME_FORMAT;
@@ -358,14 +387,21 @@ class TagCell extends PureComponent<TagCellProps> {
render() { render() {
const tags = this.props.value || []; const tags = this.props.value || [];
return [ 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>) { function CustomExpander(props: RTCell<any>) {
return ( return (
<span className={props.isExpanded ? "expanded" : ""}> <span className={props.isExpanded ? 'expanded' : ''}>
<i className="fa fa-info-circle"></i> <i className="fa fa-info-circle"></i>
</span> </span>
); );

View 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>
);
};

View File

@@ -1,18 +1,20 @@
import _ from 'lodash'; import _ from 'lodash';
import { getNextRefIdChar } from './utils'; import { getNextRefIdChar } from './utils';
import { ShowProblemTypes } from '../datasource-zabbix/types'; import { ShowProblemTypes } from '../datasource-zabbix/types';
import { ProblemsPanelOptions } from './types';
import { PanelModel } from '@grafana/data';
// Actual schema version // Actual schema version
export const CURRENT_SCHEMA_VERSION = 8; export const CURRENT_SCHEMA_VERSION = 8;
export const getDefaultTarget = (targets?) => { export const getDefaultTarget = (targets?) => {
return { return {
group: {filter: ""}, group: { filter: '' },
host: {filter: ""}, host: { filter: '' },
application: {filter: ""}, application: { filter: '' },
trigger: {filter: ""}, trigger: { filter: '' },
tags: {filter: ""}, tags: { filter: '' },
proxy: {filter: ""}, proxy: { filter: '' },
refId: getNextRefIdChar(targets), refId: getNextRefIdChar(targets),
}; };
}; };
@@ -105,7 +107,7 @@ export function migratePanelSchema(panel) {
target.options = migrateOptions(panel); target.options = migrateOptions(panel);
_.defaults(target.options, getDefaultTargetOptions()); _.defaults(target.options, getDefaultTargetOptions());
_.defaults(target, { tags: { filter: "" } }); _.defaults(target, { tags: { filter: '' } });
} }
panel.sortProblems = panel.sortTriggersBy?.value === 'priority' ? 'priority' : 'lastchange'; panel.sortProblems = panel.sortTriggersBy?.value === 'priority' ? 'priority' : 'lastchange';
@@ -161,7 +163,7 @@ function isEmptyPanel(panel) {
} }
function isEmptyTargets(targets) { 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) { function isDefaultPanel(panel) {
@@ -169,7 +171,13 @@ function isDefaultPanel(panel) {
} }
function isDefaultTarget(target) { 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) { function isEmptyTarget(target) {
@@ -179,3 +187,53 @@ function isEmptyTarget(target) {
function isInvalidTarget(target, targetKey) { function isInvalidTarget(target, targetKey) {
return target && target.refId === 'A' && targetKey === '0'; 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;
};

View File

@@ -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
};

View 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%' },
];

View File

@@ -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,
};
}

View File

@@ -1 +0,0 @@
<div class="triggers-panel-container"></div>

View File

@@ -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">&nbsp;</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">&nbsp;</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>

View File

@@ -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(),
})),
});

View File

@@ -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);
});
});

View File

@@ -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() });
}

View File

@@ -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';

View File

@@ -1,9 +1,11 @@
import { DataSourceRef } from "@grafana/data"; import { DataSourceRef } from '@grafana/data';
import { CURRENT_SCHEMA_VERSION } from './migrations';
export interface ProblemsPanelOptions { export interface ProblemsPanelOptions {
schemaVersion: number; schemaVersion: number;
datasources: any[]; datasources: any[];
targets: ProblemsPanelTarget[]; targets: ProblemsPanelTarget[];
layout: 'table' | 'list';
// Fields // Fields
hostField?: boolean; hostField?: boolean;
hostTechNameField?: boolean; hostTechNameField?: boolean;
@@ -19,15 +21,9 @@ export interface ProblemsPanelOptions {
descriptionAtNewLine?: boolean; descriptionAtNewLine?: boolean;
// Options // Options
hostsInMaintenance?: boolean; hostsInMaintenance?: boolean;
showTriggers?: 'all triggers' | 'unacknowledged' | 'acknowledges'; showTriggers?: 'all triggers' | 'unacknowledged' | 'acknowledged';
sortTriggersBy?: { sortProblems?: 'default' | 'lastchange' | 'priority';
text: string; showEvents?: Number[];
value: 'lastchange' | 'priority';
};
showEvents?: {
text: 'All' | 'OK' | 'Problems';
value: 1 | Array<0 | 1>;
};
limit?: number; limit?: number;
// View options // View options
fontSize?: string; fontSize?: string;
@@ -46,24 +42,71 @@ export interface ProblemsPanelOptions {
markAckEvents?: boolean; 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 { export interface ProblemsPanelTarget {
group: { group: {
filter: string filter: string;
}; };
host: { host: {
filter: string filter: string;
}; };
application: { application: {
filter: string filter: string;
}; };
trigger: { trigger: {
filter: string filter: string;
}; };
tags: { tags: {
filter: string filter: string;
}; };
proxy: { proxy: {
filter: string filter: string;
}; };
datasource: string; datasource: string;
} }
@@ -77,108 +120,6 @@ export interface TriggerSeverity {
export type TriggerColor = string; 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> { export interface RTRow<T> {
/** the materialized row of data */ /** the materialized row of data */
row: any; row: any;

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"moduleResolution": "node", "moduleResolution": "node",
"target": "es5", "target": "ES6",
"lib": [ "es6", "dom", "es2017" ], "lib": [ "es6", "dom", "es2017" ],
"rootDir": "./src", "rootDir": "./src",
"jsx": "react", "jsx": "react",

View File

@@ -15,10 +15,9 @@ module.exports = {
target: 'node', target: 'node',
context: resolve('src'), context: resolve('src'),
entry: { entry: {
'module': './module.js', 'module': './module.ts',
'app_config_ctrl/config': './app_config_ctrl/config.js',
'datasource-zabbix/module': './datasource-zabbix/module.ts', 'datasource-zabbix/module': './datasource-zabbix/module.ts',
'panel-triggers/module': './panel-triggers/module.js', 'panel-triggers/module': './panel-triggers/module.tsx',
}, },
output: { output: {
filename: "[name].js", filename: "[name].js",

View File

@@ -16484,11 +16484,6 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@4.4.4:
version "4.4.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
typescript@4.6.4: typescript@4.6.4:
version "4.6.4" version "4.6.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
@@ -16499,6 +16494,11 @@ typescript@4.7.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
typescript@4.8.2:
version "4.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790"
integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==
ua-parser-js@^1.0.2: ua-parser-js@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"