Connections: Make the "Add new Connection" page work without internet access (#75272)

* fix: make the "Add new Connection" page work without internet access

* feat: update error message

* fix: update the API url in the error log
This commit is contained in:
Levente Balogh 2023-09-27 15:04:23 +02:00 committed by GitHub
parent b374912937
commit 163b0d182b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 100 additions and 13 deletions

View File

@ -39,7 +39,7 @@ export function AddNewConnection() {
setSearchTerm(e.currentTarget.value.toLowerCase());
};
const { isLoading, error, plugins } = useGetAll({
const { error, plugins, isLoading } = useGetAll({
keyword: searchTerm,
type: PluginType.datasource,
});

View File

@ -1,6 +1,7 @@
import { createAction, createAsyncThunk, Update } from '@reduxjs/toolkit';
import { from, forkJoin, timeout, lastValueFrom, catchError, throwError } from 'rxjs';
import { PanelPlugin } from '@grafana/data';
import { PanelPlugin, PluginError } from '@grafana/data';
import { getBackendSrv, isFetchError } from '@grafana/runtime';
import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
import { StoreState, ThunkResult } from 'app/types';
@ -18,16 +19,76 @@ import { STATE_PREFIX } from '../constants';
import { mapLocalToCatalog, mergeLocalsAndRemotes, updatePanels } from '../helpers';
import { CatalogPlugin, RemotePlugin, LocalPlugin } from '../types';
// Fetches
export const fetchAll = createAsyncThunk(`${STATE_PREFIX}/fetchAll`, async (_, thunkApi) => {
try {
const { dispatch } = thunkApi;
const [localPlugins, pluginErrors, { payload: remotePlugins }] = await Promise.all([
getLocalPlugins(),
getPluginErrors(),
dispatch(fetchRemotePlugins()),
]);
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchLocal/pending` });
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchRemote/pending` });
return mergeLocalsAndRemotes(localPlugins, remotePlugins, pluginErrors);
const local$ = from(getLocalPlugins());
const remote$ = from(getRemotePlugins());
const pluginErrors$ = from(getPluginErrors());
forkJoin({
local: local$,
remote: remote$,
pluginErrors: pluginErrors$,
})
.pipe(
// Fetching the list of plugins from GCOM is slow / errors out
timeout({
each: 500,
with: () => {
remote$
// The request to fetch remote plugins from GCOM failed
.pipe(
catchError((err) => {
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchRemote/rejected` });
return throwError(
() => new Error('Failed to fetch plugins from catalog (default https://grafana.com/api/plugins)')
);
})
)
// Remote plugins loaded after a timeout, updating the store
.subscribe(async (remote: RemotePlugin[]) => {
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchRemote/fulfilled` });
if (remote.length > 0) {
const local = await lastValueFrom(local$);
const pluginErrors = await lastValueFrom(pluginErrors$);
thunkApi.dispatch(addPlugins(mergeLocalsAndRemotes(local, remote, pluginErrors)));
}
});
return forkJoin({ local: local$, pluginErrors: pluginErrors$ });
},
})
)
.subscribe(
({
local,
remote,
pluginErrors,
}: {
local: LocalPlugin[];
remote?: RemotePlugin[];
pluginErrors: PluginError[];
}) => {
thunkApi.dispatch({ type: `${STATE_PREFIX}/fetchLocal/fulfilled` });
// Both local and remote plugins are loaded
if (local && remote) {
thunkApi.dispatch(addPlugins(mergeLocalsAndRemotes(local, remote, pluginErrors)));
// Only remote plugins are loaded (remote timed out)
} else if (local) {
thunkApi.dispatch(addPlugins(mergeLocalsAndRemotes(local, [], pluginErrors)));
}
}
);
return null;
} catch (e) {
return thunkApi.rejectWithValue('Unknown error.');
}
@ -72,6 +133,24 @@ export const fetchDetails = createAsyncThunk<Update<CatalogPlugin>, string>(
}
);
export const addPlugins = createAction<CatalogPlugin[]>(`${STATE_PREFIX}/addPlugins`);
// 1. gets remote equivalents from the store (if there are any)
// 2. merges the remote equivalents with the local plugins
// 3. updates the the store with the updated CatalogPlugin objects
export const addLocalPlugins = createAction<LocalPlugin[]>(`${STATE_PREFIX}/addLocalPlugins`);
// 1. gets local equivalents from the store (if there are any)
// 2. merges the local equivalents with the remote plugins
// 3. updates the the store with the updated CatalogPlugin objects
export const addRemotePlugins = createAction<RemotePlugin[]>(`${STATE_PREFIX}/addLocalPlugins`);
// 1. merges the local and remote plugins
// 2. updates the store with the CatalogPlugin objects
export const addLocalAndRemotePlugins = createAction<{ local: LocalPlugin[]; remote: RemotePlugin[] }>(
`${STATE_PREFIX}/addLocalPlugins`
);
// We are also using the install API endpoint to update the plugin
export const install = createAsyncThunk<
Update<CatalogPlugin>,

View File

@ -24,7 +24,9 @@ export const useGetAll = (filters: PluginFilters, sortBy: Sorters = Sorters.name
const selector = useMemo(() => selectPlugins(filters), [filters]);
const plugins = useSelector(selector);
const { isLoading, error } = useFetchStatus();
// As the locally installed plugins load quicker than the remote ones, we only show a loading state until these are being loaded
// (In case the remote ones are not loaded within a reasonable timeout, we will merge those with the locally installed plugins once they are loaded)
const { isLoading, error } = useLocalFetchStatus();
const sortedPlugins = sortPlugins(plugins, sortBy);
return {
@ -74,6 +76,13 @@ export const useIsRemotePluginsAvailable = () => {
return error === null;
};
export const useLocalFetchStatus = () => {
const isLoading = useSelector(selectIsRequestPending('plugins/fetchLocal'));
const error = useSelector(selectRequestError('plugins/fetchLocal'));
return { isLoading, error };
};
export const useFetchStatus = () => {
const isLoading = useSelector(selectIsRequestPending(fetchAll.typePrefix));
const error = useSelector(selectRequestError(fetchAll.typePrefix));

View File

@ -6,13 +6,13 @@ import { STATE_PREFIX } from '../constants';
import { CatalogPlugin, PluginListDisplayMode, ReducerState, RequestStatus } from '../types';
import {
fetchAll,
fetchDetails,
install,
uninstall,
loadPluginDashboards,
panelPluginLoaded,
fetchAllLocal,
addPlugins,
} from './actions';
export const pluginsAdapter = createEntityAdapter<CatalogPlugin>();
@ -58,8 +58,7 @@ const slice = createSlice({
},
extraReducers: (builder) =>
builder
// Fetch All
.addCase(fetchAll.fulfilled, (state, action) => {
.addCase(addPlugins, (state, action: PayloadAction<CatalogPlugin[]>) => {
pluginsAdapter.upsertMany(state.items, action.payload);
})
// Fetch All local