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; secureJsonData?: Partial; }; type InstallPluginResponse = Pick & { 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 => this.grafanaBackend.get(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 (): Promise> => { const { key: apiToken } = await this.createGrafanaToken(); await this.updateGrafanaPluginSettings({ secureJsonData: { apiToken } }); // TODO: display alert on error const dataExporterAPIResponse = await queryApi(`/connect`, { method: 'POST', data: { apiToken, url: window.location.toString() }, }); return { apiToken, dataExporterAPIResponse }; }; static installPlugin = async (dataExporterApiUrl: string): Promise => { 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 => { try { const resp = await queryApi(`/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 => { /** * 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 = { dataExporterApiUrl: null, }; const secureJsonData: Required = { apiToken: null, }; return this.updateGrafanaPluginSettings({ jsonData, secureJsonData }, false); }; } export default PluginState;