1import { createSlice, createEntityAdapter, Reducer, AnyAction, PayloadAction } from '@reduxjs/toolkit';
2import { fetchAll, fetchDetails, install, uninstall, loadPluginDashboards, panelPluginLoaded } from './actions';
3import { CatalogPlugin, PluginListDisplayMode, ReducerState, RequestStatus } from '../types';
4import { STATE_PREFIX } from '../constants';
5import { PanelPlugin } from '@grafana/data';
6
7export const pluginsAdapter = createEntityAdapter<CatalogPlugin>();
8
9const isPendingRequest = (action: AnyAction) => new RegExp(`${STATE_PREFIX}\/(.*)\/pending`).test(action.type);
10
11const isFulfilledRequest = (action: AnyAction) => new RegExp(`${STATE_PREFIX}\/(.*)\/fulfilled`).test(action.type);
12
13const isRejectedRequest = (action: AnyAction) => new RegExp(`${STATE_PREFIX}\/(.*)\/rejected`).test(action.type);
14
15// Extract the trailing '/pending', '/rejected', or '/fulfilled'
16const getOriginalActionType = (type: string) => {
17  const separator = type.lastIndexOf('/');
18
19  return type.substring(0, separator);
20};
21
22const slice = createSlice({
23  name: 'plugins',
24  initialState: {
25    items: pluginsAdapter.getInitialState(),
26    requests: {},
27    settings: {
28      displayMode: PluginListDisplayMode.Grid,
29    },
30    // Backwards compatibility
31    // (we need to have the following fields in the store as well to be backwards compatible with other parts of Grafana)
32    // TODO<remove once the "plugin_admin_enabled" feature flag is removed>
33    plugins: [],
34    errors: [],
35    searchQuery: '',
36    hasFetched: false,
37    dashboards: [],
38    isLoadingPluginDashboards: false,
39    panels: {},
40  } as ReducerState,
41  reducers: {
42    setDisplayMode(state, action: PayloadAction<PluginListDisplayMode>) {
43      state.settings.displayMode = action.payload;
44    },
45  },
46  extraReducers: (builder) =>
47    builder
48      // Fetch All
49      .addCase(fetchAll.fulfilled, (state, action) => {
50        pluginsAdapter.upsertMany(state.items, action.payload);
51      })
52      // Fetch Details
53      .addCase(fetchDetails.fulfilled, (state, action) => {
54        pluginsAdapter.updateOne(state.items, action.payload);
55      })
56      // Install
57      .addCase(install.fulfilled, (state, action) => {
58        pluginsAdapter.updateOne(state.items, action.payload);
59      })
60      // Uninstall
61      .addCase(uninstall.fulfilled, (state, action) => {
62        pluginsAdapter.updateOne(state.items, action.payload);
63      })
64      // Load a panel plugin (backward-compatibility)
65      // TODO<remove once the "plugin_admin_enabled" feature flag is removed>
66      .addCase(panelPluginLoaded, (state, action: PayloadAction<PanelPlugin>) => {
67        state.panels[action.payload.meta.id] = action.payload;
68      })
69      // Start loading panel dashboards (backward-compatibility)
70      // TODO<remove once the "plugin_admin_enabled" feature flag is removed>
71      .addCase(loadPluginDashboards.pending, (state, action) => {
72        state.isLoadingPluginDashboards = true;
73        state.dashboards = [];
74      })
75      // Load panel dashboards (backward-compatibility)
76      // TODO<remove once the "plugin_admin_enabled" feature flag is removed>
77      .addCase(loadPluginDashboards.fulfilled, (state, action) => {
78        state.isLoadingPluginDashboards = false;
79        state.dashboards = action.payload;
80      })
81      .addMatcher(isPendingRequest, (state, action) => {
82        state.requests[getOriginalActionType(action.type)] = {
83          status: RequestStatus.Pending,
84        };
85      })
86      .addMatcher(isFulfilledRequest, (state, action) => {
87        state.requests[getOriginalActionType(action.type)] = {
88          status: RequestStatus.Fulfilled,
89        };
90      })
91      .addMatcher(isRejectedRequest, (state, action) => {
92        state.requests[getOriginalActionType(action.type)] = {
93          status: RequestStatus.Rejected,
94          error: action.payload,
95        };
96      }),
97});
98
99export const { setDisplayMode } = slice.actions;
100export const reducer: Reducer<ReducerState, AnyAction> = slice.reducer;
101