You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
8.5 KiB
214 lines
8.5 KiB
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, 'apiToken'> & { |
|
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>> => { |
|
const { key: apiToken } = await this.createGrafanaToken(); |
|
await this.updateGrafanaPluginSettings({ secureJsonData: { apiToken } }); |
|
// TODO: display alert on error |
|
const dataExporterAPIResponse = await queryApi<RT>(`/connect`, { |
|
method: 'POST', |
|
data: { apiToken, url: window.location.toString() }, |
|
}); |
|
return { apiToken, 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 { apiToken } = pluginInstallationDataExporterResponse; |
|
|
|
await this.updateGrafanaPluginSettings({ |
|
jsonData: { |
|
dataExporterApiUrl, |
|
}, |
|
secureJsonData: { |
|
apiToken, |
|
}, |
|
}); |
|
} catch (e) { |
|
return this.getHumanReadableErrorFromGrafanaProvisioningError(e, dataExporterApiUrl); |
|
} |
|
|
|
return null; |
|
}; |
|
|
|
static checkIfPluginIsConnected = async ( |
|
dataExporterApiUrl: string |
|
): Promise<PluginConnectedStatusResponse | string> => { |
|
try { |
|
const resp = await queryApi<PluginConnectedStatusResponse>(`/connect`, { |
|
method: 'GET', |
|
params: { url: window.location.toString() }, |
|
}); |
|
|
|
// TODO: check if the server version is compatible with the plugin |
|
// TODO: remove configuration if backend says that api key doesn't work |
|
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> = { |
|
apiToken: null, |
|
}; |
|
|
|
return this.updateGrafanaPluginSettings({ jsonData, secureJsonData }, false); |
|
}; |
|
} |
|
|
|
export default PluginState;
|
|
|