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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user