diff --git a/package-lock.json b/package-lock.json index 43543c4..3216d62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3166,6 +3166,29 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, + "node_modules/@grafana/scenes": { + "version": "5.20.2", + "resolved": "https://registry.npmjs.org/@grafana/scenes/-/scenes-5.20.2.tgz", + "integrity": "sha512-WTur5FhF4mYTUsi31l+vppQIL/BSf3umPVKa8labv/2udrGbtjXYElqv4dbjRnpUCaaOt780YsVCaRP+6JzV3A==", + "dependencies": { + "@floating-ui/react": "0.26.16", + "@grafana/e2e-selectors": "^11.0.0", + "@leeoniya/ufuzzy": "^1.0.14", + "@tanstack/react-virtual": "^3.9.0", + "react-grid-layout": "1.3.4", + "react-use": "17.5.0", + "react-virtualized-auto-sizer": "1.0.24", + "uuid": "^9.0.0" + }, + "peerDependencies": { + "@grafana/data": ">=10.4", + "@grafana/runtime": ">=10.4", + "@grafana/schema": ">=10.4", + "@grafana/ui": ">=10.4", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@grafana/schema": { "version": "10.4.2", "resolved": "https://registry.npmjs.org/@grafana/schema/-/schema-10.4.2.tgz", @@ -7597,6 +7620,31 @@ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==" }, + "node_modules/@tanstack/react-virtual": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz", + "integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==", + "dependencies": { + "@tanstack/virtual-core": "3.10.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.10.8", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz", + "integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -16731,6 +16779,11 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -19040,6 +19093,27 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-draggable/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-dropzone": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", @@ -19094,6 +19168,30 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-grid-layout": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.3.4.tgz", + "integrity": "sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw==", + "dependencies": { + "clsx": "^1.1.1", + "lodash.isequal": "^4.0.0", + "prop-types": "^15.8.1", + "react-draggable": "^4.0.0", + "react-resizable": "^3.0.4" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-grid-layout/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-highlight-words": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/react-highlight-words/-/react-highlight-words-0.20.0.tgz", @@ -19278,6 +19376,18 @@ } } }, + "node_modules/react-resizable": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", + "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + }, + "peerDependencies": { + "react": ">= 16.3" + } + }, "node_modules/react-router": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz", @@ -19443,6 +19553,15 @@ "react-dom": "*" } }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz", + "integrity": "sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-window": { "version": "1.8.10", "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", @@ -22943,12 +23062,13 @@ }, "packages/components": { "name": "@volkovlabs/components", - "version": "2.9.1", + "version": "3.3.0", "license": "Apache-2.0", "dependencies": { "@emotion/css": "^11.11.2", "@grafana/data": "^11.1.0", "@grafana/runtime": "^11.1.0", + "@grafana/scenes": "^5.20.2", "@grafana/ui": "^11.1.0", "@volkovlabs/jest-selectors": "^1.5.0", "classnames": "^2.5.1", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index ddaff19..c1c8e1a 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 3.4.1 (2024-10-22) + +### Features / Enhancements + +- Add useDashboardVariables and useDashboardRefresh hooks (#68) + ## 3.3.0 (2024-10-02) ### Features / Enhancements diff --git a/packages/components/README.md b/packages/components/README.md index 98f4a87..e35d93b 100644 --- a/packages/components/README.md +++ b/packages/components/README.md @@ -13,6 +13,8 @@ - `createUseDataHook` allows to create `useData` hook to get data through data source api. - `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. ### Utils diff --git a/packages/components/package.json b/packages/components/package.json index 5d01f3d..720668d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -4,6 +4,7 @@ "@emotion/css": "^11.11.2", "@grafana/data": "^11.1.0", "@grafana/runtime": "^11.1.0", + "@grafana/scenes": "^5.20.2", "@grafana/ui": "^11.1.0", "@volkovlabs/jest-selectors": "^1.5.0", "classnames": "^2.5.1", @@ -88,5 +89,5 @@ "typecheck": "tsc --emitDeclarationOnly false --noEmit" }, "types": "dist/index.d.ts", - "version": "3.3.0" + "version": "3.4.1" } diff --git a/packages/components/rollup.config.mjs b/packages/components/rollup.config.mjs index c590d81..dbab90e 100644 --- a/packages/components/rollup.config.mjs +++ b/packages/components/rollup.config.mjs @@ -25,6 +25,7 @@ export default [ 'rc-tooltip', '@grafana/data', '@grafana/runtime', + '@grafana/scenes', 'lodash', 'rc-slider/assets/index.css', '@volkovlabs/jest-selectors', @@ -46,6 +47,7 @@ export default [ 'rc-tooltip', '@grafana/data', '@grafana/runtime', + '@grafana/scenes', 'lodash', 'rc-slider/assets/index.css', '@volkovlabs/jest-selectors', diff --git a/packages/components/src/@types/global.d.ts b/packages/components/src/@types/global.d.ts new file mode 100644 index 0000000..9468e70 --- /dev/null +++ b/packages/components/src/@types/global.d.ts @@ -0,0 +1,11 @@ +import { SceneObject } from '@grafana/scenes'; + +/** + * __grafanaSceneContext contains Dashboard Scene Object if scene enabled + */ +declare global { + interface Window { + // eslint-disable-next-line @typescript-eslint/naming-convention + __grafanaSceneContext?: SceneObject; + } +} diff --git a/packages/components/src/__mocks__/@grafana/scenes.ts b/packages/components/src/__mocks__/@grafana/scenes.ts new file mode 100644 index 0000000..360480e --- /dev/null +++ b/packages/components/src/__mocks__/@grafana/scenes.ts @@ -0,0 +1,9 @@ +/** + * Mock @grafana/scenes + * mostly prevent IntersectionObserver is not defined + */ +jest.mock('@grafana/scenes', () => ({ + sceneGraph: { + getVariables: jest.fn(), + }, +})); diff --git a/packages/components/src/hooks/index.ts b/packages/components/src/hooks/index.ts index dc5305e..30718bf 100644 --- a/packages/components/src/hooks/index.ts +++ b/packages/components/src/hooks/index.ts @@ -1,3 +1,5 @@ +export * from './useDashboardRefresh'; export * from './useDashboardTimeRange'; +export * from './useDashboardVariables'; export * from './useData'; export * from './useFormBuilder'; diff --git a/packages/components/src/hooks/useDashboardRefresh.test.ts b/packages/components/src/hooks/useDashboardRefresh.test.ts new file mode 100644 index 0000000..ae27405 --- /dev/null +++ b/packages/components/src/hooks/useDashboardRefresh.test.ts @@ -0,0 +1,80 @@ +import { EventBusSrv } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; +import { sceneGraph } from '@grafana/scenes'; +import { renderHook } from '@testing-library/react'; + +import { useDashboardRefresh } from './useDashboardRefresh'; + +/** + * Mock @grafana/scenes + */ +jest.mock('@grafana/scenes', () => ({ + sceneGraph: { + getTimeRange: jest.fn(), + }, +})); + +/** + * Mock @grafana/runtime + */ +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getAppEvents: jest.fn(), +})); + +describe('useDashboardRefresh', () => { + /** + * App Events + */ + const appEvents = new EventBusSrv(); + + /** + * Enable Scene + */ + const enableScene = () => { + Object.defineProperty(global, '__grafanaSceneContext', { + writable: true, + value: {}, + }); + }; + + beforeEach(() => { + jest.mocked(getAppEvents).mockReturnValue(appEvents); + }); + + afterEach(() => { + appEvents.removeAllListeners(); + Object.defineProperty(global, '__grafanaSceneContext', { + writable: true, + value: undefined, + }); + }); + + it('Should refresh non scene dashboard', () => { + const { result } = renderHook(() => useDashboardRefresh()); + + let refreshEvent; + + appEvents.getStream({ type: 'variables-changed' } as never).subscribe((event) => (refreshEvent = event)); + + result.current(); + + expect(refreshEvent).toEqual({ + type: 'variables-changed', + payload: { refreshAll: true }, + }); + }); + + it('Should refresh scene dashboard', () => { + enableScene(); + + const onRefresh = jest.fn(); + jest.mocked(sceneGraph.getTimeRange).mockReturnValue({ onRefresh } as never); + + const { result } = renderHook(() => useDashboardRefresh()); + + result.current(); + + expect(onRefresh).toHaveBeenCalled(); + }); +}); diff --git a/packages/components/src/hooks/useDashboardRefresh.ts b/packages/components/src/hooks/useDashboardRefresh.ts new file mode 100644 index 0000000..1f63ff1 --- /dev/null +++ b/packages/components/src/hooks/useDashboardRefresh.ts @@ -0,0 +1,22 @@ +import { getAppEvents } from '@grafana/runtime'; +import { sceneGraph } from '@grafana/scenes'; +import { useCallback } from 'react'; + +/** + * Use Dashboard Refresh + */ +export const useDashboardRefresh = () => { + return useCallback(() => { + /** + * Refresh on scene dashboard + */ + if (window.__grafanaSceneContext) { + return sceneGraph.getTimeRange(window.__grafanaSceneContext)?.onRefresh(); + } + + /** + * Refresh dashboard + */ + return getAppEvents().publish({ type: 'variables-changed', payload: { refreshAll: true } }); + }, []); +}; diff --git a/packages/components/src/hooks/useDashboardVariables.test.ts b/packages/components/src/hooks/useDashboardVariables.test.ts new file mode 100644 index 0000000..c5bc335 --- /dev/null +++ b/packages/components/src/hooks/useDashboardVariables.test.ts @@ -0,0 +1,291 @@ +import { EventBusSrv } from '@grafana/data'; +import { RefreshEvent } from '@grafana/runtime'; +import { act, renderHook } from '@testing-library/react'; + +import { useDashboardVariables } from './useDashboardVariables'; + +/** + * Variable + */ +interface Variable { + /** + * Name + * + * @type {string} + */ + name: string; +} + +/** + * Mock @grafana/runtime + */ +const getVariablesMock = jest.fn((): Variable[] => []); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getTemplateSrv: jest.fn(() => ({ + getVariables: getVariablesMock, + })), +})); + +/** + * Mock Timers + */ +jest.useFakeTimers(); + +describe('Use Dashboard Variables', () => { + /** + * Event Bus + */ + const eventBus = new EventBusSrv(); + + /** + * Create Variable + */ + const createVariable = (item: Partial): Variable => ({ + name: '', + ...item, + }); + + beforeEach(() => { + /** + * Mock Variables + */ + getVariablesMock.mockReset(); + getVariablesMock.mockReturnValue([]); + }); + + it('Should return variable', () => { + /** + * Mock Variables + */ + getVariablesMock.mockReturnValue([createVariable({ name: 'device' })]); + + const { result } = renderHook(() => + useDashboardVariables({ + eventBus, + variableName: 'device', + getOne: (state, variableName) => state.find((item) => item.name === variableName), + toState: (variables) => variables, + initial: [], + }) + ); + + expect(result.current.variable).toEqual( + expect.objectContaining({ + name: 'device', + }) + ); + }); + + it('Should return variable by name', () => { + /** + * Mock Variables + */ + getVariablesMock.mockReturnValue([createVariable({ name: 'device' }), createVariable({ name: 'country' })]); + + const { result } = renderHook(() => + useDashboardVariables({ + eventBus, + variableName: 'device', + getOne: (state, variableName) => state.find((item) => item.name === variableName), + toState: (variables) => variables, + initial: [], + }) + ); + + expect(result.current.getVariable('device')).toEqual( + expect.objectContaining({ + name: 'device', + }) + ); + expect(result.current.getVariable('country')).toEqual( + expect.objectContaining({ + name: 'country', + }) + ); + }); + + it('Should update state on refresh event', async () => { + /** + * Mock Variables + */ + getVariablesMock.mockReturnValue([createVariable({ name: 'device' })]); + + const { result } = renderHook(() => + useDashboardVariables({ + eventBus, + variableName: 'device', + getOne: (state, variableName) => state.find((item) => item.name === variableName), + toState: (variables) => variables, + initial: [], + }) + ); + + expect(result.current.getVariable('device')).toEqual( + expect.objectContaining({ + name: 'device', + }) + ); + + /** + * Mock Variables + */ + getVariablesMock.mockReturnValue([]); + + /** + * Publish Refresh + */ + await act(async () => eventBus.publish(new RefreshEvent())); + + /** + * Check Updated State + */ + expect(result.current.getVariable('device')).not.toBeDefined(); + }); + + describe('Scene', () => { + /** + * Create Use State Result + * @param variables + */ + const createUseStateResult = (variables: Array<{ state: { loading: boolean } }>) => ({ + $variables: { + state: { + variables, + }, + }, + }); + + const useStateMock = jest.fn(() => createUseStateResult([])); + + beforeEach(() => { + useStateMock.mockReturnValue(createUseStateResult([])); + + Object.defineProperty(global, '__grafanaSceneContext', { + writable: true, + value: { + useState: useStateMock, + }, + }); + }); + + afterAll(() => { + Object.defineProperty(global, '__grafanaSceneContext', { + writable: true, + value: undefined, + }); + }); + + it('Should update state after all vars loaded', async () => { + /** + * Mock Variables + */ + getVariablesMock.mockReturnValue([]); + + /** + * Mock Scene Variables + */ + useStateMock.mockReturnValue(createUseStateResult([{ state: { loading: true } }])); + + const { result } = renderHook(() => + useDashboardVariables({ + eventBus, + variableName: 'device', + getOne: (state, variableName) => state.find((item) => item.name === variableName), + toState: (variables) => variables, + initial: [], + refreshCheckCount: 3, + }) + ); + + expect(result.current.variable).not.toBeDefined(); + + /** + * Mock Variables + */ + getVariablesMock.mockReturnValue([{ name: 'device' }]); + + /** + * Run Check Timer + */ + await act(async () => jest.runOnlyPendingTimersAsync()); + + /** + * Check if state is not updated + */ + expect(result.current.variable).not.toBeDefined(); + + /** + * Mock Scene Variables + */ + useStateMock.mockReturnValue(createUseStateResult([{ state: { loading: false } }])); + + /** + * Run Check Timer + */ + await act(async () => jest.runOnlyPendingTimersAsync()); + + /** + * Check if state updated + */ + expect(result.current.variable).toEqual( + expect.objectContaining({ + name: 'device', + }) + ); + }); + + it('Should stop checking if check count exceeded', async () => { + /** + * Mock Variables + */ + getVariablesMock.mockReturnValue([]); + + /** + * Mock Scene Variables + */ + useStateMock.mockReturnValue(createUseStateResult([{ state: { loading: true } }])); + + const { result } = renderHook(() => + useDashboardVariables({ + eventBus, + variableName: 'device', + getOne: (state, variableName) => state.find((item) => item.name === variableName), + toState: (variables) => variables, + initial: [], + refreshCheckCount: 2, + }) + ); + + expect(result.current.variable).not.toBeDefined(); + + /** + * Mock Variables + */ + getVariablesMock.mockReturnValue([{ name: 'device' }]); + + /** + * Run Check Timer + */ + await act(async () => jest.runOnlyPendingTimersAsync()); + await act(async () => jest.runOnlyPendingTimersAsync()); + await act(async () => jest.runOnlyPendingTimersAsync()); + + /** + * Mock Scene Variables + */ + useStateMock.mockReturnValue(createUseStateResult([{ state: { loading: false } }])); + + /** + * Run Check Timer + */ + await act(async () => jest.runOnlyPendingTimersAsync()); + + /** + * Check if state updated + */ + expect(result.current.variable).not.toBeDefined(); + }); + }); +}); diff --git a/packages/components/src/hooks/useDashboardVariables.ts b/packages/components/src/hooks/useDashboardVariables.ts new file mode 100644 index 0000000..30fcad2 --- /dev/null +++ b/packages/components/src/hooks/useDashboardVariables.ts @@ -0,0 +1,114 @@ +import { EventBus, TypedVariableModel } from '@grafana/data'; +import { getTemplateSrv, RefreshEvent } from '@grafana/runtime'; +import { useCallback, useEffect, useReducer, useRef, useState } from 'react'; + +/** + * Use Dashboard Variables + */ +export const useDashboardVariables = ({ + eventBus, + variableName, + refreshCheckCount = 5, + refreshCheckInterval = 500, + getOne, + toState, + initial, +}: { + eventBus: EventBus; + variableName: string; + refreshCheckCount?: number; + refreshCheckInterval?: number; + initial: TState; + getOne: (state: TState, variableName: string) => TVariable | undefined; + toState: (variables: TypedVariableModel[]) => TState; +}) => { + /** + * Memoize functions + */ + const cachedFunctions = useRef({ + getOne, + toState, + }); + + /** + * State + */ + const [variables, setVariables] = useState(initial); + const [variable, setVariable] = useState(); + + /** + * For Scene Dashboard + */ + const sceneObjectState = window.__grafanaSceneContext?.useState(); + const [refreshCount, forceUpdate] = useReducer((x) => x + 1, 0); + const checkLoadingStateTimer = useRef | null>(null); + + /** + * Sometimes Variables have Loading State in Dashboard Scene + * So just to refresh until loaded state or counter exceeded + */ + useEffect(() => { + /** + * Non-scene dashboard so skip checking + */ + if (!sceneObjectState?.$variables?.state.variables || refreshCount >= refreshCheckCount) { + return; + } + + const variables = sceneObjectState.$variables?.state.variables; + const isLoading = variables?.some((variable) => variable?.state.loading); + + const clearTimer = () => { + if (checkLoadingStateTimer.current) { + clearTimeout(checkLoadingStateTimer.current); + checkLoadingStateTimer.current = null; + } + }; + + if (isLoading) { + checkLoadingStateTimer.current = setTimeout(() => { + forceUpdate(); + }, refreshCheckInterval); + } else { + clearTimer(); + setVariables(cachedFunctions.current.toState(getTemplateSrv().getVariables())); + } + + return () => { + clearTimer(); + }; + }, [sceneObjectState?.$variables?.state.variables, refreshCount]); + + /** + * Load Variables + */ + useEffect(() => { + setVariables(cachedFunctions.current.toState(getTemplateSrv().getVariables())); + + /** + * Update variable on Refresh + */ + const subscriber = eventBus.getStream(RefreshEvent).subscribe(() => { + setVariables(cachedFunctions.current.toState(getTemplateSrv().getVariables())); + }); + + return () => { + subscriber.unsubscribe(); + }; + }, [eventBus]); + + const getVariable = useCallback( + (variableName: string) => cachedFunctions.current.getOne(variables, variableName), + [variables] + ); + + useEffect(() => { + setVariable(getVariable(variableName)); + }, [getVariable, variableName]); + + return { + variable, + getVariable, + variables, + }; +}; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 42bda7c..942696a 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1,7 +1,13 @@ import { TEST_IDS } from './constants'; export * from './components'; -export { createUseDataHook, useDashboardTimeRange, useFormBuilder } from './hooks'; +export { + createUseDataHook, + useDashboardRefresh, + useDashboardTimeRange, + useDashboardVariables, + useFormBuilder, +} from './hooks'; export * from './types'; export { CodeParameterItem, CodeParametersBuilder, FormBuilder } from './utils';