From 5d8c53bb2950e5af4a8d77ed4766df2b71145c0f Mon Sep 17 00:00:00 2001 From: Alex Simonok Date: Wed, 23 Oct 2024 15:52:53 +0300 Subject: [PATCH] Add useDatasourceRequest hook (#69) * Add useDatasourceRequest hook * Prepare components release 3.5.0 * Fix lint errors * Update README.md --------- Co-authored-by: Mikhail Volkov --- packages/components/CHANGELOG.md | 6 + packages/components/README.md | 9 +- packages/components/package.json | 2 +- packages/components/src/hooks/index.ts | 1 + .../src/hooks/useDatasourceRequest.test.ts | 163 ++++++++++++++++++ .../src/hooks/useDatasourceRequest.ts | 91 ++++++++++ packages/components/src/index.ts | 2 + 7 files changed, 269 insertions(+), 5 deletions(-) create mode 100644 packages/components/src/hooks/useDatasourceRequest.test.ts create mode 100644 packages/components/src/hooks/useDatasourceRequest.ts diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index c1c8e1a..4c9c902 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 3.5.0 (2024-10-23) + +### Features / Enhancements + +- Add useDatasourceRequest hooks (#69) + ## 3.4.1 (2024-10-22) ### Features / Enhancements diff --git a/packages/components/README.md b/packages/components/README.md index e35d93b..3458ba5 100644 --- a/packages/components/README.md +++ b/packages/components/README.md @@ -8,15 +8,16 @@ - `RangeSlider` allows to enter number range values by slider. - `Slider` allows to enter number values by slider and/or NumberInput. -### Hooks +## Hooks - `createUseDataHook` allows to create `useData` hook to get data through data source api. +- `useDashboardRefresh` allows to refresh dashboard. - `useDashboardTimeRange` allows to use actual dashboard time range. -- `useFormBuilder` allows to create declarative forms. - `useDashboardVariables` allows to use dashboard variables. -- `useDashboardRefresh` allows to refresh dashboard. +- `useDatasourceRequest` allows to run data source query. +- `useFormBuilder` allows to create declarative forms. -### Utils +## Utils - `CodeParametersBuilder` allows to create parameters for custom code. diff --git a/packages/components/package.json b/packages/components/package.json index 720668d..30b0ebf 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -89,5 +89,5 @@ "typecheck": "tsc --emitDeclarationOnly false --noEmit" }, "types": "dist/index.d.ts", - "version": "3.4.1" + "version": "3.5.0" } diff --git a/packages/components/src/hooks/index.ts b/packages/components/src/hooks/index.ts index 30718bf..9c69b48 100644 --- a/packages/components/src/hooks/index.ts +++ b/packages/components/src/hooks/index.ts @@ -2,4 +2,5 @@ export * from './useDashboardRefresh'; export * from './useDashboardTimeRange'; export * from './useDashboardVariables'; export * from './useData'; +export * from './useDatasourceRequest'; export * from './useFormBuilder'; diff --git a/packages/components/src/hooks/useDatasourceRequest.test.ts b/packages/components/src/hooks/useDatasourceRequest.test.ts new file mode 100644 index 0000000..0ee1aa8 --- /dev/null +++ b/packages/components/src/hooks/useDatasourceRequest.test.ts @@ -0,0 +1,163 @@ +import { LoadingState } from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { renderHook } from '@testing-library/react'; +import { Observable } from 'rxjs'; + +import { useDatasourceRequest } from './useDatasourceRequest'; + +/** + * Response + * + * @param response + */ +export const getResponse = (response: any) => + new Observable((subscriber) => { + subscriber.next(response); + subscriber.complete(); + }); + +/** + * Mock @grafana/runtime + */ +jest.mock('@grafana/runtime', () => ({ + getDataSourceSrv: jest.fn(), +})); + +describe('Use Datasource Request', () => { + it('Should run query', async () => { + const dataSourceSrv = { + query: jest.fn(() => + getResponse({ + data: { + message: 'hello', + }, + }) + ), + }; + const getDataSourceSrvMock = jest.fn(() => dataSourceSrv); + + jest.mocked(getDataSourceSrv).mockImplementationOnce( + () => + ({ + get: getDataSourceSrvMock, + }) as any + ); + const { result } = renderHook(() => useDatasourceRequest()); + + const response = await result.current({ + query: { + key1: 'value1', + key2: 'value2', + }, + datasource: 'abc', + replaceVariables: jest.fn((str) => str), + payload: {}, + }); + + /** + * Should get datasource + */ + expect(getDataSourceSrvMock).toHaveBeenCalledWith('abc'); + + /** + * Should pass query + */ + expect(dataSourceSrv.query).toHaveBeenCalledWith({ + targets: [{ key1: 'value1', key2: 'value2' }], + }); + + /** + * Should return result + */ + expect(response).toEqual({ + data: { + message: 'hello', + }, + }); + }); + + it('Should handle promise result query', async () => { + const dataSourceSrv = { + query: jest.fn(() => + Promise.resolve({ + data: { + message: 'hello', + }, + }) + ), + }; + const getDataSourceSrvMock = jest.fn(() => dataSourceSrv); + + jest.mocked(getDataSourceSrv).mockImplementationOnce( + () => + ({ + get: getDataSourceSrvMock, + }) as any + ); + const { result } = renderHook(() => useDatasourceRequest()); + + const response = await result.current({ + query: { + key1: 'value1', + key2: 'value2', + }, + datasource: 'abc', + replaceVariables: jest.fn((str) => str), + payload: {}, + }); + + /** + * Should get datasource + */ + expect(getDataSourceSrvMock).toHaveBeenCalledWith('abc'); + + /** + * Should pass query + */ + expect(dataSourceSrv.query).toHaveBeenCalledWith({ + targets: [{ key1: 'value1', key2: 'value2' }], + }); + + /** + * Should return result + */ + expect(response).toEqual({ + data: { + message: 'hello', + }, + }); + }); + + it('Should handle promise error', async () => { + const dataSourceSrv = { + query: jest.fn(() => + Promise.resolve({ + state: LoadingState.Error, + }) + ), + }; + const getDataSourceSrvMock = jest.fn(() => dataSourceSrv); + + jest.mocked(getDataSourceSrv).mockImplementationOnce( + () => + ({ + get: getDataSourceSrvMock, + }) as any + ); + const { result } = renderHook(() => useDatasourceRequest()); + + const response = await result + .current({ + query: { + key1: 'value1', + key2: 'value2', + }, + datasource: 'abc', + replaceVariables: jest.fn((str) => str), + payload: {}, + }) + .catch(() => false); + + expect(response).toBeFalsy(); + }); +}); diff --git a/packages/components/src/hooks/useDatasourceRequest.ts b/packages/components/src/hooks/useDatasourceRequest.ts new file mode 100644 index 0000000..c977fe0 --- /dev/null +++ b/packages/components/src/hooks/useDatasourceRequest.ts @@ -0,0 +1,91 @@ +import { DataQueryResponse, InterpolateFunction, LoadingState } from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { useCallback } from 'react'; +import { lastValueFrom } from 'rxjs'; + +/** + * Data Source Response Error + */ +export class DatasourceResponseError { + public readonly message: string; + + constructor( + public readonly error: unknown, + target: string + ) { + if (error && typeof error === 'object') { + if ('message' in error && typeof error.message === 'string') { + this.message = error.message; + } else { + this.message = JSON.stringify(error, null, 2); + } + } else { + this.message = 'Unknown Error'; + } + + this.message += `\nRequest: ${target}`; + } +} + +/** + * Use Data Source Request + */ +export const useDatasourceRequest = () => { + return useCallback( + async ({ + query, + datasource, + replaceVariables, + payload, + }: { + query: unknown; + datasource: string; + replaceVariables: InterpolateFunction; + payload: unknown; + }): Promise => { + const ds = await getDataSourceSrv().get(datasource); + + /** + * Replace Variables + */ + const targetJson = replaceVariables(JSON.stringify(query, null, 2), { + payload: { + value: payload, + }, + }); + + const target = JSON.parse(targetJson); + + try { + /** + * Response + */ + const response = ds.query({ + targets: [target], + } as never); + + const handleResponse = (response: DataQueryResponse) => { + if (response.state && response.state === LoadingState.Error) { + throw response?.errors?.[0] || response; + } + return response; + }; + + /** + * Handle as promise + */ + if (response instanceof Promise) { + return await response.then(handleResponse); + } + + /** + * Handle as observable + */ + return await lastValueFrom(response).then(handleResponse); + } catch (error) { + throw new DatasourceResponseError(error, targetJson); + } + }, + [] + ); +}; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 942696a..ed34ee3 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -3,9 +3,11 @@ import { TEST_IDS } from './constants'; export * from './components'; export { createUseDataHook, + DatasourceResponseError, useDashboardRefresh, useDashboardTimeRange, useDashboardVariables, + useDatasourceRequest, useFormBuilder, } from './hooks'; export * from './types';