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