123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- import { lastValueFrom } from 'rxjs';
- import { DataSourcePluginMeta, DataSourceSettings, locationUtil } from '@grafana/data';
- import { DataSourceWithBackend, getDataSourceSrv, locationService } from '@grafana/runtime';
- import { updateNavIndex } from 'app/core/actions';
- import { getBackendSrv } from 'app/core/services/backend_srv';
- import { accessControlQueryParam } from 'app/core/utils/accessControl';
- import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
- import { getPluginSettings } from 'app/features/plugins/pluginSettings';
- import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
- import { DataSourcePluginCategory, ThunkDispatch, ThunkResult } from 'app/types';
- import { contextSrv } from '../../../core/services/context_srv';
- import { buildCategories } from './buildCategories';
- import { buildNavModel } from './navModel';
- import {
- dataSourceLoaded,
- dataSourceMetaLoaded,
- dataSourcePluginsLoad,
- dataSourcePluginsLoaded,
- dataSourcesLoaded,
- initDataSourceSettingsFailed,
- initDataSourceSettingsSucceeded,
- testDataSourceFailed,
- testDataSourceStarting,
- testDataSourceSucceeded,
- } from './reducers';
- import { getDataSource, getDataSourceMeta } from './selectors';
- export interface DataSourceTypesLoadedPayload {
- plugins: DataSourcePluginMeta[];
- categories: DataSourcePluginCategory[];
- }
- export interface InitDataSourceSettingDependencies {
- loadDataSource: typeof loadDataSource;
- loadDataSourceMeta: typeof loadDataSourceMeta;
- getDataSource: typeof getDataSource;
- getDataSourceMeta: typeof getDataSourceMeta;
- importDataSourcePlugin: typeof importDataSourcePlugin;
- }
- export interface TestDataSourceDependencies {
- getDatasourceSrv: typeof getDataSourceSrv;
- getBackendSrv: typeof getBackendSrv;
- }
- export const initDataSourceSettings = (
- pageId: string,
- dependencies: InitDataSourceSettingDependencies = {
- loadDataSource,
- loadDataSourceMeta,
- getDataSource,
- getDataSourceMeta,
- importDataSourcePlugin,
- }
- ): ThunkResult<void> => {
- return async (dispatch, getState) => {
- if (!pageId) {
- dispatch(initDataSourceSettingsFailed(new Error('Invalid ID')));
- return;
- }
- try {
- const loadedDataSource = await dispatch(dependencies.loadDataSource(pageId));
- await dispatch(dependencies.loadDataSourceMeta(loadedDataSource));
- // have we already loaded the plugin then we can skip the steps below?
- if (getState().dataSourceSettings.plugin) {
- return;
- }
- const dataSource = dependencies.getDataSource(getState().dataSources, pageId);
- const dataSourceMeta = dependencies.getDataSourceMeta(getState().dataSources, dataSource!.type);
- const importedPlugin = await dependencies.importDataSourcePlugin(dataSourceMeta);
- dispatch(initDataSourceSettingsSucceeded(importedPlugin));
- } catch (err) {
- dispatch(initDataSourceSettingsFailed(err));
- }
- };
- };
- export const testDataSource = (
- dataSourceName: string,
- dependencies: TestDataSourceDependencies = {
- getDatasourceSrv,
- getBackendSrv,
- }
- ): ThunkResult<void> => {
- return async (dispatch: ThunkDispatch, getState) => {
- const dsApi = await dependencies.getDatasourceSrv().get(dataSourceName);
- if (!dsApi.testDatasource) {
- return;
- }
- dispatch(testDataSourceStarting());
- dependencies.getBackendSrv().withNoBackendCache(async () => {
- try {
- const result = await dsApi.testDatasource();
- dispatch(testDataSourceSucceeded(result));
- } catch (err) {
- const { statusText, message: errMessage, details, data } = err;
- const message = errMessage || data?.message || 'HTTP error ' + statusText;
- dispatch(testDataSourceFailed({ message, details }));
- }
- });
- };
- };
- export function loadDataSources(): ThunkResult<void> {
- return async (dispatch) => {
- const response = await getBackendSrv().get('/api/datasources');
- dispatch(dataSourcesLoaded(response));
- };
- }
- export function loadDataSource(uid: string): ThunkResult<Promise<DataSourceSettings>> {
- return async (dispatch) => {
- const dataSource = await getDataSourceUsingUidOrId(uid);
- dispatch(dataSourceLoaded(dataSource));
- return dataSource;
- };
- }
- export function loadDataSourceMeta(dataSource: DataSourceSettings): ThunkResult<void> {
- return async (dispatch) => {
- const pluginInfo = (await getPluginSettings(dataSource.type)) as DataSourcePluginMeta;
- const plugin = await importDataSourcePlugin(pluginInfo);
- const isBackend = plugin.DataSourceClass.prototype instanceof DataSourceWithBackend;
- const meta = {
- ...pluginInfo,
- isBackend: pluginInfo.backend || isBackend,
- };
- dispatch(dataSourceMetaLoaded(meta));
- plugin.meta = meta;
- dispatch(updateNavIndex(buildNavModel(dataSource, plugin)));
- };
- }
- /**
- * Get data source by uid or id, if old id detected handles redirect
- */
- export async function getDataSourceUsingUidOrId(uid: string | number): Promise<DataSourceSettings> {
- // Try first with uid api
- try {
- const byUid = await lastValueFrom(
- getBackendSrv().fetch<DataSourceSettings>({
- method: 'GET',
- url: `/api/datasources/uid/${uid}`,
- params: accessControlQueryParam(),
- showErrorAlert: false,
- })
- );
- if (byUid.ok) {
- return byUid.data;
- }
- } catch (err) {
- console.log('Failed to lookup data source by uid', err);
- }
- // try lookup by old db id
- const id = typeof uid === 'string' ? parseInt(uid, 10) : uid;
- if (!Number.isNaN(id)) {
- const response = await lastValueFrom(
- getBackendSrv().fetch<DataSourceSettings>({
- method: 'GET',
- url: `/api/datasources/${id}`,
- params: accessControlQueryParam(),
- showErrorAlert: false,
- })
- );
- // If the uid is a number, then this is a refresh on one of the settings tabs
- // and we can return the response data
- if (response.ok && typeof uid === 'number' && response.data.id === uid) {
- return response.data;
- }
- // Not ideal to do a full page reload here but so tricky to handle this
- // otherwise We can update the location using react router, but need to
- // fully reload the route as the nav model page index is not matching with
- // the url in that case. And react router has no way to unmount remount a
- // route
- if (response.ok && response.data.id.toString() === uid) {
- window.location.href = locationUtil.assureBaseUrl(`/datasources/edit/${response.data.uid}`);
- return {} as DataSourceSettings; // avoids flashing an error
- }
- }
- throw Error('Could not find data source');
- }
- export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult<void> {
- return async (dispatch, getStore) => {
- await dispatch(loadDataSources());
- const dataSources = getStore().dataSources.dataSources;
- const newInstance = {
- name: plugin.name,
- type: plugin.id,
- access: 'proxy',
- isDefault: dataSources.length === 0,
- };
- if (nameExits(dataSources, newInstance.name)) {
- newInstance.name = findNewName(dataSources, newInstance.name);
- }
- const result = await getBackendSrv().post('/api/datasources', newInstance);
- await getDatasourceSrv().reload();
- await contextSrv.fetchUserPermissions();
- locationService.push(`/datasources/edit/${result.datasource.uid}`);
- };
- }
- export function loadDataSourcePlugins(): ThunkResult<void> {
- return async (dispatch) => {
- dispatch(dataSourcePluginsLoad());
- const plugins = await getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' });
- const categories = buildCategories(plugins);
- dispatch(dataSourcePluginsLoaded({ plugins, categories }));
- };
- }
- export function updateDataSource(dataSource: DataSourceSettings): ThunkResult<void> {
- return async (dispatch) => {
- await getBackendSrv().put(`/api/datasources/${dataSource.id}`, dataSource); // by UID not yet supported
- await getDatasourceSrv().reload();
- return dispatch(loadDataSource(dataSource.uid));
- };
- }
- export function deleteDataSource(): ThunkResult<void> {
- return async (dispatch, getStore) => {
- const dataSource = getStore().dataSources.dataSource;
- await getBackendSrv().delete(`/api/datasources/${dataSource.id}`);
- await getDatasourceSrv().reload();
- locationService.push('/datasources');
- };
- }
- interface ItemWithName {
- name: string;
- }
- export function nameExits(dataSources: ItemWithName[], name: string) {
- return (
- dataSources.filter((dataSource) => {
- return dataSource.name.toLowerCase() === name.toLowerCase();
- }).length > 0
- );
- }
- export function findNewName(dataSources: ItemWithName[], name: string) {
- // Need to loop through current data sources to make sure
- // the name doesn't exist
- while (nameExits(dataSources, name)) {
- // If there's a duplicate name that doesn't end with '-x'
- // we can add -1 to the name and be done.
- if (!nameHasSuffix(name)) {
- name = `${name}-1`;
- } else {
- // if there's a duplicate name that ends with '-x'
- // we can try to increment the last digit until the name is unique
- // remove the 'x' part and replace it with the new number
- name = `${getNewName(name)}${incrementLastDigit(getLastDigit(name))}`;
- }
- }
- return name;
- }
- function nameHasSuffix(name: string) {
- return name.endsWith('-', name.length - 1);
- }
- function getLastDigit(name: string) {
- return parseInt(name.slice(-1), 10);
- }
- function incrementLastDigit(digit: number) {
- return isNaN(digit) ? 1 : digit + 1;
- }
- function getNewName(name: string) {
- return name.slice(0, name.length - 1);
- }
|