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:
195
src/datasource/components/AnnotationQueryEditor.tsx
Normal file
195
src/datasource/components/AnnotationQueryEditor.tsx
Normal 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>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
345
src/datasource/components/ConfigEditor.tsx
Normal file
345
src/datasource/components/ConfigEditor.tsx
Normal 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;
|
||||
};
|
||||
@@ -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)};
|
||||
`,
|
||||
};
|
||||
}
|
||||
50
src/datasource/components/FunctionEditor/FunctionEditor.tsx
Normal file
50
src/datasource/components/FunctionEditor/FunctionEditor.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
`,
|
||||
});
|
||||
@@ -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),
|
||||
}),
|
||||
});
|
||||
58
src/datasource/components/FunctionEditor/helpers.ts
Normal file
58
src/datasource/components/FunctionEditor/helpers.ts
Normal 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;
|
||||
}
|
||||
208
src/datasource/components/QueryEditor.tsx
Normal file
208
src/datasource/components/QueryEditor.tsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
102
src/datasource/components/QueryEditor/ITServicesQueryEditor.tsx
Normal file
102
src/datasource/components/QueryEditor/ITServicesQueryEditor.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
26
src/datasource/components/QueryEditor/ItemIdQueryEditor.tsx
Normal file
26
src/datasource/components/QueryEditor/ItemIdQueryEditor.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
174
src/datasource/components/QueryEditor/MetricsQueryEditor.tsx
Normal file
174
src/datasource/components/QueryEditor/MetricsQueryEditor.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
232
src/datasource/components/QueryEditor/ProblemsQueryEditor.tsx
Normal file
232
src/datasource/components/QueryEditor/ProblemsQueryEditor.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
13
src/datasource/components/QueryEditor/QueryEditorRow.tsx
Normal file
13
src/datasource/components/QueryEditor/QueryEditorRow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
232
src/datasource/components/QueryEditor/QueryOptionsEditor.tsx
Normal file
232
src/datasource/components/QueryEditor/QueryOptionsEditor.tsx
Normal 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)};
|
||||
`,
|
||||
});
|
||||
192
src/datasource/components/QueryEditor/TextMetricsQueryEditor.tsx
Normal file
192
src/datasource/components/QueryEditor/TextMetricsQueryEditor.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
160
src/datasource/components/QueryEditor/TriggersQueryEditor.tsx
Normal file
160
src/datasource/components/QueryEditor/TriggersQueryEditor.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
13
src/datasource/components/QueryEditor/utils.ts
Normal file
13
src/datasource/components/QueryEditor/utils.ts
Normal 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}`,
|
||||
}));
|
||||
};
|
||||
176
src/datasource/components/VariableQueryEditor.tsx
Normal file
176
src/datasource/components/VariableQueryEditor.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
60
src/datasource/components/ZabbixInput.tsx
Normal file
60
src/datasource/components/ZabbixInput.tsx
Normal 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} />;
|
||||
};
|
||||
Reference in New Issue
Block a user