Skip to content

Commit

Permalink
Add useDatasourceRequest hook (#69)
Browse files Browse the repository at this point in the history
* Add useDatasourceRequest hook

* Prepare components release 3.5.0

* Fix lint errors

* Update README.md

---------

Co-authored-by: Mikhail Volkov <[email protected]>
  • Loading branch information
asimonok and mikhail-vl authored Oct 23, 2024
1 parent 3f29c24 commit 5d8c53b
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 5 deletions.
6 changes: 6 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 5 additions & 4 deletions packages/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,5 @@
"typecheck": "tsc --emitDeclarationOnly false --noEmit"
},
"types": "dist/index.d.ts",
"version": "3.4.1"
"version": "3.5.0"
}
1 change: 1 addition & 0 deletions packages/components/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './useDashboardRefresh';
export * from './useDashboardTimeRange';
export * from './useDashboardVariables';
export * from './useData';
export * from './useDatasourceRequest';
export * from './useFormBuilder';
163 changes: 163 additions & 0 deletions packages/components/src/hooks/useDatasourceRequest.test.ts
Original file line number Diff line number Diff line change
@@ -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) =>

Check warning on line 13 in packages/components/src/hooks/useDatasourceRequest.test.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
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

Check warning on line 43 in packages/components/src/hooks/useDatasourceRequest.test.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
);
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

Check warning on line 95 in packages/components/src/hooks/useDatasourceRequest.test.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
);
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();
});
});
91 changes: 91 additions & 0 deletions packages/components/src/hooks/useDatasourceRequest.ts
Original file line number Diff line number Diff line change
@@ -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<DataQueryResponse> => {
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);
}
},
[]
);
};
2 changes: 2 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 5d8c53b

Please sign in to comment.