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.

215 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) =>
2 years ago
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 {
2 years ago
const resp = await queryApi<PluginConnectedStatusResponse>(`/connect`, {
method: 'GET',
params: { url: window.location.toString() },
});
// TODO: check if the server version is compatible with the plugin
2 years ago
// 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;