diff --git a/packages/grafana-runtime/src/internal/index.ts b/packages/grafana-runtime/src/internal/index.ts index e7119717bf3..de669f7af74 100644 --- a/packages/grafana-runtime/src/internal/index.ts +++ b/packages/grafana-runtime/src/internal/index.ts @@ -25,3 +25,5 @@ export { setGetObservablePluginLinks, type GetObservablePluginLinks, } from '../services/pluginExtensions/getObservablePluginLinks'; + +export { UserStorage } from '../utils/userStorage'; diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts index c256f77baea..e249d727a68 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts @@ -37,6 +37,14 @@ class MyDataSource extends DataSourceWithBackend { applyTemplateVariables(query: MyQuery, scopedVars: ScopedVars, filters?: AdHocVariableFilter[] | undefined): MyQuery { return { ...query, applyTemplateVariablesCalled: true, filters }; } + + async getValue(key: string) { + return await this.userStorage.getItem(key); + } + + async setValue(key: string, value: string) { + await this.userStorage.setItem(key, value); + } } const mockDatasourceRequest = jest.fn, BackendSrvRequest[]>(); @@ -536,6 +544,15 @@ describe('DataSourceWithBackend', () => { expect(publicDashboardQueryHandler).toHaveBeenCalledWith(request); }); }); + + describe('user storage', () => { + test('sets and gets a value', async () => { + const { ds } = createMockDatasource(); + + await ds.setValue('multiplier', '1'); + expect(await ds.getValue('multiplier')).toBe('1'); + }); + }); }); function createMockDatasource() { diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index 3d5a32dc252..ab3a2b7670d 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -34,6 +34,7 @@ import { import { publicDashboardQueryHandler } from './publicDashboardQueryHandler'; import { BackendDataSourceResponse, toDataQueryResponse } from './queryResponse'; +import { UserStorage } from './userStorage'; /** * @internal @@ -121,8 +122,11 @@ class DataSourceWithBackend< TQuery extends DataQuery = DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData, > extends DataSourceApi { + protected userStorage: UserStorage; + constructor(instanceSettings: DataSourceInstanceSettings) { super(instanceSettings); + this.userStorage = new UserStorage(instanceSettings.type); } /** diff --git a/packages/grafana-runtime/src/utils/userStorage.test.tsx b/packages/grafana-runtime/src/utils/userStorage.test.tsx index 46999241686..91fe2374508 100644 --- a/packages/grafana-runtime/src/utils/userStorage.test.tsx +++ b/packages/grafana-runtime/src/utils/userStorage.test.tsx @@ -48,15 +48,15 @@ describe('userStorage', () => { it('use localStorage if the user is not logged in', async () => { config.bootData.user.isSignedIn = false; const storage = usePluginUserStorage(); - storage.getItem('key'); - expect(localStorage.getItem).toHaveBeenCalled(); + await storage.getItem('key'); + expect(localStorage.getItem).toHaveBeenCalledWith('plugin-id:abc:key'); }); it('use localStorage if the user storage is not found', async () => { request.mockReturnValue(Promise.reject({ status: 404 } as FetchError)); const storage = usePluginUserStorage(); await storage.getItem('key'); - expect(localStorage.getItem).toHaveBeenCalled(); + expect(localStorage.getItem).toHaveBeenCalledWith('plugin-id:abc:key'); }); it('returns the value from the user storage', async () => { @@ -73,8 +73,8 @@ describe('userStorage', () => { it('use localStorage if the user is not logged in', async () => { config.bootData.user.isSignedIn = false; const storage = usePluginUserStorage(); - storage.setItem('key', 'value'); - expect(localStorage.setItem).toHaveBeenCalled(); + await storage.setItem('key', 'value'); + expect(localStorage.setItem).toHaveBeenCalledWith('plugin-id:abc:key', 'value'); }); it('creates a new user storage if it does not exist', async () => { diff --git a/packages/grafana-runtime/src/utils/userStorage.tsx b/packages/grafana-runtime/src/utils/userStorage.tsx index 57ed8354404..245322e0a68 100644 --- a/packages/grafana-runtime/src/utils/userStorage.tsx +++ b/packages/grafana-runtime/src/utils/userStorage.tsx @@ -37,9 +37,9 @@ async function apiRequest(requestOptions: RequestOptions) { /** * A class for interacting with the backend user storage. - * Unexported because it is currently only be used through the useUserStorage hook. + * Exposed internally only to avoid misuse (wrong service name).. */ -class UserStorage { +export class UserStorage { private service: string; private resourceName: string; private userUID: string; @@ -76,13 +76,13 @@ class UserStorage { async getItem(key: string): Promise { if (!this.canUseUserStorage) { // Fallback to localStorage - return localStorage.getItem(this.resourceName); + return localStorage.getItem(`${this.resourceName}:${key}`); } // Ensure this.storageSpec is initialized await this.init(); if (!this.storageSpec) { // Also, fallback to localStorage for backward compatibility - return localStorage.getItem(this.resourceName); + return localStorage.getItem(`${this.resourceName}:${key}`); } return this.storageSpec.data[key]; } @@ -90,7 +90,7 @@ class UserStorage { async setItem(key: string, value: string): Promise { if (!this.canUseUserStorage) { // Fallback to localStorage - localStorage.setItem(key, value); + localStorage.setItem(`${this.resourceName}:${key}`, value); return; }