1import { createAction, createAsyncThunk, Update } from '@reduxjs/toolkit'; 2import { getBackendSrv } from '@grafana/runtime'; 3import { PanelPlugin } from '@grafana/data'; 4import { StoreState, ThunkResult } from 'app/types'; 5import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; 6import { 7 getRemotePlugins, 8 getPluginErrors, 9 getLocalPlugins, 10 getPluginDetails, 11 installPlugin, 12 uninstallPlugin, 13} from '../api'; 14import { STATE_PREFIX } from '../constants'; 15import { mergeLocalsAndRemotes, updatePanels } from '../helpers'; 16import { CatalogPlugin, RemotePlugin } from '../types'; 17import { invalidatePluginInCache } from '../../pluginCacheBuster'; 18 19export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, thunkApi) => { 20 try { 21 const { dispatch } = thunkApi; 22 const [localPlugins, pluginErrors, { payload: remotePlugins }] = await Promise.all([ 23 getLocalPlugins(), 24 getPluginErrors(), 25 dispatch(fetchRemotePlugins()), 26 ]); 27 28 return mergeLocalsAndRemotes(localPlugins, remotePlugins, pluginErrors); 29 } catch (e) { 30 return thunkApi.rejectWithValue('Unknown error.'); 31 } 32}); 33 34export const fetchRemotePlugins = createAsyncThunk<RemotePlugin[], void, { rejectValue: RemotePlugin[] }>( 35 `${STATE_PREFIX}/fetchRemotePlugins`, 36 async (_, thunkApi) => { 37 try { 38 return await getRemotePlugins(); 39 } catch (error) { 40 error.isHandled = true; 41 return thunkApi.rejectWithValue([]); 42 } 43 } 44); 45 46export const fetchDetails = createAsyncThunk(`${STATE_PREFIX}/fetchDetails`, async (id: string, thunkApi) => { 47 try { 48 const details = await getPluginDetails(id); 49 50 return { 51 id, 52 changes: { details }, 53 } as Update<CatalogPlugin>; 54 } catch (e) { 55 return thunkApi.rejectWithValue('Unknown error.'); 56 } 57}); 58 59// We are also using the install API endpoint to update the plugin 60export const install = createAsyncThunk( 61 `${STATE_PREFIX}/install`, 62 async ({ id, version, isUpdating = false }: { id: string; version?: string; isUpdating?: boolean }, thunkApi) => { 63 const changes = isUpdating 64 ? { isInstalled: true, installedVersion: version, hasUpdate: false } 65 : { isInstalled: true, installedVersion: version }; 66 try { 67 await installPlugin(id); 68 await updatePanels(); 69 70 if (isUpdating) { 71 invalidatePluginInCache(id); 72 } 73 74 return { id, changes } as Update<CatalogPlugin>; 75 } catch (e) { 76 return thunkApi.rejectWithValue('Unknown error.'); 77 } 78 } 79); 80 81export const uninstall = createAsyncThunk(`${STATE_PREFIX}/uninstall`, async (id: string, thunkApi) => { 82 try { 83 await uninstallPlugin(id); 84 await updatePanels(); 85 86 invalidatePluginInCache(id); 87 88 return { 89 id, 90 changes: { isInstalled: false, installedVersion: undefined }, 91 } as Update<CatalogPlugin>; 92 } catch (e) { 93 return thunkApi.rejectWithValue('Unknown error.'); 94 } 95}); 96 97// We need this to be backwards-compatible with other parts of Grafana. 98// (Originally in "public/app/features/plugins/state/actions.ts") 99// TODO<remove once the "plugin_admin_enabled" feature flag is removed> 100export const loadPluginDashboards = createAsyncThunk(`${STATE_PREFIX}/loadPluginDashboards`, async (_, thunkApi) => { 101 const state = thunkApi.getState() as StoreState; 102 const dataSourceType = state.dataSources.dataSource.type; 103 const url = `api/plugins/${dataSourceType}/dashboards`; 104 105 return getBackendSrv().get(url); 106}); 107 108export const panelPluginLoaded = createAction<PanelPlugin>(`${STATE_PREFIX}/panelPluginLoaded`); 109 110// We need this to be backwards-compatible with other parts of Grafana. 111// (Originally in "public/app/features/plugins/state/actions.ts") 112// It cannot be constructed with `createAsyncThunk()` as we need the return value on the call-site, 113// and we cannot easily change the call-site to unwrap the result. 114// TODO<remove once the "plugin_admin_enabled" feature flag is removed> 115export const loadPanelPlugin = (id: string): ThunkResult<Promise<PanelPlugin>> => { 116 return async (dispatch, getStore) => { 117 let plugin = getStore().plugins.panels[id]; 118 119 if (!plugin) { 120 plugin = await importPanelPlugin(id); 121 122 // second check to protect against raise condition 123 if (!getStore().plugins.panels[id]) { 124 dispatch(panelPluginLoaded(plugin)); 125 } 126 } 127 128 return plugin; 129 }; 130}; 131