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