Build plugin with grafana toolkit (#1539)

* Use grafana toolkit template for building plugin

* Fix linter and type errors

* Update styles building

* Fix sass deprecation warning

* Remove empty js files produced by webpack building sass

* Fix signing script

* Replace classnames with cx

* Fix data source config page

* Use custom webpack config instead of overriding original one

* Use gpx_ prefix for plugin executable

* Remove unused configs

* Roll back react hooks dependencies usage

* Move plugin-specific ts config to root config file

* Temporary do not use rst2html for function description tooltip

* Remove unused code

* remove unused dependencies

* update react table dependency

* Migrate tests to typescript

* remove unused dependencies

* Remove old webpack configs

* Add sign target to makefile

* Add magefile

* Update CI test job

* Update go packages

* Update build instructions

* Downgrade go version to 1.18

* Fix go version in ci

* Fix metric picker

* Add comment to webpack config

* remove angular mocks

* update bra config

* Rename datasource-zabbix to datasource (fix mage build)

* Add instructions for building backend with mage

* Fix webpack targets

* Fix ci backend tests

* Add initial e2e tests

* Fix e2e ci tests

* Update docker compose for cypress tests

* build grafana docker image

* Fix docker stop task

* CI: add Grafana compatibility check
This commit is contained in:
Alexander Zobnin
2022-12-09 14:14:34 +03:00
committed by GitHub
parent 26ed740945
commit e3e896742b
136 changed files with 5765 additions and 4636 deletions

View File

@@ -0,0 +1,195 @@
import _ from 'lodash';
import React, { useEffect, FormEvent } from 'react';
import { useAsyncFn } from 'react-use';
import { AnnotationQuery, SelectableValue } from '@grafana/data';
import { InlineField, InlineSwitch, Input, Select } from '@grafana/ui';
import { ZabbixMetricsQuery } from '../types';
import { ZabbixQueryEditorProps } from './QueryEditor';
import { QueryEditorRow } from './QueryEditor/QueryEditorRow';
import { MetricPicker } from '../../components';
import { getVariableOptions } from './QueryEditor/utils';
import { prepareAnnotation } from '../migrations';
const severityOptions: Array<SelectableValue<number>> = [
{ value: 0, label: 'Not classified' },
{ value: 1, label: 'Information' },
{ value: 2, label: 'Warning' },
{ value: 3, label: 'Average' },
{ value: 4, label: 'High' },
{ value: 5, label: 'Disaster' },
];
type Props = ZabbixQueryEditorProps & {
annotation?: AnnotationQuery<ZabbixMetricsQuery>;
onAnnotationChange?: (annotation: AnnotationQuery<ZabbixMetricsQuery>) => void;
};
export const AnnotationQueryEditor = ({ annotation, onAnnotationChange, datasource }: Props) => {
annotation = prepareAnnotation(annotation);
const query = annotation.target;
const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({
value: group.name,
label: group.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: groupsLoading, value: groupsOptions }, fetchGroups] = useAsyncFn(async () => {
const options = await loadGroupOptions();
return options;
}, []);
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter);
return options;
}, [query.group.filter]);
const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter);
return options;
}, [query.group.filter, query.host.filter]);
// Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
useEffect(() => {
fetchGroups();
}, []);
useEffect(() => {
fetchHosts();
}, [groupFilter]);
useEffect(() => {
fetchApps();
}, [groupFilter, hostFilter]);
const onChange = (query: any) => {
onAnnotationChange({
...annotation,
target: query,
});
};
const onFilterChange = (prop: string) => {
return (value: string) => {
if (value !== null) {
onChange({ ...query, [prop]: { filter: value } });
}
};
};
const onTextFilterChange = (prop: string) => {
return (v: FormEvent<HTMLInputElement>) => {
const newValue = v?.currentTarget?.value;
if (newValue !== null) {
onChange({ ...query, [prop]: { filter: newValue } });
}
};
};
const onMinSeverityChange = (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...query, options: { ...query.options, minSeverity: option.value } });
}
};
const onOptionSwitch = (prop: string) => () => {
onChange({ ...query, options: { ...query.options, [prop]: !query.options[prop] } });
};
return (
<>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
width={24}
value={query.group?.filter}
options={groupsOptions}
isLoading={groupsLoading}
onChange={onFilterChange('group')}
/>
</InlineField>
<InlineField label="Host" labelWidth={12}>
<MetricPicker
width={24}
value={query.host?.filter}
options={hostOptions}
isLoading={hostsLoading}
onChange={onFilterChange('host')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application?.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Problem" labelWidth={12}>
<Input
width={24}
defaultValue={query.trigger?.filter}
placeholder="Problem name"
onBlur={onTextFilterChange('trigger')}
/>
</InlineField>
</QueryEditorRow>
<>
<InlineField label="Min severity" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.options?.minSeverity}
options={severityOptions}
onChange={onMinSeverityChange}
/>
</InlineField>
<InlineField label="Show OK events" labelWidth={24}>
<InlineSwitch value={query.options.showOkEvents} onChange={onOptionSwitch('showOkEvents')} />
</InlineField>
<InlineField label="Hide acknowledged events" labelWidth={24}>
<InlineSwitch value={query.options.hideAcknowledged} onChange={onOptionSwitch('hideAcknowledged')} />
</InlineField>
<InlineField label="Show hostname" labelWidth={24}>
<InlineSwitch value={query.options.showHostname} onChange={onOptionSwitch('showHostname')} />
</InlineField>
</>
</>
);
};

View File

@@ -0,0 +1,345 @@
import React, { useEffect, useState } from 'react';
import { getDataSourceSrv } from '@grafana/runtime';
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, SelectableValue } from '@grafana/data';
import { Button, DataSourceHttpSettings, InlineFormLabel, LegacyForms, Select } from '@grafana/ui';
import { ZabbixDSOptions, ZabbixSecureJSONData } from '../types';
const { FormField, Switch } = LegacyForms;
const SUPPORTED_SQL_DS = ['mysql', 'postgres', 'influxdb'];
export type Props = DataSourcePluginOptionsEditorProps<ZabbixDSOptions, ZabbixSecureJSONData>;
export const ConfigEditor = (props: Props) => {
const { options, onOptionsChange } = props;
const [selectedDBDatasource, setSelectedDBDatasource] = useState(null);
const [currentDSType, setCurrentDSType] = useState('');
// Apply some defaults on initial render
useEffect(() => {
const { jsonData, secureJsonFields } = options;
// Set secureJsonFields.password to password and then remove it from config
const { password, ...restJsonData } = jsonData;
if (!secureJsonFields?.password) {
if (!options.secureJsonData) {
options.secureJsonData = {};
}
options.secureJsonData.password = password;
}
onOptionsChange({
...options,
jsonData: {
trends: true,
trendsFrom: '',
trendsRange: '',
cacheTTL: '',
timeout: undefined,
disableDataAlignment: false,
...restJsonData,
},
});
if (options.jsonData.dbConnectionEnable) {
if (!options.jsonData.dbConnectionDatasourceId) {
const dsName = options.jsonData.dbConnectionDatasourceName;
getDataSourceSrv()
.get(dsName)
.then((ds) => {
if (ds) {
const selectedDs = getDirectDBDatasources().find((dsOption) => dsOption.id === ds.id);
setSelectedDBDatasource({ label: selectedDs.name, value: selectedDs.id });
setCurrentDSType(selectedDs.type);
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
dbConnectionDatasourceId: ds.id,
},
});
}
});
} else {
const selectedDs = getDirectDBDatasources().find(
(dsOption) => dsOption.id === options.jsonData.dbConnectionDatasourceId
);
setSelectedDBDatasource({ label: selectedDs.name, value: selectedDs.id });
setCurrentDSType(selectedDs.type);
}
}
}, []);
return (
<>
<DataSourceHttpSettings
defaultUrl={'http://localhost/zabbix/api_jsonrpc.php'}
dataSourceConfig={options}
showAccessOptions={true}
onChange={onOptionsChange}
/>
<div className="gf-form-group">
<h3 className="page-heading">Zabbix API details</h3>
<div className="gf-form max-width-25">
<FormField
labelWidth={7}
inputWidth={15}
label="Username"
value={options.jsonData.username || ''}
onChange={jsonDataChangeHandler('username', options, onOptionsChange)}
required
/>
</div>
<div className="gf-form max-width-25">
{options.secureJsonFields?.password ? (
<>
<FormField
labelWidth={7}
inputWidth={15}
label="Password"
disabled={true}
value=""
placeholder="Configured"
/>
<Button onClick={resetSecureJsonField('password', options, onOptionsChange)}>Reset</Button>
</>
) : (
<FormField
labelWidth={7}
inputWidth={15}
label="Password"
type="password"
value={options.secureJsonData?.password || options.jsonData.password || ''}
onChange={secureJsonDataChangeHandler('password', options, onOptionsChange)}
required
/>
)}
</div>
<Switch
label="Trends"
labelClass="width-7"
checked={options.jsonData.trends}
onChange={jsonDataSwitchHandler('trends', options, onOptionsChange)}
/>
{options.jsonData.trends && (
<>
<div className="gf-form">
<FormField
labelWidth={7}
inputWidth={4}
label="After"
value={options.jsonData.trendsFrom || ''}
placeholder="7d"
onChange={jsonDataChangeHandler('trendsFrom', options, onOptionsChange)}
tooltip="Time after which trends will be used.
Best practice is to set this value to your history storage period (7d, 30d, etc)."
/>
</div>
<div className="gf-form">
<FormField
labelWidth={7}
inputWidth={4}
label="Range"
value={options.jsonData.trendsRange || ''}
placeholder="4d"
onChange={jsonDataChangeHandler('trendsRange', options, onOptionsChange)}
tooltip="Time range width after which trends will be used instead of history.
It's better to set this value in range of 4 to 7 days to prevent loading large amount of history data."
/>
</div>
</>
)}
<div className="gf-form">
<FormField
labelWidth={7}
inputWidth={4}
label="Cache TTL"
value={options.jsonData.cacheTTL || ''}
placeholder="1h"
onChange={jsonDataChangeHandler('cacheTTL', options, onOptionsChange)}
tooltip="Zabbix data source caches metric names in memory. Specify how often data will be updated."
/>
</div>
<div className="gf-form">
<FormField
labelWidth={7}
inputWidth={4}
type="number"
label="Timeout"
value={options.jsonData.timeout}
onChange={(event) => {
onOptionsChange({
...options,
jsonData: { ...options.jsonData, timeout: parseInt(event.currentTarget.value, 10) },
});
}}
tooltip="Zabbix API connection timeout in seconds. Default is 30."
/>
</div>
</div>
<div className="gf-form-group">
<h3 className="page-heading">Direct DB Connection</h3>
<Switch
label="Enable"
labelClass="width-9"
checked={options.jsonData.dbConnectionEnable}
onChange={jsonDataSwitchHandler('dbConnectionEnable', options, onOptionsChange)}
/>
{options.jsonData.dbConnectionEnable && (
<>
<div className="gf-form">
<InlineFormLabel width={9}>Data Source</InlineFormLabel>
<Select
width={32}
options={getDirectDBDSOptions()}
value={selectedDBDatasource}
onChange={directDBDatasourceChanegeHandler(
options,
onOptionsChange,
setSelectedDBDatasource,
setCurrentDSType
)}
/>
</div>
{currentDSType === 'influxdb' && (
<div className="gf-form">
<FormField
labelWidth={9}
inputWidth={16}
label="Retention Policy"
value={options.jsonData.dbConnectionRetentionPolicy || ''}
placeholder="Retention policy name"
onChange={jsonDataChangeHandler('dbConnectionRetentionPolicy', options, onOptionsChange)}
tooltip="Specify retention policy name for fetching long-term stored data (optional).
Leave it blank if only default retention policy used."
/>
</div>
)}
</>
)}
</div>
<div className="gf-form-group">
<h3 className="page-heading">Other</h3>
<Switch
label="Disable acknowledges for read-only users"
labelClass="width-16"
checked={options.jsonData.disableReadOnlyUsersAck}
onChange={jsonDataSwitchHandler('disableReadOnlyUsersAck', options, onOptionsChange)}
/>
<Switch
label="Disable data alignment"
labelClass="width-16"
checked={!!options.jsonData.disableDataAlignment}
onChange={jsonDataSwitchHandler('disableDataAlignment', options, onOptionsChange)}
tooltip="Data alignment feature aligns points based on item update interval.
For instance, if value collected once per minute, then timestamp of the each point will be set to the start of corresponding minute.
This alignment required for proper work of the stacked graphs.
If you don't need stacked graphs and want to get exactly the same timestamps as in Zabbix, then you can disable this feature."
/>
</div>
</>
);
};
const jsonDataChangeHandler =
(
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) =>
(event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>) => {
onChange({
...value,
jsonData: {
...value.jsonData,
[key]: event.currentTarget.value,
},
});
};
const jsonDataSwitchHandler =
(
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) =>
(event: React.SyntheticEvent<HTMLInputElement>) => {
onChange({
...value,
jsonData: {
...value.jsonData,
[key]: (event.target as HTMLInputElement).checked,
},
});
};
const secureJsonDataChangeHandler =
(
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) =>
(event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>) => {
onChange({
...value,
secureJsonData: {
...value.secureJsonData,
[key]: event.currentTarget.value,
},
});
};
const resetSecureJsonField =
(
key: keyof ZabbixDSOptions,
value: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange']
) =>
(event: React.SyntheticEvent<HTMLButtonElement>) => {
onChange({
...value,
secureJsonFields: {
...value.secureJsonFields,
[key]: false,
},
});
};
const directDBDatasourceChanegeHandler =
(
options: DataSourceSettings<ZabbixDSOptions, ZabbixSecureJSONData>,
onChange: Props['onOptionsChange'],
setSelectedDS: React.Dispatch<any>,
setSelectedDSType: React.Dispatch<any>
) =>
(value: SelectableValue<number>) => {
const selectedDs = getDirectDBDatasources().find((dsOption) => dsOption.id === value.value);
setSelectedDS({ label: selectedDs.name, value: selectedDs.id });
setSelectedDSType(selectedDs.type);
onChange({
...options,
jsonData: {
...options.jsonData,
dbConnectionDatasourceId: value.value,
},
});
};
const getDirectDBDatasources = () => {
let dsList = (getDataSourceSrv() as any).getAll();
dsList = dsList.filter((ds) => SUPPORTED_SQL_DS.includes(ds.type));
return dsList;
};
const getDirectDBDSOptions = () => {
const dsList = getDirectDBDatasources();
const dsOpts: Array<SelectableValue<number>> = dsList.map((ds) => ({
label: ds.name,
value: ds.id,
description: ds.type,
}));
return dsOpts;
};

View File

@@ -0,0 +1,73 @@
import { css, cx } from '@emotion/css';
import React, { useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, ClickOutsideWrapper, Icon, Input, Menu, useStyles2, useTheme2 } from '@grafana/ui';
import { FuncDef } from '../../types';
import { getCategories } from '../../metricFunctions';
// import { mapFuncDefsToSelectables } from './helpers';
type Props = {
// funcDefs: MetricFunc;
onFuncAdd: (def: FuncDef) => void;
};
export function AddZabbixFunction({ onFuncAdd }: Props) {
const [showMenu, setShowMenu] = useState(false);
const styles = useStyles2(getStyles);
const theme = useTheme2();
const onFuncAddInternal = (def: FuncDef) => {
onFuncAdd(def);
setShowMenu(false);
};
const onSearch = (e: React.FormEvent<HTMLInputElement>) => {
console.log(e.currentTarget.value);
};
const onClickOutside = () => {
setShowMenu(false);
};
const menuItems = useMemo(() => buildMenuItems(onFuncAddInternal), [onFuncAdd]);
return (
<div>
{!showMenu && (
<Button
icon="plus"
variant="secondary"
className={cx(styles.button)}
aria-label="Add new function"
onClick={() => setShowMenu(!showMenu)}
/>
)}
{showMenu && (
<ClickOutsideWrapper onClick={onClickOutside} useCapture>
<Input onChange={onSearch} suffix={<Icon name="search" />} />
<Menu style={{ position: 'absolute', zIndex: theme.zIndex.dropdown }}>{menuItems}</Menu>
</ClickOutsideWrapper>
)}
</div>
);
}
function buildMenuItems(onClick: (func: FuncDef) => void) {
const categories = getCategories();
const menuItems: JSX.Element[] = [];
for (const categoryName in categories) {
const functions = categories[categoryName];
const subItems = functions.map((f) => <Menu.Item label={f.name} key={f.name} onClick={() => onClick(f)} />);
menuItems.push(<Menu.Item label={categoryName} key={categoryName} childItems={subItems} />);
}
return menuItems;
}
function getStyles(theme: GrafanaTheme2) {
return {
button: css`
margin-right: ${theme.spacing(0.5)};
`,
};
}

View File

@@ -0,0 +1,50 @@
import { css } from '@emotion/css';
import React from 'react';
import { FunctionEditorControlsProps, FunctionEditorControls } from './FunctionEditorControls';
import { useStyles2, Tooltip } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { MetricFunc } from '../../types';
interface FunctionEditorProps extends FunctionEditorControlsProps {
func: MetricFunc;
}
const getStyles = (theme: GrafanaTheme2) => {
return {
icon: css`
margin-right: ${theme.spacing(0.5)};
`,
label: css({
fontWeight: theme.typography.fontWeightMedium,
fontSize: theme.typography.bodySmall.fontSize, // to match .gf-form-label
cursor: 'pointer',
display: 'inline-block',
}),
};
};
export const FunctionEditor: React.FC<FunctionEditorProps> = ({ onMoveLeft, onMoveRight, func, ...props }) => {
const styles = useStyles2(getStyles);
const renderContent = ({ updatePopperPosition }: any) => (
<FunctionEditorControls
{...props}
func={func}
onMoveLeft={() => {
onMoveLeft(func);
updatePopperPosition();
}}
onMoveRight={() => {
onMoveRight(func);
updatePopperPosition();
}}
/>
);
return (
<Tooltip content={renderContent} placement="top" interactive>
<span className={styles.label}>{func.def.name}</span>
</Tooltip>
);
};

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { Icon } from '@grafana/ui';
import { MetricFunc } from '../../types';
const DOCS_FUNC_REF_URL = 'https://alexanderzobnin.github.io/grafana-zabbix/reference/functions/';
export interface FunctionEditorControlsProps {
onMoveLeft: (func: MetricFunc) => void;
onMoveRight: (func: MetricFunc) => void;
onRemove: (func: MetricFunc) => void;
}
const FunctionHelpButton = (props: { description?: string; name: string }) => {
return (
<Icon
className="pointer"
name="question-circle"
onClick={() => {
window.open(`${DOCS_FUNC_REF_URL}#${props.name}`, '_blank');
}}
/>
);
};
export const FunctionEditorControls = (
props: FunctionEditorControlsProps & {
func: MetricFunc;
}
) => {
const { func, onMoveLeft, onMoveRight, onRemove } = props;
return (
<div
style={{
display: 'flex',
width: '60px',
justifyContent: 'space-between',
}}
>
<Icon className="pointer" name="arrow-left" onClick={() => onMoveLeft(func)} />
<FunctionHelpButton name={func.def.name} description={func.def.description} />
<Icon className="pointer" name="times" onClick={() => onRemove(func)} />
<Icon className="pointer" name="arrow-right" onClick={() => onMoveRight(func)} />
</div>
);
};

View File

@@ -0,0 +1,77 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Segment, SegmentInput, useStyles2 } from '@grafana/ui';
export type EditableParam = {
name: string;
value: string;
optional: boolean;
multiple: boolean;
options: Array<SelectableValue<string>>;
};
type FieldEditorProps = {
editableParam: EditableParam;
onChange: (value: string) => void;
onExpandedChange: (expanded: boolean) => void;
autofocus: boolean;
};
/**
* Render a function parameter with a segment dropdown for multiple options or simple input.
*/
export function FunctionParamEditor({ editableParam, onChange, onExpandedChange, autofocus }: FieldEditorProps) {
const styles = useStyles2(getStyles);
if (editableParam.options?.length > 0) {
return (
<Segment
autofocus={autofocus}
value={editableParam.value}
inputPlaceholder={editableParam.name}
className={styles.segment}
options={editableParam.options}
placeholder={' +' + editableParam.name}
onChange={(value) => {
onChange(value.value || '');
}}
onExpandedChange={onExpandedChange}
inputMinWidth={150}
allowCustomValue={true}
allowEmptyValue={true}
></Segment>
);
} else {
return (
<SegmentInput
autofocus={autofocus}
className={styles.input}
value={editableParam.value || ''}
placeholder={' +' + editableParam.name}
inputPlaceholder={editableParam.name}
onChange={(value) => {
onChange(value.toString());
}}
onExpandedChange={onExpandedChange}
// input style
style={{ height: '25px', paddingTop: '2px', marginTop: '2px', paddingLeft: '4px', minWidth: '100px' }}
></SegmentInput>
);
}
}
const getStyles = (theme: GrafanaTheme2) => ({
segment: css({
margin: 0,
padding: 0,
}),
input: css`
margin: 0;
padding: 0;
input {
height: 25px;
},
`,
});

View File

@@ -0,0 +1,90 @@
import { css, cx } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { HorizontalGroup, InlineLabel, useStyles2 } from '@grafana/ui';
import { FunctionEditor } from './FunctionEditor';
import { EditableParam, FunctionParamEditor } from './FunctionParamEditor';
import { mapFuncInstanceToParams } from './helpers';
import { MetricFunc } from '../../types';
export type FunctionEditorProps = {
func: MetricFunc;
onMoveLeft: (func: MetricFunc) => void;
onMoveRight: (func: MetricFunc) => void;
onRemove: (func: MetricFunc) => void;
onParamChange: (func: MetricFunc, index: number, value: string) => void;
};
/**
* Allows editing function params and removing/moving a function (note: editing function name is not supported)
*/
export function ZabbixFunctionEditor({ func, onMoveLeft, onMoveRight, onRemove, onParamChange }: FunctionEditorProps) {
const styles = useStyles2(getStyles);
// keep track of mouse over and isExpanded state to display buttons for adding optional/multiple params
// only when the user mouse over over the function editor OR any param editor is expanded.
const [mouseOver, setIsMouseOver] = useState(false);
const [expanded, setIsExpanded] = useState(false);
let params = mapFuncInstanceToParams(func);
params = params.filter((p: EditableParam, index: number) => {
// func.added is set for newly added functions - see autofocus below
return (index < func.def.params.length && !p.optional) || func.added || p.value || expanded || mouseOver;
});
return (
<div
className={cx(styles.container)}
onMouseOver={() => setIsMouseOver(true)}
onMouseLeave={() => setIsMouseOver(false)}
>
<HorizontalGroup spacing="none">
<FunctionEditor func={func} onMoveLeft={onMoveLeft} onMoveRight={onMoveRight} onRemove={onRemove} />
<InlineLabel className={styles.label}>(</InlineLabel>
{params.map((editableParam: EditableParam, index: number) => {
return (
<React.Fragment key={index}>
<FunctionParamEditor
autofocus={index === 0 && func.added}
editableParam={editableParam}
onChange={(value) => {
if (value !== '' || editableParam.optional) {
// dispatch(actions.updateFunctionParam({ func, index, value }));
onParamChange(func, index, value);
}
setIsExpanded(false);
setIsMouseOver(false);
}}
onExpandedChange={setIsExpanded}
/>
{index !== params.length - 1 ? ',' : ''}
</React.Fragment>
);
})}
<InlineLabel className={styles.label}>)</InlineLabel>
</HorizontalGroup>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
backgroundColor: theme.colors.background.secondary,
borderRadius: theme.shape.borderRadius(),
marginRight: theme.spacing(0.5),
padding: `0 ${theme.spacing(1)}`,
height: `${theme.v1.spacing.formInputHeight}px`,
}),
error: css`
border: 1px solid ${theme.colors.error.main};
`,
label: css({
padding: 0,
margin: 0,
}),
button: css({
padding: theme.spacing(0.5),
}),
});

View File

@@ -0,0 +1,58 @@
import { SelectableValue } from '@grafana/data';
import { MetricFunc } from '../../types';
export type ParamDef = {
name: string;
type: string;
options?: Array<string | number>;
multiple?: boolean;
optional?: boolean;
version?: string;
};
export type EditableParam = {
name: string;
value: string;
optional: boolean;
multiple: boolean;
options: Array<SelectableValue<string>>;
};
function createEditableParam(paramDef: ParamDef, additional: boolean, value?: string | number): EditableParam {
return {
name: paramDef.name,
value: value?.toString() || '',
optional: !!paramDef.optional || additional, // only first param is required when multiple are allowed
multiple: !!paramDef.multiple,
options:
paramDef.options?.map((option: string | number) => ({
value: option.toString(),
label: option.toString(),
})) ?? [],
};
}
/**
* Create a list of params that can be edited in the function editor.
*/
export function mapFuncInstanceToParams(func: MetricFunc): EditableParam[] {
// list of required parameters (from func.def)
const params: EditableParam[] = func.def.params.map((paramDef: ParamDef, index: number) =>
createEditableParam(paramDef, false, func.params[index])
);
// list of additional (multiple or optional) params entered by the user
while (params.length < func.params.length) {
const paramDef = func.def.params[func.def.params.length - 1];
const value = func.params[params.length];
params.push(createEditableParam(paramDef, true, value));
}
// extra "fake" param to allow adding more multiple values at the end
if (params.length && params[params.length - 1].value && params[params.length - 1]?.multiple) {
const paramDef = func.def.params[func.def.params.length - 1];
params.push(createEditableParam(paramDef, true, ''));
}
return params;
}

View File

@@ -0,0 +1,208 @@
import React, { useEffect } from 'react';
import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { InlineField, InlineFieldRow, Select } from '@grafana/ui';
import * as c from '../constants';
import * as migrations from '../migrations';
import { ZabbixDatasource } from '../datasource';
import { ShowProblemTypes, ZabbixDSOptions, ZabbixMetricsQuery, ZabbixQueryOptions } from '../types';
import { MetricsQueryEditor } from './QueryEditor/MetricsQueryEditor';
import { QueryFunctionsEditor } from './QueryEditor/QueryFunctionsEditor';
import { QueryOptionsEditor } from './QueryEditor/QueryOptionsEditor';
import { TextMetricsQueryEditor } from './QueryEditor/TextMetricsQueryEditor';
import { ProblemsQueryEditor } from './QueryEditor/ProblemsQueryEditor';
import { ItemIdQueryEditor } from './QueryEditor/ItemIdQueryEditor';
import { ITServicesQueryEditor } from './QueryEditor/ITServicesQueryEditor';
import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor';
const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [
{
value: c.MODE_METRICS,
label: 'Metrics',
description: 'Query numeric metrics',
},
{
value: c.MODE_TEXT,
label: 'Text',
description: 'Query text data',
},
{
value: c.MODE_ITSERVICE,
label: 'IT Services',
description: 'Query IT Services data',
},
{
value: c.MODE_ITEMID,
label: 'Item Id',
description: 'Query metrics by item ids',
},
{
value: c.MODE_TRIGGERS,
label: 'Triggers',
description: 'Query triggers data',
},
{
value: c.MODE_PROBLEMS,
label: 'Problems',
description: 'Query problems',
},
];
const getDefaultQuery: () => Partial<ZabbixMetricsQuery> = () => ({
queryType: c.MODE_METRICS,
group: { filter: '' },
host: { filter: '' },
application: { filter: '' },
itemTag: { filter: '' },
item: { filter: '' },
functions: [],
triggers: {
count: true,
minSeverity: 3,
acknowledged: 2,
},
trigger: { filter: '' },
tags: { filter: '' },
proxy: { filter: '' },
textFilter: '',
options: {
showDisabledItems: false,
skipEmptyValues: false,
disableDataAlignment: false,
useZabbixValueMapping: false,
},
table: {
skipEmptyValues: false,
},
});
function getSLAQueryDefaults() {
return {
itServiceFilter: '',
slaProperty: 'sla',
slaInterval: 'none',
};
}
function getProblemsQueryDefaults(): Partial<ZabbixMetricsQuery> {
return {
showProblems: ShowProblemTypes.Problems,
options: {
minSeverity: 0,
sortProblems: 'default',
acknowledged: 2,
hostsInMaintenance: false,
hostProxy: false,
limit: c.DEFAULT_ZABBIX_PROBLEMS_LIMIT,
useTimeRange: false,
},
};
}
export interface ZabbixQueryEditorProps
extends QueryEditorProps<ZabbixDatasource, ZabbixMetricsQuery, ZabbixDSOptions> {}
export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQueryEditorProps) => {
query = { ...getDefaultQuery(), ...query };
const { queryType } = query;
if (queryType === c.MODE_PROBLEMS || queryType === c.MODE_TRIGGERS) {
const defaults = getProblemsQueryDefaults();
query = { ...defaults, ...query };
query.options = { ...defaults.options, ...query.options };
}
if (queryType === c.MODE_ITSERVICE) {
query = { ...getSLAQueryDefaults(), ...query };
}
// Migrate query on load
useEffect(() => {
const migratedQuery = migrations.migrate(query);
onChange(migratedQuery);
}, []);
const onPropChange = (prop: string) => {
return (option: SelectableValue) => {
if (option.value !== null) {
onChangeInternal({ ...query, [prop]: option.value });
}
};
};
const onChangeInternal = (query: ZabbixMetricsQuery) => {
onChange(query);
onRunQuery();
};
const onOptionsChange = (options: ZabbixQueryOptions) => {
onChangeInternal({ ...query, options });
};
const renderMetricsEditor = () => {
return (
<>
<MetricsQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />
<QueryFunctionsEditor query={query} onChange={onChangeInternal} />
</>
);
};
const renderItemIdsEditor = () => {
return (
<>
<ItemIdQueryEditor query={query} onChange={onChangeInternal} />
<QueryFunctionsEditor query={query} onChange={onChangeInternal} />
</>
);
};
const renderTextMetricsEditor = () => {
return (
<>
<TextMetricsQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />
{/* <QueryFunctionsEditor query={query} onChange={onChangeInternal} /> */}
</>
);
};
const renderITServicesEditor = () => {
return (
<>
<ITServicesQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />
<QueryFunctionsEditor query={query} onChange={onChangeInternal} />
</>
);
};
const renderProblemsEditor = () => {
return <ProblemsQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />;
};
const renderTriggersEditor = () => {
return <TriggersQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />;
};
return (
<>
<InlineFieldRow>
<InlineField label="Query type" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={queryType}
options={zabbixQueryTypeOptions}
onChange={onPropChange('queryType')}
/>
</InlineField>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</InlineFieldRow>
{queryType === c.MODE_METRICS && renderMetricsEditor()}
{queryType === c.MODE_ITEMID && renderItemIdsEditor()}
{queryType === c.MODE_TEXT && renderTextMetricsEditor()}
{queryType === c.MODE_ITSERVICE && renderITServicesEditor()}
{queryType === c.MODE_PROBLEMS && renderProblemsEditor()}
{queryType === c.MODE_TRIGGERS && renderTriggersEditor()}
<QueryOptionsEditor queryType={queryType} queryOptions={query.options} onChange={onOptionsChange} />
</>
);
};

View File

@@ -0,0 +1,102 @@
import _ from 'lodash';
import React, { useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField, Select } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const slaPropertyList: Array<SelectableValue<string>> = [
{ label: 'Status', value: 'status' },
{ label: 'SLA', value: 'sla' },
{ label: 'OK time', value: 'okTime' },
{ label: 'Problem time', value: 'problemTime' },
{ label: 'Down time', value: 'downtimeTime' },
];
const slaIntervals: Array<SelectableValue<string>> = [
{ label: 'No interval', value: 'none' },
{ label: 'Auto', value: 'auto' },
{ label: '1 hour', value: '1h' },
{ label: '12 hours', value: '12h' },
{ label: '24 hours', value: '1d' },
{ label: '1 week', value: '1w' },
{ label: '1 month', value: '1M' },
];
export interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const ITServicesQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadITServiceOptions = async () => {
const services = await datasource.zabbix.getITService();
const options = services?.map((s) => ({
value: s.name,
label: s.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: itServicesLoading, value: itServicesOptions }, fetchITServices] = useAsyncFn(async () => {
const options = await loadITServiceOptions();
return options;
}, []);
useEffect(() => {
fetchITServices();
}, []);
const onPropChange = (prop: string) => {
return (option: SelectableValue) => {
if (option.value) {
onChange({ ...query, [prop]: option.value });
}
};
};
const onITServiceChange = (value: string) => {
if (value !== null) {
onChange({ ...query, itServiceFilter: value });
}
};
return (
<QueryEditorRow>
<InlineField label="IT Service" labelWidth={12}>
<MetricPicker
width={24}
value={query.itServiceFilter}
options={itServicesOptions}
isLoading={itServicesLoading}
onChange={onITServiceChange}
/>
</InlineField>
<InlineField label="Property" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.slaProperty}
options={slaPropertyList}
onChange={onPropChange('slaProperty')}
/>
</InlineField>
<InlineField label="Interval" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.slaInterval}
options={slaIntervals}
onChange={onPropChange('slaInterval')}
/>
</InlineField>
</QueryEditorRow>
);
};

View File

@@ -0,0 +1,26 @@
import React, { FormEvent } from 'react';
import { InlineField, Input } from '@grafana/ui';
import { ZabbixMetricsQuery } from '../../types';
import { QueryEditorRow } from './QueryEditorRow';
export interface Props {
query: ZabbixMetricsQuery;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const ItemIdQueryEditor = ({ query, onChange }: Props) => {
const onItemIdsChange = (v: FormEvent<HTMLInputElement>) => {
const newValue = v?.currentTarget?.value;
if (newValue !== null) {
onChange({ ...query, itemids: newValue });
}
};
return (
<QueryEditorRow>
<InlineField label="Item Ids" labelWidth={12}>
<Input width={24} defaultValue={query.itemids} onBlur={onItemIdsChange} />
</InlineField>
</QueryEditorRow>
);
};

View File

@@ -0,0 +1,174 @@
import _ from 'lodash';
import React, { useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
export interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const MetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({
value: group.name,
label: group.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: groupsLoading, value: groupsOptions }, fetchGroups] = useAsyncFn(async () => {
const options = await loadGroupOptions();
return options;
}, []);
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter);
return options;
}, [query.group.filter]);
const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter);
return options;
}, [query.group.filter, query.host.filter]);
const loadItemOptions = async (group: string, host: string, app: string, itemTag: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const appFilter = datasource.replaceTemplateVars(app);
const tagFilter = datasource.replaceTemplateVars(itemTag);
const options = {
itemtype: 'num',
showDisabledItems: query.options.showDisabledItems,
};
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, appFilter, tagFilter, options);
let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({
value: item.name,
label: item.name,
}));
itemOptions = _.uniqBy(itemOptions, (o) => o.value);
itemOptions.unshift(...getVariableOptions());
return itemOptions;
};
const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => {
const options = await loadItemOptions(
query.group.filter,
query.host.filter,
query.application.filter,
query.itemTag.filter
);
return options;
}, [query.group.filter, query.host.filter, query.application.filter, query.itemTag.filter]);
// Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
const appFilter = datasource.replaceTemplateVars(query.application?.filter);
const tagFilter = datasource.replaceTemplateVars(query.itemTag?.filter);
useEffect(() => {
fetchGroups();
}, []);
useEffect(() => {
fetchHosts();
}, [groupFilter]);
useEffect(() => {
fetchApps();
}, [groupFilter, hostFilter]);
useEffect(() => {
fetchItems();
}, [groupFilter, hostFilter, appFilter, tagFilter]);
const onFilterChange = (prop: string) => {
return (value: string) => {
if (value !== null) {
onChange({ ...query, [prop]: { filter: value } });
}
};
};
return (
<>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
width={24}
value={query.group.filter}
options={groupsOptions}
isLoading={groupsLoading}
onChange={onFilterChange('group')}
/>
</InlineField>
<InlineField label="Host" labelWidth={12}>
<MetricPicker
width={24}
value={query.host.filter}
options={hostOptions}
isLoading={hostsLoading}
onChange={onFilterChange('host')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Item" labelWidth={12}>
<MetricPicker
width={24}
value={query.item.filter}
options={itemOptions}
isLoading={itemsLoading}
onChange={onFilterChange('item')}
/>
</InlineField>
</QueryEditorRow>
</>
);
};

View File

@@ -0,0 +1,232 @@
import _ from 'lodash';
import React, { useEffect, FormEvent } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField, Input, Select } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const showProblemsOptions: Array<SelectableValue<string>> = [
{ label: 'Problems', value: 'problems' },
{ label: 'Recent problems', value: 'recent' },
{ label: 'History', value: 'history' },
];
const severityOptions: Array<SelectableValue<number>> = [
{ value: 0, label: 'Not classified' },
{ value: 1, label: 'Information' },
{ value: 2, label: 'Warning' },
{ value: 3, label: 'Average' },
{ value: 4, label: 'High' },
{ value: 5, label: 'Disaster' },
];
export interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const ProblemsQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({
value: group.name,
label: group.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: groupsLoading, value: groupsOptions }, fetchGroups] = useAsyncFn(async () => {
const options = await loadGroupOptions();
return options;
}, []);
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter);
return options;
}, [query.group.filter]);
const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter);
return options;
}, [query.group.filter, query.host.filter]);
const loadProxyOptions = async () => {
const proxies = await datasource.zabbix.getProxies();
const options = proxies?.map((proxy) => ({
value: proxy.host,
label: proxy.host,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: proxiesLoading, value: proxiesOptions }, fetchProxies] = useAsyncFn(async () => {
const options = await loadProxyOptions();
return options;
}, []);
// Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
useEffect(() => {
fetchGroups();
}, []);
useEffect(() => {
fetchHosts();
}, [groupFilter]);
useEffect(() => {
fetchApps();
}, [groupFilter, hostFilter]);
useEffect(() => {
fetchProxies();
}, []);
const onTextFilterChange = (prop: string) => {
return (v: FormEvent<HTMLInputElement>) => {
const newValue = v?.currentTarget?.value;
if (newValue !== null) {
onChange({ ...query, [prop]: { filter: newValue } });
}
};
};
const onFilterChange = (prop: string) => {
return (value: string) => {
if (value !== null) {
onChange({ ...query, [prop]: { filter: value } });
}
};
};
const onPropChange = (prop: string) => {
return (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...query, [prop]: option.value });
}
};
};
const onMinSeverityChange = (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...query, options: { ...query.options, minSeverity: option.value } });
}
};
return (
<>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
width={24}
value={query.group?.filter}
options={groupsOptions}
isLoading={groupsLoading}
onChange={onFilterChange('group')}
/>
</InlineField>
<InlineField label="Host" labelWidth={12}>
<MetricPicker
width={24}
value={query.host?.filter}
options={hostOptions}
isLoading={hostsLoading}
onChange={onFilterChange('host')}
/>
</InlineField>
<InlineField label="Proxy" labelWidth={12}>
<MetricPicker
width={24}
value={query.proxy?.filter}
options={proxiesOptions}
isLoading={proxiesLoading}
onChange={onFilterChange('proxy')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application?.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Problem" labelWidth={12}>
<Input
width={24}
defaultValue={query.trigger?.filter}
placeholder="Problem name"
onBlur={onTextFilterChange('trigger')}
/>
</InlineField>
<InlineField label="Tags" labelWidth={12}>
<Input
width={24}
defaultValue={query.tags?.filter}
placeholder="tag1:value1, tag2:value2"
onBlur={onTextFilterChange('tags')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Show" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.showProblems}
options={showProblemsOptions}
onChange={onPropChange('showProblems')}
/>
</InlineField>
<InlineField label="Min severity" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.options?.minSeverity}
options={severityOptions}
onChange={onMinSeverityChange}
/>
</InlineField>
</QueryEditorRow>
</>
);
};

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { InlineFieldRow } from '@grafana/ui';
export const QueryEditorRow = ({ children }: React.PropsWithChildren<{}>) => {
return (
<InlineFieldRow>
{children}
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</InlineFieldRow>
);
};

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { swap } from '../../utils';
import { createFuncInstance } from '../../metricFunctions';
import { FuncDef, MetricFunc, ZabbixMetricsQuery } from '../../types';
import { QueryEditorRow } from './QueryEditorRow';
import { InlineFormLabel } from '@grafana/ui';
import { ZabbixFunctionEditor } from '../FunctionEditor/ZabbixFunctionEditor';
import { AddZabbixFunction } from '../FunctionEditor/AddZabbixFunction';
export interface Props {
query: ZabbixMetricsQuery;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const QueryFunctionsEditor = ({ query, onChange }: Props) => {
const onFuncParamChange = (func: MetricFunc, index: number, value: string) => {
func.params[index] = value;
const funcIndex = query.functions.findIndex((f) => f === func);
const functions = query.functions;
functions[funcIndex] = func;
onChange({ ...query, functions });
};
const onMoveFuncLeft = (func: MetricFunc) => {
const index = query.functions.indexOf(func);
const functions = swap(query.functions, index, index - 1);
onChange({ ...query, functions });
};
const onMoveFuncRight = (func: MetricFunc) => {
const index = query.functions.indexOf(func);
const functions = swap(query.functions, index, index + 1);
onChange({ ...query, functions });
};
const onRemoveFunc = (func: MetricFunc) => {
const functions = query.functions?.filter((f) => f !== func);
onChange({ ...query, functions });
};
const onFuncAdd = (def: FuncDef) => {
const newFunc = createFuncInstance(def);
newFunc.added = true;
let functions = query.functions.concat(newFunc);
functions = moveAliasFuncLast(functions);
// if ((newFunc.params.length && newFunc.added) || newFunc.def.params.length === 0) {
// }
onChange({ ...query, functions });
};
return (
<QueryEditorRow>
<InlineFormLabel width={6}>Functions</InlineFormLabel>
{query.functions?.map((f, i) => {
return (
<ZabbixFunctionEditor
func={f}
key={i}
onParamChange={onFuncParamChange}
onMoveLeft={onMoveFuncLeft}
onMoveRight={onMoveFuncRight}
onRemove={onRemoveFunc}
/>
);
})}
<AddZabbixFunction onFuncAdd={onFuncAdd} />
</QueryEditorRow>
);
};
function moveAliasFuncLast(functions: MetricFunc[]) {
const aliasFuncIndex = functions.findIndex((func) => func.def.category === 'Alias');
console.log(aliasFuncIndex);
if (aliasFuncIndex >= 0) {
const aliasFunc = functions[aliasFuncIndex];
functions.splice(aliasFuncIndex, 1);
functions.push(aliasFunc);
}
return functions;
}

View File

@@ -0,0 +1,232 @@
import { css } from '@emotion/css';
import React, { useState, FormEvent } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import {
HorizontalGroup,
Icon,
InlineField,
InlineFieldRow,
InlineSwitch,
Input,
Select,
useStyles2,
} from '@grafana/ui';
import * as c from '../../constants';
import { ZabbixQueryOptions } from '../../types';
const ackOptions: Array<SelectableValue<number>> = [
{ label: 'all triggers', value: 2 },
{ label: 'unacknowledged', value: 0 },
{ label: 'acknowledged', value: 1 },
];
const sortOptions: Array<SelectableValue<string>> = [
{ label: 'Default', value: 'default' },
{ label: 'Last change', value: 'lastchange' },
{ label: 'Severity', value: 'severity' },
];
interface Props {
queryType: string;
queryOptions: ZabbixQueryOptions;
onChange: (options: ZabbixQueryOptions) => void;
}
export const QueryOptionsEditor = ({ queryType, queryOptions, onChange }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const styles = useStyles2(getStyles);
const onLimitChange = (v: FormEvent<HTMLInputElement>) => {
const newValue = Number(v?.currentTarget?.value);
if (newValue !== null) {
onChange({ ...queryOptions, limit: newValue });
}
};
const onPropChange = (prop: string) => {
return (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...queryOptions, [prop]: option.value });
}
};
};
const renderClosed = () => {
return (
<>
<HorizontalGroup>
{!isOpen && <Icon name="angle-right" />}
{isOpen && <Icon name="angle-down" />}
<span className={styles.label}>Options</span>
<div className={styles.options}>{renderOptions()}</div>
</HorizontalGroup>
</>
);
};
const renderOptions = () => {
const elements: JSX.Element[] = [];
for (const key in queryOptions) {
if (queryOptions.hasOwnProperty(key)) {
const value = queryOptions[key];
if (value === true && value !== '' && value !== null && value !== undefined) {
elements.push(<span className={styles.optionContainer} key={key}>{`${key} = ${value}`}</span>);
}
}
}
return elements;
};
const renderEditor = () => {
return (
<div className={styles.editorContainer}>
{queryType === c.MODE_METRICS && renderMetricOptions()}
{queryType === c.MODE_ITEMID && renderMetricOptions()}
{queryType === c.MODE_ITSERVICE && renderMetricOptions()}
{queryType === c.MODE_TEXT && renderTextMetricsOptions()}
{queryType === c.MODE_PROBLEMS && renderProblemsOptions()}
{queryType === c.MODE_TRIGGERS && renderTriggersOptions()}
</div>
);
};
const renderMetricOptions = () => {
return (
<>
<InlineField label="Show disabled items" labelWidth={24}>
<InlineSwitch
value={queryOptions.showDisabledItems}
onChange={() => onChange({ ...queryOptions, showDisabledItems: !queryOptions.showDisabledItems })}
/>
</InlineField>
<InlineField label="Use Zabbix value mapping" labelWidth={24}>
<InlineSwitch
value={queryOptions.useZabbixValueMapping}
onChange={() => onChange({ ...queryOptions, useZabbixValueMapping: !queryOptions.useZabbixValueMapping })}
/>
</InlineField>
<InlineField label="Disable data alignment" labelWidth={24}>
<InlineSwitch
value={queryOptions.disableDataAlignment}
onChange={() => onChange({ ...queryOptions, disableDataAlignment: !queryOptions.disableDataAlignment })}
/>
</InlineField>
</>
);
};
const renderTextMetricsOptions = () => {
return (
<>
<InlineField label="Show disabled items" labelWidth={24}>
<InlineSwitch
value={queryOptions.showDisabledItems}
onChange={() => onChange({ ...queryOptions, showDisabledItems: !queryOptions.showDisabledItems })}
/>
</InlineField>
</>
);
};
const renderProblemsOptions = () => {
return (
<>
<InlineField label="Acknowledged" labelWidth={24}>
<Select
isSearchable={false}
width={24}
value={queryOptions.acknowledged}
options={ackOptions}
onChange={onPropChange('acknowledged')}
/>
</InlineField>
<InlineField label="Sort by" labelWidth={24}>
<Select
isSearchable={false}
width={24}
value={queryOptions.sortProblems}
options={sortOptions}
onChange={onPropChange('sortProblems')}
/>
</InlineField>
<InlineField label="Use time range" labelWidth={24}>
<InlineSwitch
value={queryOptions.useTimeRange}
onChange={() => onChange({ ...queryOptions, useTimeRange: !queryOptions.useTimeRange })}
/>
</InlineField>
<InlineField label="Hosts in maintenance" labelWidth={24}>
<InlineSwitch
value={queryOptions.hostsInMaintenance}
onChange={() => onChange({ ...queryOptions, hostsInMaintenance: !queryOptions.hostsInMaintenance })}
/>
</InlineField>
<InlineField label="Host proxy" labelWidth={24}>
<InlineSwitch
value={queryOptions.hostProxy}
onChange={() => onChange({ ...queryOptions, hostProxy: !queryOptions.hostProxy })}
/>
</InlineField>
<InlineField label="Limit" labelWidth={24}>
<Input width={12} type="number" defaultValue={queryOptions.limit} onBlur={onLimitChange} />
</InlineField>
</>
);
};
const renderTriggersOptions = () => {
return (
<>
<InlineField label="Acknowledged" labelWidth={24}>
<Select
isSearchable={false}
width={24}
value={queryOptions.acknowledged}
options={ackOptions}
onChange={onPropChange('acknowledged')}
/>
</InlineField>
</>
);
};
return (
<>
<InlineFieldRow>
<div className={styles.container} onClick={() => setIsOpen(!isOpen)}>
{renderClosed()}
</div>
</InlineFieldRow>
<InlineFieldRow>{isOpen && renderEditor()}</InlineFieldRow>
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
backgroundColor: theme.colors.background.secondary,
borderRadius: theme.shape.borderRadius(),
marginRight: theme.spacing(0.5),
marginBottom: theme.spacing(0.5),
padding: `0 ${theme.spacing(1)}`,
height: `${theme.v1.spacing.formInputHeight}px`,
width: `100%`,
}),
label: css({
color: theme.colors.info.text,
fontWeight: theme.typography.fontWeightMedium,
cursor: 'pointer',
}),
options: css({
color: theme.colors.text.disabled,
fontSize: theme.typography.bodySmall.fontSize,
}),
optionContainer: css`
margin-right: ${theme.spacing(2)};
`,
editorContainer: css`
display: flex;
flex-direction: column;
margin-left: ${theme.spacing(4)};
`,
});

View File

@@ -0,0 +1,192 @@
import _ from 'lodash';
import React, { useEffect, FormEvent } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField, InlineSwitch, Input } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
export interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const TextMetricsQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({
value: group.name,
label: group.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: groupsLoading, value: groupsOptions }, fetchGroups] = useAsyncFn(async () => {
const options = await loadGroupOptions();
return options;
}, []);
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter);
return options;
}, [query.group.filter]);
const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter);
return options;
}, [query.group.filter, query.host.filter]);
const loadItemOptions = async (group: string, host: string, app: string, itemTag: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const appFilter = datasource.replaceTemplateVars(app);
const tagFilter = datasource.replaceTemplateVars(itemTag);
const options = {
itemtype: 'text',
showDisabledItems: query.options.showDisabledItems,
};
const items = await datasource.zabbix.getAllItems(groupFilter, hostFilter, appFilter, tagFilter, options);
let itemOptions: Array<SelectableValue<string>> = items?.map((item) => ({
value: item.name,
label: item.name,
}));
itemOptions = _.uniqBy(itemOptions, (o) => o.value);
itemOptions.unshift(...getVariableOptions());
return itemOptions;
};
const [{ loading: itemsLoading, value: itemOptions }, fetchItems] = useAsyncFn(async () => {
const options = await loadItemOptions(
query.group.filter,
query.host.filter,
query.application.filter,
query.itemTag.filter
);
return options;
}, [query.group.filter, query.host.filter, query.application.filter, query.itemTag.filter]);
// Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
const appFilter = datasource.replaceTemplateVars(query.application?.filter);
const tagFilter = datasource.replaceTemplateVars(query.itemTag?.filter);
useEffect(() => {
fetchGroups();
}, []);
useEffect(() => {
fetchHosts();
}, [groupFilter]);
useEffect(() => {
fetchApps();
}, [groupFilter, hostFilter]);
useEffect(() => {
fetchItems();
}, [groupFilter, hostFilter, appFilter, tagFilter]);
const onTextFilterChange = (v: FormEvent<HTMLInputElement>) => {
const newValue = v?.currentTarget?.value;
if (newValue !== null) {
onChange({ ...query, textFilter: newValue });
}
};
const onFilterChange = (prop: string) => {
return (value: string) => {
if (value !== null) {
onChange({ ...query, [prop]: { filter: value } });
}
};
};
return (
<>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
width={24}
value={query.group.filter}
options={groupsOptions}
isLoading={groupsLoading}
onChange={onFilterChange('group')}
/>
</InlineField>
<InlineField label="Host" labelWidth={12}>
<MetricPicker
width={24}
value={query.host.filter}
options={hostOptions}
isLoading={hostsLoading}
onChange={onFilterChange('host')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Item" labelWidth={12}>
<MetricPicker
width={24}
value={query.item.filter}
options={itemOptions}
isLoading={itemsLoading}
onChange={onFilterChange('item')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Text filter" labelWidth={12}>
<Input width={24} defaultValue={query.textFilter} onBlur={onTextFilterChange} />
</InlineField>
<InlineField label="Use capture groups" labelWidth={16}>
<InlineSwitch
value={query.useCaptureGroups}
onChange={() => onChange({ ...query, useCaptureGroups: !query.useCaptureGroups })}
/>
</InlineField>
</QueryEditorRow>
</>
);
};

View File

@@ -0,0 +1,160 @@
import _ from 'lodash';
import React, { useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField, InlineSwitch, Select } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const severityOptions: Array<SelectableValue<number>> = [
{ value: 0, label: 'Not classified' },
{ value: 1, label: 'Information' },
{ value: 2, label: 'Warning' },
{ value: 3, label: 'Average' },
{ value: 4, label: 'High' },
{ value: 5, label: 'Disaster' },
];
export interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const TriggersQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadGroupOptions = async () => {
const groups = await datasource.zabbix.getAllGroups();
const options = groups?.map((group) => ({
value: group.name,
label: group.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: groupsLoading, value: groupsOptions }, fetchGroups] = useAsyncFn(async () => {
const options = await loadGroupOptions();
return options;
}, []);
const loadHostOptions = async (group: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hosts = await datasource.zabbix.getAllHosts(groupFilter);
let options: Array<SelectableValue<string>> = hosts?.map((host) => ({
value: host.name,
label: host.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift({ value: '/.*/' });
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => {
const options = await loadHostOptions(query.group.filter);
return options;
}, [query.group.filter]);
const loadAppOptions = async (group: string, host: string) => {
const groupFilter = datasource.replaceTemplateVars(group);
const hostFilter = datasource.replaceTemplateVars(host);
const apps = await datasource.zabbix.getAllApps(groupFilter, hostFilter);
let options: Array<SelectableValue<string>> = apps?.map((app) => ({
value: app.name,
label: app.name,
}));
options = _.uniqBy(options, (o) => o.value);
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: appsLoading, value: appOptions }, fetchApps] = useAsyncFn(async () => {
const options = await loadAppOptions(query.group.filter, query.host.filter);
return options;
}, [query.group.filter, query.host.filter]);
// Update suggestions on every metric change
const groupFilter = datasource.replaceTemplateVars(query.group?.filter);
const hostFilter = datasource.replaceTemplateVars(query.host?.filter);
useEffect(() => {
fetchGroups();
}, []);
useEffect(() => {
fetchHosts();
}, [groupFilter]);
useEffect(() => {
fetchApps();
}, [groupFilter, hostFilter]);
const onFilterChange = (prop: string) => {
return (value: string) => {
if (value !== null) {
onChange({ ...query, [prop]: { filter: value } });
}
};
};
const onMinSeverityChange = (option: SelectableValue) => {
if (option.value !== null) {
onChange({ ...query, options: { ...query.options, minSeverity: option.value } });
}
};
return (
<>
<QueryEditorRow>
<InlineField label="Group" labelWidth={12}>
<MetricPicker
width={24}
value={query.group?.filter}
options={groupsOptions}
isLoading={groupsLoading}
onChange={onFilterChange('group')}
/>
</InlineField>
<InlineField label="Host" labelWidth={12}>
<MetricPicker
width={24}
value={query.host?.filter}
options={hostOptions}
isLoading={hostsLoading}
onChange={onFilterChange('host')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Application" labelWidth={12}>
<MetricPicker
width={24}
value={query.application?.filter}
options={appOptions}
isLoading={appsLoading}
onChange={onFilterChange('application')}
/>
</InlineField>
<InlineField label="Min severity" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.triggers?.minSeverity}
options={severityOptions}
onChange={onMinSeverityChange}
/>
</InlineField>
<InlineField label="Count" labelWidth={12}>
<InlineSwitch
value={query.triggers?.count}
onChange={() => onChange({ ...query, triggers: { ...query.triggers, count: !query.triggers?.count } })}
/>
</InlineField>
</QueryEditorRow>
</>
);
};

View File

@@ -0,0 +1,13 @@
import { getTemplateSrv } from '@grafana/runtime';
export const getVariableOptions = () => {
const variables = getTemplateSrv()
.getVariables()
.filter((v) => {
return v.type !== 'datasource' && v.type !== 'interval';
});
return variables?.map((v) => ({
value: `$${v.name}`,
label: `$${v.name}`,
}));
};

View File

@@ -0,0 +1,176 @@
import React, { PureComponent } from 'react';
import { parseLegacyVariableQuery } from '../utils';
import { SelectableValue } from '@grafana/data';
import { VariableQuery, VariableQueryData, VariableQueryProps, VariableQueryTypes } from '../types';
import { ZabbixInput } from './ZabbixInput';
import { InlineFormLabel, Input, Select } from '@grafana/ui';
export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [
{ value: VariableQueryTypes.Group, label: 'Group' },
{ value: VariableQueryTypes.Host, label: 'Host' },
{ value: VariableQueryTypes.Application, label: 'Application' },
{ value: VariableQueryTypes.ItemTag, label: 'Item tag' },
{ value: VariableQueryTypes.Item, label: 'Item' },
{ value: VariableQueryTypes.ItemValues, label: 'Item values' },
];
defaults: VariableQueryData = {
selectedQueryType: { value: VariableQueryTypes.Group, label: 'Group' },
queryType: VariableQueryTypes.Group,
group: '/.*/',
host: '',
application: '',
itemTag: '',
item: '',
};
constructor(props: VariableQueryProps) {
super(props);
if (this.props.query && typeof this.props.query === 'string') {
// Backward compatibility
const query = parseLegacyVariableQuery(this.props.query);
const selectedQueryType = this.getSelectedQueryType(query.queryType);
this.state = {
selectedQueryType,
legacyQuery: this.props.query,
...query,
};
} else if (this.props.query) {
const query = this.props.query as VariableQuery;
const selectedQueryType = this.getSelectedQueryType(query.queryType);
this.state = {
...this.defaults,
...query,
selectedQueryType,
};
} else {
this.state = this.defaults;
}
}
getSelectedQueryType(queryType: VariableQueryTypes) {
return this.queryTypes.find((q) => q.value === queryType);
}
handleQueryUpdate = (evt: React.ChangeEvent<HTMLInputElement>, prop: string) => {
const value = evt.currentTarget.value;
this.setState((prevState: VariableQueryData) => {
const newQuery = {
...prevState,
};
newQuery[prop] = value;
return {
...newQuery,
};
});
};
handleQueryChange = () => {
const { queryType, group, host, application, itemTag, item } = this.state;
const queryModel = { queryType, group, host, application, itemTag, item };
this.props.onChange(queryModel, `Zabbix - ${queryType}`);
};
handleQueryTypeChange = (selectedItem: SelectableValue<VariableQueryTypes>) => {
this.setState({
...this.state,
selectedQueryType: selectedItem,
queryType: selectedItem.value,
});
const { group, host, application, itemTag, item } = this.state;
const queryType = selectedItem.value;
const queryModel = { queryType, group, host, application, itemTag, item };
this.props.onChange(queryModel, `Zabbix - ${queryType}`);
};
render() {
const { selectedQueryType, legacyQuery, group, host, application, itemTag, item } = this.state;
const { datasource } = this.props;
const supportsItemTags = datasource?.zabbix?.isZabbix54OrHigher() || false;
return (
<>
<div className="gf-form max-width-21">
<InlineFormLabel width={10}>Query Type</InlineFormLabel>
<Select
width={11}
value={selectedQueryType}
options={this.queryTypes}
onChange={this.handleQueryTypeChange}
/>
</div>
<div className="gf-form-inline">
<div className="gf-form max-width-30">
<InlineFormLabel width={10}>Group</InlineFormLabel>
<ZabbixInput
value={group}
onChange={(evt) => this.handleQueryUpdate(evt, 'group')}
onBlur={this.handleQueryChange}
/>
</div>
{selectedQueryType.value !== VariableQueryTypes.Group && (
<div className="gf-form max-width-30">
<InlineFormLabel width={10}>Host</InlineFormLabel>
<ZabbixInput
value={host}
onChange={(evt) => this.handleQueryUpdate(evt, 'host')}
onBlur={this.handleQueryChange}
/>
</div>
)}
</div>
{(selectedQueryType.value === VariableQueryTypes.Application ||
selectedQueryType.value === VariableQueryTypes.ItemTag ||
selectedQueryType.value === VariableQueryTypes.Item ||
selectedQueryType.value === VariableQueryTypes.ItemValues) && (
<div className="gf-form-inline">
{supportsItemTags && (
<div className="gf-form max-width-30">
<InlineFormLabel width={10}>Item tag</InlineFormLabel>
<ZabbixInput
value={itemTag}
onChange={(evt) => this.handleQueryUpdate(evt, 'itemTag')}
onBlur={this.handleQueryChange}
/>
</div>
)}
{!supportsItemTags && (
<div className="gf-form max-width-30">
<InlineFormLabel width={10}>Application</InlineFormLabel>
<ZabbixInput
value={application}
onChange={(evt) => this.handleQueryUpdate(evt, 'application')}
onBlur={this.handleQueryChange}
/>
</div>
)}
{(selectedQueryType.value === VariableQueryTypes.Item ||
selectedQueryType.value === VariableQueryTypes.ItemValues) && (
<div className="gf-form max-width-30">
<InlineFormLabel width={10}>Item</InlineFormLabel>
<ZabbixInput
value={item}
onChange={(evt) => this.handleQueryUpdate(evt, 'item')}
onBlur={this.handleQueryChange}
/>
</div>
)}
</div>
)}
{legacyQuery && (
<div className="gf-form">
<InlineFormLabel width={10} tooltip="Original query string, read-only">
Legacy Query
</InlineFormLabel>
<Input value={legacyQuery} readOnly={true} />
</div>
)}
</>
);
}
}

View File

@@ -0,0 +1,60 @@
import React, { FC } from 'react';
import { css } from '@emotion/css';
import { EventsWithValidation, ValidationEvents, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { isRegex, variableRegex } from '../utils';
import * as grafanaUi from '@grafana/ui';
const Input = (grafanaUi as any).LegacyForms?.Input || (grafanaUi as any).Input;
const variablePattern = RegExp(`^${variableRegex.source}`);
const getStyles = (theme: GrafanaTheme2) => ({
inputRegex: css`
color: ${theme.colors.warning.main};
`,
inputVariable: css`
color: ${theme.colors.action.focus};
`,
});
const zabbixInputValidationEvents: ValidationEvents = {
[EventsWithValidation.onBlur]: [
{
rule: (value) => {
if (!value) {
return true;
}
if (value.length > 1 && value[0] === '/') {
if (value[value.length - 1] !== '/') {
return false;
}
}
return true;
},
errorMessage: 'Not a valid regex',
},
{
rule: (value) => {
if (value === '*') {
return false;
}
return true;
},
errorMessage: 'Wildcards not supported. Use /.*/ instead',
},
],
};
export const ZabbixInput: FC<any> = ({ value, ref, validationEvents, ...restProps }) => {
const styles = useStyles2(getStyles);
let inputClass = styles.inputRegex;
if (variablePattern.test(value as string)) {
inputClass = styles.inputVariable;
} else if (isRegex(value)) {
inputClass = styles.inputRegex;
}
return <Input className={inputClass} value={value} validationEvents={zabbixInputValidationEvents} {...restProps} />;
};