Skip to content

Commit

Permalink
merge changes
Browse files Browse the repository at this point in the history
  • Loading branch information
mershad-manesh committed Jan 4, 2024
2 parents 72fd6d6 + 71094d1 commit b6504b2
Show file tree
Hide file tree
Showing 14 changed files with 151 additions and 82 deletions.
2 changes: 1 addition & 1 deletion docs/antora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: '9'
title: Plugin for Grafana
asciidoc:
attributes:
full-display-version: '9.0.11'
full-display-version: '9.0.12-SNAPSHOT'
grafana-version-required: '9.x'
grafana-version-tested: '9.0'
node-js-build-version: '16.x'
Expand Down
2 changes: 1 addition & 1 deletion grafana.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
9.4.7
9.5.14
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opennms-grafana-plugin",
"version": "9.0.11",
"version": "9.0.12-SNAPSHOT",
"description": "An OpenNMS Integration for Grafana",
"repository": {
"type": "git",
Expand Down Expand Up @@ -64,7 +64,7 @@
"which": "^4.0.0"
},
"engines": {
"node": ">=14"
"node": ">=16"
},
"spec": {
"specTemplate": "src/rpm/spec.mustache",
Expand Down
2 changes: 1 addition & 1 deletion site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ asciidoc:
attributes:
experimental: true
stem: latexmath
full-display-version: '9.0.11'
full-display-version: '9.0.12-SNAPSHOT'
grafana-version-required: '9.x'
grafana-version-tested: '9.0'
node-js-build-version: '16.x'
Expand Down
67 changes: 52 additions & 15 deletions src/datasources/entity-ds/queries/queryBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { TypedVariableModel, VariableOption } from '@grafana/data'
import { TemplateSrv } from '@grafana/runtime'
import { API } from 'opennms'
import { EntityQuery, EntityQueryRequest, FilterEditorData } from './../types'
import { ALL_SELECTION_VALUE, EntityTypes } from '../../../constants/constants'
import { isInteger } from '../../../lib/utils'
import { isInteger, trimChar } from '../../../lib/utils'
import { getAttributeMapping } from './attributeMappings'
import { getFilterId } from '../EntityHelper'

const isAllVariable = (templateVar, templateSrv) => {
return templateVar.current.value &&
templateSrv.isAllValue(templateVar.current.value)
return templateVar?.current.value &&
templateSrv.isAllValue(templateVar?.current.value)
}

const isMultiVariable = (templateVar) => {
Expand All @@ -23,11 +24,12 @@ const isEmptyNodeRestriction = (clause: API.Clause) => {
// annoyingly, depending on how you interact with the UI, if one value is selected it will
// *either* be an array with 1 entry, or just the raw value >:|
// so we normalize it back to just the raw value here if necessary
const normalizeSingleArrayValue = (value: any) => {
const normalizeSingleArrayValue = (value: any): string | string[] => {
if (Array.isArray(value) && value.length === 1) {
return value[0]
}

// could be a string, or multi-valued string[]
return value
}

Expand Down Expand Up @@ -91,11 +93,16 @@ const subtituteNodeRestriction = (clause: API.Clause) => {
}
}

// Perform variable substitution for clauses.
// This will be called recursively if clause.restriction is actually a NestedRestriction.
// Note: templateSrv is derived from '@grafana/runtime' TemplateSrv but is actually a class having
// quite a few more methods, etc., so we use 'any' instead. See:
// https://github.com/grafana/grafana/blob/main/public/app/features/templating/template_srv.ts
/**
* Perform variable substitution for clauses.
*
* This will be called recursively if clause.restriction is actually a NestedRestriction OR
* if the clause has a multi-valued template variable in which case we call recursively to create clauses from the resulting values.
*
* Note: templateSrv is derived from '@grafana/runtime' TemplateSrv but is actually a class having
* quite a few more methods, etc., so we use 'any' instead. See:
* https://github.com/grafana/grafana/blob/main/public/app/features/templating/template_srv.ts
*/
const substitute = (clauses: API.Clause[], request: EntityQueryRequest<EntityQuery>, templateSrv: any) => {
const remove: API.Clause[] = []
const clausesWithRestrictions = clauses.filter(c => c.restriction)
Expand All @@ -105,11 +112,15 @@ const substitute = (clauses: API.Clause[], request: EntityQueryRequest<EntityQue
// this is actually a NestedRestriction, recurse through subclauses
substitute(clause.restriction.clauses, request, templateSrv)
} else if (clause.restriction.value) {
// clause.restriction.value may be:
// - a template variable (in $var or ${var} or ${var:text} format)
// - an actual value (e.g. a node id or label), for example for multi-valued template variables and we
// broke it up into nested restrictions below
const restriction = clause.restriction as API.Restriction
const variableName = templateSrv.getVariableName(restriction.value)
const templateVariable = getTemplateVariable(templateSrv, variableName)
const variableName: string = templateSrv.getVariableName(restriction.value) || ''
const templateVariable: TypedVariableModel | undefined = getTemplateVariable(templateSrv, variableName)

// Process multi-selects
// Process template variables set to "all"
if (isMultiVariable(templateVariable) && isAllVariable(templateVariable, templateSrv)) {
// if we're querying "all" we just dump the clause altogether
remove.push(clause)
Expand All @@ -118,8 +129,11 @@ const substitute = (clauses: API.Clause[], request: EntityQueryRequest<EntityQue

let skipSimpleSubstitution = false

// Process multiple selection template variables
if (isMultiVariable(templateVariable)) {
const normalizedValue = normalizeSingleArrayValue(templateVariable.current.value)
const currentValues: string | string[] = getCurrentValuesFromMultiValuedTemplateVariables(templateVariable, variableName, restriction.value)

const normalizedValue = normalizeSingleArrayValue(currentValues)

// now if it's *still* an array, we chop it up into nested restrictions
if (Array.isArray(normalizedValue)) {
Expand Down Expand Up @@ -148,10 +162,33 @@ const substitute = (clauses: API.Clause[], request: EntityQueryRequest<EntityQue
removeEmptyClauses(clauses, remove)
}

/**
* Get the current values from a resolved template variable, accounting for a ':text' specifier on the template variable declaration.
* @param templateVariable a Grafana template variable object, TypedVariableModel. Should contain a VariableOption 'current' property
* @param variableName The template variable name, without any decoration
* @param restrictionValue The value from the restriction clause, should be a template variable reference like '$node', '${node}' or '${node:text}'
* @returns The current values of the multi-valued template variable
*/
const getCurrentValuesFromMultiValuedTemplateVariables = (templateVariable: TypedVariableModel | undefined, variableName: string, restrictionValue: string) => {
// OPG-466. If restrictionValue has a format attribute, e.g. '${node:text}', use the 'text'
// part of the templateVariable.current values, instead of the 'value' part
const originalValueTrimmed = trimChar(trimChar(restrictionValue, '$'), '{', '}')
const hasTextFormat = originalValueTrimmed === `${variableName}:text`
const currentObj = (templateVariable as any)?.current ? (templateVariable as any)?.current as VariableOption : undefined

let currentValues: string | string[] = []

if (currentObj) {
currentValues = hasTextFormat ? currentObj.text : currentObj.value
}

return currentValues
}

export const getTemplateVariable = (templateSrv: any, name: string) => {
const variables = templateSrv.getVariables()
const variables: TypedVariableModel[] = templateSrv.getVariables()

if (variables && variables.length) {
if (name && variables && variables.length) {
return variables.find(v => v.name === name)
}

Expand Down
38 changes: 21 additions & 17 deletions src/datasources/perf-ds/PerformanceDataSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,16 @@ export class PerformanceDataSource extends DataSourceApi<PerformanceQuery> {
* @returns
*/
async doResourcesForNodeRequest(nodeId: string) {
const response = await this.simpleRequest.doOpenNMSRequest({
url: '/rest/resources/fornode/' + encodeURIComponent(nodeId),
method: 'GET'
})
const response = await this.simpleRequest.doOpenNMSRequest({
url: '/rest/resources/fornode/' + encodeURIComponent(nodeId),
method: 'GET'
})

return response && response.data ? response.data as OnmsResourceDto : null
return response && response.data ? response.data as OnmsResourceDto : null
}

async metricFindLocations() {
return await this.simpleRequest.getLocations();
return await this.simpleRequest.getLocations()
}

async metricFindNodeFilterQuery(query: string, labelFormat: string, valueFormat: string) {
Expand All @@ -278,22 +278,26 @@ export class PerformanceDataSource extends DataSourceApi<PerformanceQuery> {
results.push({ text, value, expandable: true })
})

// ensure template variable value displays (e.g. dropdowns) are in alphabetical order based on text label
results.sort((a, b) => a.text.localeCompare(b.text))

return results
}

async metricFindNodeResourceQuery(query, ...options) {
let textProperty = "id", resourceType = '*', regex = null;
if (options.length > 0) {
textProperty = options[0];
}
if (options.length > 1) {
resourceType = options[1];
}
if (options.length > 2) {
regex = options[2];
}
let textProperty = 'id', resourceType = '*', regex = null

if (options.length > 0) {
textProperty = options[0]
}
if (options.length > 1) {
resourceType = options[1]
}
if (options.length > 2) {
regex = options[2]
}

return await this.getNodeResources(query, textProperty, resourceType, regex)
return await this.getNodeResources(query, textProperty, resourceType, regex)
}

async getNodeResources(nodes: string, textProperty: string, resourceType: string, regex?: string | null) {
Expand Down
4 changes: 2 additions & 2 deletions src/datasources/perf-ds/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { DataQuery, DataQueryRequest, DataSourceJsonData, QueryEditorProps, QueryResultMeta, SelectableValue } from "@grafana/data";
import { PerformanceDataSource } from "./PerformanceDataSource";
import { DataQuery, DataQueryRequest, DataSourceJsonData, QueryEditorProps, QueryResultMeta, SelectableValue } from '@grafana/data'
import { PerformanceDataSource } from './PerformanceDataSource'

/**
* These are options configured for each DataSource instance
Expand Down
16 changes: 8 additions & 8 deletions src/panels/alarm-table/AlarmTableAdditional.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ interface AlarmTableAdditionalProps {
context: any;
}

/** Build the default AlarmTableAdditionalState, enabling autoRefresh by default. */
/** Build the default AlarmTableAdditionalState, enabling displayActionNotice by default. */
const buildDefaultState = (state: AlarmTableAdditionalState | undefined) => {
if (state) {
return {
...state,
autoRefresh: state.autoRefresh === undefined ? true : state.autoRefresh
displayActionNotice: state.displayActionNotice === undefined ? true : state.displayActionNotice
}
}

return { autoRefresh: true, useGrafanaUser: false }
return { displayActionNotice: true, useGrafanaUser: false }
}

export const AlarmTableAdditional: React.FC<AlarmTableAdditionalProps> = ({ onChange, context }) => {
Expand All @@ -35,8 +35,8 @@ export const AlarmTableAdditional: React.FC<AlarmTableAdditionalProps> = ({ onCh
'NOTE: The data source must be configured using an user with the \'admin\' role ' +
'in order to perform actions as other users.'

const autoRefreshTooltipText = 'Enables auto-refresh after performing an action such as acknowledge, clear or escalate. ' +
'NOTE: This will refresh the entire dashboard.'
const actionNoticeTooltipText = 'Enables a notice to be displayed after performing an action such as acknowledge, clear or escalate ' +
'which will notify the user whether the action succeeded and prompt them to refresh the panel.'

useEffect(() => {
onChange(alarmTableAdditional);
Expand All @@ -61,11 +61,11 @@ export const AlarmTableAdditional: React.FC<AlarmTableAdditionalProps> = ({ onCh
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label='Auto refresh' tooltip={autoRefreshTooltipText}>
<InlineField label='Display action notice' tooltip={actionNoticeTooltipText}>
<div style={{ display: 'flex', alignItems: 'center', height: '32px' }}>
<Switch
value={alarmTableAdditional.autoRefresh}
onChange={() => setAlarmTableState('autoRefresh', !alarmTableAdditional.autoRefresh)} />
value={alarmTableAdditional.displayActionNotice}
onChange={() => setAlarmTableState('displayActionNotice', !alarmTableAdditional.displayActionNotice)} />
</div>
</InlineField>
</InlineFieldRow>
Expand Down
39 changes: 26 additions & 13 deletions src/panels/alarm-table/AlarmTableControl.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useRef } from 'react'
import { PanelProps } from '@grafana/data'
import { AppEvents, PanelProps } from '@grafana/data'
import { getAppEvents } from '@grafana/runtime'
import { Button, ContextMenu, Modal, Pagination, Tab, TabContent, Table, TabsBar } from '@grafana/ui'
import { AlarmTableMenu } from './AlarmTableMenu'
import { AlarmTableModalContent } from './modal/AlarmTableModalContent'
Expand All @@ -14,9 +15,9 @@ import { useAlarmTableSelection } from './hooks/useAlarmTableSelection'
import { useAlarmTableModalTabs } from './hooks/useAlarmTableModalTabs'
import { useOpenNMSClient } from '../../hooks/useOpenNMSClient'
import { useAlarm } from './hooks/useAlarm'
import { capitalize } from 'lib/utils'

export const AlarmTableControl: React.FC<PanelProps<AlarmTableControlProps>> = (props) => {

const alarmIndexes = useRef<boolean[]>([] as boolean[])

const { state, setState, rowClicked, soloIndex } = useAlarmTableSelection(() => {
Expand All @@ -31,8 +32,8 @@ export const AlarmTableControl: React.FC<PanelProps<AlarmTableControlProps>> = (
state.indexes,
props?.data?.series?.[0]?.fields || [],
() => setMenuOpen(false),
(actionName: string) => refreshPanel(actionName),
props?.options?.alarmTable?.alarmTableAdditional?.useGrafanaUser || false,
(actionName: string, results: any[]) => displayActionNotice(actionName, results),
props?.options?.alarmTable?.alarmTableAdditional?.useGrafanaUser ?? false,
client)

const { tabActive, tabClick, resetTabs } = useAlarmTableModalTabs()
Expand All @@ -42,17 +43,29 @@ export const AlarmTableControl: React.FC<PanelProps<AlarmTableControlProps>> = (
useAlarmTableConfigDefaults(props.fieldConfig, props.onFieldConfigChange, props.options)

/**
* Callback when an action menu item is clicked to resend the datasource query and refresh the panel.
* Grafana does not seem to offer a clear way to do this programmatically, so we "click"
* the "Refresh dashboard" button.
* Callback when an action menu item is clicked to display a message.
* Grafana does not offer a clear way to refresh the panel programmatically, so we inform the user
* they must do so.
*/
const refreshPanel = (actionName: string) => {
if (props?.options?.alarmTable.alarmTableAdditional.autoRefresh) {
const refreshButton = document.body.querySelector('div.refresh-picker button') as HTMLElement
const testId = refreshButton?.dataset.testid
const displayActionNotice = (actionName: string, results: any[]) => {
if (props?.options?.alarmTable.alarmTableAdditional.displayActionNotice) {
const numErrors = results.filter(r => r?.status === 'error').reduce((acc: number, result) => acc + 1, 0)

const appEvents = getAppEvents()
const capitalAction = capitalize(actionName)

if (!numErrors) {
appEvents.publish({
type: AppEvents.alertSuccess.name,
payload: [`Alarm ${capitalAction} was successful. You may need to refresh the panel for the updated status to display.`]
})
} else {
const message = numErrors === 1 ? `Error processing alarm ${capitalAction}` : `There were ${numErrors} errors processing alarm ${capitalAction}`

if (refreshButton && testId && testId.includes('RefreshPicker')) {
refreshButton.click()
appEvents.publish({
type: AppEvents.alertError.name,
payload: [message]
})
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/panels/alarm-table/AlarmTableTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface AlarmTableControlState {
}

export interface AlarmTableAdditionalState {
autoRefresh: boolean
displayActionNotice: boolean
useGrafanaUser: boolean
}

Expand Down
14 changes: 9 additions & 5 deletions src/panels/alarm-table/hooks/useAlarmProperties.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { ArrayVector, DataFrame } from '@grafana/data'
import { ArrayVector, DataFrame, Field } from '@grafana/data'
import cloneDeep from 'lodash/cloneDeep'
import { AlarmTableColumnSizeItem } from '../AlarmTableTypes'

Expand Down Expand Up @@ -73,12 +73,16 @@ export const useAlarmProperties = (oldProperties: DataFrame, alarmTable) => {
if (rowsPerPage > 0 && totalRows > rowsPerPage) {
const myPage = page

filteredProps.fields = filteredProps.fields.map((field) => {
const oldValues = [...field.values.buffer]
filteredProps.fields = filteredProps.fields.map((field: Field) => {
// field.values is a Vector<any>, safest to call 'toArray()'
// rather than assume it's an ArrayVector with a 'buffer' field
const values = field.values.toArray()
const start = (myPage - 1) * rowsPerPage
const end = start + rowsPerPage
const spliced = oldValues.splice(start, end)
field.values = new ArrayVector(spliced)

const sliced = values.length > start ? values.slice(start, end) : []
field.values = new ArrayVector(sliced)

return field
})

Expand Down
Loading

0 comments on commit b6504b2

Please sign in to comment.