import { queryApi } from './services/api_service' ;
import {
DataExporterAppPluginMeta ,
DataExporterPluginMetaJSONData ,
DataExporterPluginMetaSecureJSONData ,
} from './types' ;
import { getBackendSrv } from '@grafana/runtime' ;
import axios from 'axios' ;
export type UpdateGrafanaPluginSettingsProps = {
jsonData? : Partial < DataExporterPluginMetaJSONData > ;
secureJsonData? : Partial < DataExporterPluginMetaSecureJSONData > ;
} ;
type InstallPluginResponse < DataExporterAPIResponse = any > = Pick <
DataExporterPluginMetaSecureJSONData ,
'grafanaToken'
> & {
dataExporterAPIResponse : DataExporterAPIResponse ;
} ;
export type PluginConnectedStatusResponse = {
version : string ;
} ;
class PluginState {
static GRAFANA_PLUGIN_SETTINGS_URL = '/api/plugins/corpglory-dataexporter-app/settings' ;
static grafanaBackend = getBackendSrv ( ) ;
static generateInvalidDataExporterApiURLErrorMsg = ( dataExporterApiUrl : string ) : string = >
` Could not communicate with your DataExporter API at ${ dataExporterApiUrl } . \ nValidate that the URL is correct, your DataExporter API is running, and that it is accessible from your Grafana instance. ` ;
static generateUnknownErrorMsg = ( dataExporterApiUrl : string ) : string = >
` An unknown error occured when trying to install the plugin. Are you sure that your DataExporter API URL, ${ dataExporterApiUrl } , is correct? \ nRefresh your page and try again, or try removing your plugin configuration and reconfiguring. ` ;
static getHumanReadableErrorFromDataExporterError = ( e : any , dataExporterApiUrl : string ) : string = > {
let errorMsg : string ;
const unknownErrorMsg = this . generateUnknownErrorMsg ( dataExporterApiUrl ) ;
const consoleMsg = ` occured while trying to install the plugin w/ the DataExporter backend ` ;
if ( axios . isAxiosError ( e ) ) {
const statusCode = e . response ? . status ;
console . warn ( ` An HTTP related error ${ consoleMsg } ` , e . response ) ;
if ( statusCode === 502 ) {
// 502 occurs when the plugin-proxy cannot communicate w/ the DataExporter API using the provided URL
errorMsg = this . generateInvalidDataExporterApiURLErrorMsg ( dataExporterApiUrl ) ;
} else if ( statusCode === 400 ) {
/ * *
* A 400 is 'bubbled-up' from the DataExporter API . It indicates one of three cases :
* 1 . there is a communication error when DataExporter API tries to contact Grafana ' s API
* 2 . there is an auth error when DataExporter API tries to contact Grafana ' s API
* 3 . ( likely rare ) user inputs an DataExporterApiUrl that is not RFC 1034 / 1035 compliant
*
* Check if the response body has an 'error' JSON attribute , if it does , assume scenario 1 or 2
* Use the error message provided to give the user more context / helpful debugging information
* /
errorMsg = e . response ? . data ? . error || unknownErrorMsg ;
} else {
// this scenario shouldn't occur..
errorMsg = unknownErrorMsg ;
}
} else {
// a non-axios related error occured.. this scenario shouldn't occur...
console . warn ( ` An unknown error ${ consoleMsg } ` , e ) ;
errorMsg = unknownErrorMsg ;
}
return errorMsg ;
} ;
static getHumanReadableErrorFromGrafanaProvisioningError = ( e : any , dataExporterApiUrl : string ) : string = > {
let errorMsg : string ;
if ( axios . isAxiosError ( e ) ) {
// The user likely put in a bogus URL for the DataExporter API URL
console . warn ( 'An HTTP related error occured while trying to provision the plugin w/ Grafana' , e . response ) ;
errorMsg = this . generateInvalidDataExporterApiURLErrorMsg ( dataExporterApiUrl ) ;
} else {
// a non-axios related error occured.. this scenario shouldn't occur...
console . warn ( 'An unknown error occured while trying to provision the plugin w/ Grafana' , e ) ;
errorMsg = this . generateUnknownErrorMsg ( dataExporterApiUrl ) ;
}
return errorMsg ;
} ;
static getGrafanaPluginSettings = async ( ) : Promise < DataExporterAppPluginMeta > = >
this . grafanaBackend . get < DataExporterAppPluginMeta > ( this . GRAFANA_PLUGIN_SETTINGS_URL ) ;
static updateGrafanaPluginSettings = async ( data : UpdateGrafanaPluginSettingsProps , enabled = true ) = >
this . grafanaBackend . post (
this . GRAFANA_PLUGIN_SETTINGS_URL ,
{ . . . data , enabled , pinned : true } ,
// @ts-ignore
// for some reason, there is no `options` argument in Grafana's public types for BackendSrv but it exists
{ showSuccessAlert : false }
) ;
static createGrafanaToken = async ( ) = > {
const baseUrl = '/api/auth/keys' ;
const keys = await this . grafanaBackend . get ( baseUrl ) ;
const existingKey = keys . find ( ( key : { id : number ; name : string ; role : string } ) = > key . name === 'DataExporter' ) ;
if ( existingKey ) {
// @ts-ignore
// for some reason, there is no `options` argument in Grafana's public types for BackendSrv but it exists
await this . grafanaBackend . delete ( ` ${ baseUrl } / ${ existingKey . id } ` , undefined , { showSuccessAlert : false } ) ;
}
return await this . grafanaBackend . post (
baseUrl ,
{
name : 'DataExporter' ,
role : 'Admin' ,
secondsToLive : null ,
} ,
// @ts-ignore
// for some reason, there is no `options` argument in Grafana's public types for BackendSrv but it exists
{ showSuccessAlert : false }
) ;
} ;
static timeout = ( pollCount : number ) = > new Promise ( ( resolve ) = > setTimeout ( resolve , 10 * 2 * * pollCount ) ) ;
static connectBackend = async < RT > ( ) : Promise < InstallPluginResponse < RT > > = > {
// TODO: try to disable success alerts from Grafana API
const { key : grafanaToken } = await this . createGrafanaToken ( ) ;
await this . updateGrafanaPluginSettings ( { secureJsonData : { grafanaToken } } ) ;
const dataExporterAPIResponse = await queryApi < RT > ( ` /connect ` , {
method : 'POST' ,
} ) ;
return { grafanaToken , dataExporterAPIResponse } ;
} ;
static installPlugin = async ( dataExporterApiUrl : string ) : Promise < string | null > = > {
let pluginInstallationDataExporterResponse : InstallPluginResponse < { version : string } > ;
// Step 1. Try provisioning the plugin w/ the Grafana API
try {
await this . updateGrafanaPluginSettings ( { jsonData : { dataExporterApiUrl } } ) ;
} catch ( e ) {
return this . getHumanReadableErrorFromGrafanaProvisioningError ( e , dataExporterApiUrl ) ;
}
/ * *
* Step 2 :
* - Create a grafana token
* - store that token in the Grafana plugin settings
* - configure the plugin in DataExporter ' s backend
* /
try {
pluginInstallationDataExporterResponse = await this . connectBackend < { version : string } > ( ) ;
} catch ( e ) {
return this . getHumanReadableErrorFromDataExporterError ( e , dataExporterApiUrl ) ;
}
// Step 3. reprovision the Grafana plugin settings, storing information that we get back from DataExporter's backend
try {
const { grafanaToken } = pluginInstallationDataExporterResponse ;
await this . updateGrafanaPluginSettings ( {
jsonData : {
dataExporterApiUrl ,
} ,
secureJsonData : {
grafanaToken ,
} ,
} ) ;
} catch ( e ) {
return this . getHumanReadableErrorFromGrafanaProvisioningError ( e , dataExporterApiUrl ) ;
}
return null ;
} ;
static checkIfPluginIsConnected = async (
dataExporterApiUrl : string
) : Promise < PluginConnectedStatusResponse | string > = > {
try {
const resp = await queryApi < PluginConnectedStatusResponse > ( ` /status ` , {
method : 'GET' ,
} ) ;
// TODO: check if the server version is compatible with the plugin
if ( resp . version ) {
return resp ;
} else {
throw new Error ( ` Something is working at ${ dataExporterApiUrl } but it's not DataExporter backend ` ) ;
}
} catch ( e ) {
return this . getHumanReadableErrorFromDataExporterError ( e , dataExporterApiUrl ) ;
}
} ;
static resetPlugin = ( ) : Promise < void > = > {
/ * *
* mark both of these objects as Required . . this will ensure that we are resetting every attribute back to null
* and throw a type error in the event that DataExporterPluginMetaJSONData or DataExporterPluginMetaSecureJSONData is updated
* but we forget to add the attribute here
* /
const jsonData : Required < DataExporterPluginMetaJSONData > = {
dataExporterApiUrl : null ,
} ;
const secureJsonData : Required < DataExporterPluginMetaSecureJSONData > = {
grafanaToken : null ,
} ;
return this . updateGrafanaPluginSettings ( { jsonData , secureJsonData } , false ) ;
} ;
}
export default PluginState ;