mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Variables: enables cancel for slow query variables queries (#24430)
* Refactor: initial commit * Tests: updates tests * Tests: updates snapshots * Chore: updates after PR comments * Chore: renamed initVariablesBatch * Tests: adds transactionReducer tests * Chore: updates after PR comments * Refactor: renames cancelAllDataSourceRequests * Refactor: reduces cancellation complexity * Tests: adds tests for cancelAllInFlightRequests * Tests: adds initVariablesTransaction tests * Tests: adds tests for cleanUpVariables and cancelVariables * Always cleanup dashboard on unmount, even if init is in progress. Check if init phase has changed after services init is completed * fixed failing tests and added some more to test new scenario. Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
This commit is contained in:
parent
0f5b894256
commit
e65dbcfea1
@ -53,6 +53,8 @@ enum CancellationType {
|
|||||||
dataSourceRequest,
|
dataSourceRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CANCEL_ALL_REQUESTS_REQUEST_ID = 'cancel_all_requests_request_id';
|
||||||
|
|
||||||
export interface BackendSrvDependencies {
|
export interface BackendSrvDependencies {
|
||||||
fromFetch: (input: string | Request, init?: RequestInit) => Observable<Response>;
|
fromFetch: (input: string | Request, init?: RequestInit) => Observable<Response>;
|
||||||
appEvents: Emitter;
|
appEvents: Emitter;
|
||||||
@ -182,6 +184,10 @@ export class BackendSrv implements BackendService {
|
|||||||
this.inFlightRequests.next(requestId);
|
this.inFlightRequests.next(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelAllInFlightRequests() {
|
||||||
|
this.inFlightRequests.next(CANCEL_ALL_REQUESTS_REQUEST_ID);
|
||||||
|
}
|
||||||
|
|
||||||
async datasourceRequest(options: BackendSrvRequest): Promise<any> {
|
async datasourceRequest(options: BackendSrvRequest): Promise<any> {
|
||||||
// A requestId is provided by the datasource as a unique identifier for a
|
// A requestId is provided by the datasource as a unique identifier for a
|
||||||
// particular query. Every observable below has a takeUntil that subscribes to this.inFlightRequests and
|
// particular query. Every observable below has a takeUntil that subscribes to this.inFlightRequests and
|
||||||
@ -528,12 +534,18 @@ export class BackendSrv implements BackendService {
|
|||||||
this.inFlightRequests.pipe(
|
this.inFlightRequests.pipe(
|
||||||
filter(requestId => {
|
filter(requestId => {
|
||||||
let cancelRequest = false;
|
let cancelRequest = false;
|
||||||
|
|
||||||
if (options && options.requestId && options.requestId === requestId) {
|
if (options && options.requestId && options.requestId === requestId) {
|
||||||
// when a new requestId is started it will be published to inFlightRequests
|
// when a new requestId is started it will be published to inFlightRequests
|
||||||
// if a previous long running request that hasn't finished yet has the same requestId
|
// if a previous long running request that hasn't finished yet has the same requestId
|
||||||
// we need to cancel that request
|
// we need to cancel that request
|
||||||
cancelRequest = true;
|
cancelRequest = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requestId === CANCEL_ALL_REQUESTS_REQUEST_ID) {
|
||||||
|
cancelRequest = true;
|
||||||
|
}
|
||||||
|
|
||||||
return cancelRequest;
|
return cancelRequest;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -7,6 +7,7 @@ import { BackendSrv, getBackendSrv } from '../services/backend_srv';
|
|||||||
import { Emitter } from '../utils/emitter';
|
import { Emitter } from '../utils/emitter';
|
||||||
import { ContextSrv, User } from '../services/context_srv';
|
import { ContextSrv, User } from '../services/context_srv';
|
||||||
import { CoreEvents } from '../../types';
|
import { CoreEvents } from '../../types';
|
||||||
|
import { describe, expect } from '../../../test/lib/common';
|
||||||
|
|
||||||
const getTestContext = (overides?: object) => {
|
const getTestContext = (overides?: object) => {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
@ -571,4 +572,87 @@ describe('backendSrv', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cancelAllInFlightRequests', () => {
|
||||||
|
describe('when called with 2 separate requests and then cancelAllInFlightRequests is called', () => {
|
||||||
|
enum RequestType {
|
||||||
|
request,
|
||||||
|
dataSourceRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = '/api/dashboard/';
|
||||||
|
const options = {
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataSourceRequestResult = {
|
||||||
|
data: ([] as unknown[]) as any[],
|
||||||
|
status: -1,
|
||||||
|
statusText: 'Request was aborted',
|
||||||
|
config: options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRequestObservable = (message: string, unsubscribe: any) =>
|
||||||
|
new Observable(subscriber => {
|
||||||
|
subscriber.next({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
statusText: 'Ok',
|
||||||
|
text: () => Promise.resolve(JSON.stringify({ message })),
|
||||||
|
headers: {
|
||||||
|
map: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redirected: false,
|
||||||
|
type: 'basic',
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
return unsubscribe;
|
||||||
|
}).pipe(delay(10000));
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
firstRequestType | secondRequestType | firstRequestResult | secondRequestResult
|
||||||
|
${RequestType.request} | ${RequestType.request} | ${[]} | ${[]}
|
||||||
|
${RequestType.dataSourceRequest} | ${RequestType.dataSourceRequest} | ${dataSourceRequestResult} | ${dataSourceRequestResult}
|
||||||
|
${RequestType.request} | ${RequestType.dataSourceRequest} | ${[]} | ${dataSourceRequestResult}
|
||||||
|
${RequestType.dataSourceRequest} | ${RequestType.request} | ${dataSourceRequestResult} | ${[]}
|
||||||
|
`(
|
||||||
|
'then it both requests should be cancelled and unsubscribed',
|
||||||
|
async ({ firstRequestType, secondRequestType, firstRequestResult, secondRequestResult }) => {
|
||||||
|
const unsubscribe = jest.fn();
|
||||||
|
const { backendSrv, fromFetchMock } = getTestContext({ url });
|
||||||
|
const firstObservable = getRequestObservable('First', unsubscribe);
|
||||||
|
const secondObservable = getRequestObservable('Second', unsubscribe);
|
||||||
|
|
||||||
|
fromFetchMock.mockImplementationOnce(() => firstObservable);
|
||||||
|
fromFetchMock.mockImplementation(() => secondObservable);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstRequest =
|
||||||
|
firstRequestType === RequestType.request
|
||||||
|
? backendSrv.request(options)
|
||||||
|
: backendSrv.datasourceRequest(options);
|
||||||
|
|
||||||
|
const secondRequest =
|
||||||
|
secondRequestType === RequestType.request
|
||||||
|
? backendSrv.request(options)
|
||||||
|
: backendSrv.datasourceRequest(options);
|
||||||
|
|
||||||
|
backendSrv.cancelAllInFlightRequests();
|
||||||
|
|
||||||
|
const result = await Promise.all([firstRequest, secondRequest]);
|
||||||
|
|
||||||
|
expect(result[0]).toEqual(firstRequestResult);
|
||||||
|
expect(result[1]).toEqual(secondRequestResult);
|
||||||
|
expect(unsubscribe).toHaveBeenCalledTimes(2);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,19 +2,15 @@ import React from 'react';
|
|||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { DashboardPage, mapStateToProps, Props, State } from './DashboardPage';
|
import { DashboardPage, mapStateToProps, Props, State } from './DashboardPage';
|
||||||
import { DashboardModel } from '../state';
|
import { DashboardModel } from '../state';
|
||||||
import { cleanUpDashboard } from '../state/reducers';
|
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||||
import {
|
|
||||||
mockToolkitActionCreator,
|
|
||||||
mockToolkitActionCreatorWithoutPayload,
|
|
||||||
ToolkitActionCreatorWithoutPayloadMockType,
|
|
||||||
} from 'test/core/redux/mocks';
|
|
||||||
import { DashboardInitPhase, DashboardRouteInfo } from 'app/types';
|
import { DashboardInitPhase, DashboardRouteInfo } from 'app/types';
|
||||||
import { notifyApp, updateLocation } from 'app/core/actions';
|
import { notifyApp, updateLocation } from 'app/core/actions';
|
||||||
|
import { cleanUpDashboardAndVariables } from '../state/actions';
|
||||||
|
|
||||||
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
|
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
|
||||||
|
|
||||||
interface ScenarioContext {
|
interface ScenarioContext {
|
||||||
cleanUpDashboardMock: ToolkitActionCreatorWithoutPayloadMockType;
|
cleanUpDashboardAndVariablesMock: typeof cleanUpDashboardAndVariables;
|
||||||
dashboard?: DashboardModel | null;
|
dashboard?: DashboardModel | null;
|
||||||
setDashboardProp: (overrides?: any, metaOverrides?: any) => void;
|
setDashboardProp: (overrides?: any, metaOverrides?: any) => void;
|
||||||
wrapper?: ShallowWrapper<Props, State, DashboardPage>;
|
wrapper?: ShallowWrapper<Props, State, DashboardPage>;
|
||||||
@ -47,7 +43,7 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
|
|||||||
let setupFn: () => void;
|
let setupFn: () => void;
|
||||||
|
|
||||||
const ctx: ScenarioContext = {
|
const ctx: ScenarioContext = {
|
||||||
cleanUpDashboardMock: mockToolkitActionCreatorWithoutPayload(cleanUpDashboard),
|
cleanUpDashboardAndVariablesMock: jest.fn(),
|
||||||
setup: fn => {
|
setup: fn => {
|
||||||
setupFn = fn;
|
setupFn = fn;
|
||||||
},
|
},
|
||||||
@ -67,7 +63,8 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
|
|||||||
initDashboard: jest.fn(),
|
initDashboard: jest.fn(),
|
||||||
updateLocation: mockToolkitActionCreator(updateLocation),
|
updateLocation: mockToolkitActionCreator(updateLocation),
|
||||||
notifyApp: mockToolkitActionCreator(notifyApp),
|
notifyApp: mockToolkitActionCreator(notifyApp),
|
||||||
cleanUpDashboard: ctx.cleanUpDashboardMock,
|
cleanUpDashboardAndVariables: ctx.cleanUpDashboardAndVariablesMock,
|
||||||
|
cancelVariables: jest.fn(),
|
||||||
dashboard: null,
|
dashboard: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -233,7 +230,7 @@ describe('DashboardPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should call clean up action', () => {
|
it('Should call clean up action', () => {
|
||||||
expect(ctx.cleanUpDashboardMock).toHaveBeenCalledTimes(1);
|
expect(ctx.cleanUpDashboardAndVariablesMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -8,16 +8,14 @@ import classNames from 'classnames';
|
|||||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||||
import { getMessageFromError } from 'app/core/utils/errors';
|
import { getMessageFromError } from 'app/core/utils/errors';
|
||||||
import { Branding } from 'app/core/components/Branding/Branding';
|
import { Branding } from 'app/core/components/Branding/Branding';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { DashboardGrid } from '../dashgrid/DashboardGrid';
|
import { DashboardGrid } from '../dashgrid/DashboardGrid';
|
||||||
import { DashNav } from '../components/DashNav';
|
import { DashNav } from '../components/DashNav';
|
||||||
import { DashboardSettings } from '../components/DashboardSettings';
|
import { DashboardSettings } from '../components/DashboardSettings';
|
||||||
import { PanelEditor } from '../components/PanelEditor/PanelEditor';
|
import { PanelEditor } from '../components/PanelEditor/PanelEditor';
|
||||||
import { Alert, CustomScrollbar, Icon } from '@grafana/ui';
|
import { Alert, Button, CustomScrollbar, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
|
||||||
// Redux
|
// Redux
|
||||||
import { initDashboard } from '../state/initDashboard';
|
import { initDashboard } from '../state/initDashboard';
|
||||||
import { cleanUpDashboard } from '../state/reducers';
|
|
||||||
import { notifyApp, updateLocation } from 'app/core/actions';
|
import { notifyApp, updateLocation } from 'app/core/actions';
|
||||||
// Types
|
// Types
|
||||||
import {
|
import {
|
||||||
@ -32,6 +30,8 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
|||||||
import { InspectTab, PanelInspector } from '../components/Inspector/PanelInspector';
|
import { InspectTab, PanelInspector } from '../components/Inspector/PanelInspector';
|
||||||
import { getConfig } from '../../../core/config';
|
import { getConfig } from '../../../core/config';
|
||||||
import { SubMenu } from '../components/SubMenu/SubMenu';
|
import { SubMenu } from '../components/SubMenu/SubMenu';
|
||||||
|
import { cleanUpDashboardAndVariables } from '../state/actions';
|
||||||
|
import { cancelVariables } from '../../variables/state/actions';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
urlUid?: string;
|
urlUid?: string;
|
||||||
@ -51,11 +51,12 @@ export interface Props {
|
|||||||
dashboard: DashboardModel | null;
|
dashboard: DashboardModel | null;
|
||||||
initError?: DashboardInitError;
|
initError?: DashboardInitError;
|
||||||
initDashboard: typeof initDashboard;
|
initDashboard: typeof initDashboard;
|
||||||
cleanUpDashboard: typeof cleanUpDashboard;
|
cleanUpDashboardAndVariables: typeof cleanUpDashboardAndVariables;
|
||||||
notifyApp: typeof notifyApp;
|
notifyApp: typeof notifyApp;
|
||||||
updateLocation: typeof updateLocation;
|
updateLocation: typeof updateLocation;
|
||||||
inspectTab?: InspectTab;
|
inspectTab?: InspectTab;
|
||||||
isPanelEditorOpen?: boolean;
|
isPanelEditorOpen?: boolean;
|
||||||
|
cancelVariables: typeof cancelVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
@ -90,10 +91,8 @@ export class DashboardPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.props.dashboard) {
|
this.props.cleanUpDashboardAndVariables();
|
||||||
this.props.cleanUpDashboard();
|
this.setPanelFullscreenClass(false);
|
||||||
this.setPanelFullscreenClass(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
@ -210,11 +209,24 @@ export class DashboardPage extends PureComponent<Props, State> {
|
|||||||
this.setState({ updateScrollTop: 0 });
|
this.setState({ updateScrollTop: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cancelVariables = () => {
|
||||||
|
this.props.updateLocation({ path: '/' });
|
||||||
|
};
|
||||||
|
|
||||||
renderSlowInitState() {
|
renderSlowInitState() {
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-loading">
|
<div className="dashboard-loading">
|
||||||
<div className="dashboard-loading__text">
|
<div className="dashboard-loading__text">
|
||||||
<Icon name="fa fa-spinner" className="fa-spin" /> {this.props.initPhase}
|
<VerticalGroup spacing="md">
|
||||||
|
<HorizontalGroup align="center" justify="center" spacing="xs">
|
||||||
|
<Icon name="fa fa-spinner" className="fa-spin" /> {this.props.initPhase}
|
||||||
|
</HorizontalGroup>{' '}
|
||||||
|
<HorizontalGroup align="center" justify="center">
|
||||||
|
<Button variant="secondary" size="md" icon="repeat" onClick={this.cancelVariables}>
|
||||||
|
Cancel loading dashboard
|
||||||
|
</Button>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</VerticalGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -336,9 +348,10 @@ export const mapStateToProps = (state: StoreState) => ({
|
|||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
initDashboard,
|
initDashboard,
|
||||||
cleanUpDashboard,
|
cleanUpDashboardAndVariables,
|
||||||
notifyApp,
|
notifyApp,
|
||||||
updateLocation,
|
updateLocation,
|
||||||
|
cancelVariables,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage));
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage));
|
||||||
|
@ -320,12 +320,36 @@ exports[`DashboardPage Dashboard is fetching slowly Should render slow init stat
|
|||||||
<div
|
<div
|
||||||
className="dashboard-loading__text"
|
className="dashboard-loading__text"
|
||||||
>
|
>
|
||||||
<Icon
|
<Component
|
||||||
className="fa-spin"
|
spacing="md"
|
||||||
name="fa fa-spinner"
|
>
|
||||||
/>
|
<Component
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
spacing="xs"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className="fa-spin"
|
||||||
|
name="fa fa-spinner"
|
||||||
|
/>
|
||||||
|
|
||||||
Fetching
|
Fetching
|
||||||
|
</Component>
|
||||||
|
|
||||||
|
<Component
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon="repeat"
|
||||||
|
onClick={[Function]}
|
||||||
|
size="md"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
Cancel loading dashboard
|
||||||
|
</Button>
|
||||||
|
</Component>
|
||||||
|
</Component>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -3,12 +3,18 @@ import { getBackendSrv } from '@grafana/runtime';
|
|||||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||||
// Actions
|
// Actions
|
||||||
import { loadPluginDashboards } from '../../plugins/state/actions';
|
import { loadPluginDashboards } from '../../plugins/state/actions';
|
||||||
import { loadDashboardPermissions, panelModelAndPluginReady, setPanelAngularComponent } from './reducers';
|
import {
|
||||||
|
cleanUpDashboard,
|
||||||
|
loadDashboardPermissions,
|
||||||
|
panelModelAndPluginReady,
|
||||||
|
setPanelAngularComponent,
|
||||||
|
} from './reducers';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { notifyApp } from 'app/core/actions';
|
||||||
import { loadPanelPlugin } from 'app/features/plugins/state/actions';
|
import { loadPanelPlugin } from 'app/features/plugins/state/actions';
|
||||||
// Types
|
// Types
|
||||||
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel, ThunkResult } from 'app/types';
|
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel, ThunkResult } from 'app/types';
|
||||||
import { PanelModel } from './PanelModel';
|
import { PanelModel } from './PanelModel';
|
||||||
|
import { cancelVariables } from '../../variables/state/actions';
|
||||||
|
|
||||||
export function getDashboardPermissions(id: number): ThunkResult<void> {
|
export function getDashboardPermissions(id: number): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
@ -153,3 +159,8 @@ export function changePanelPlugin(panel: PanelModel, pluginId: string): ThunkRes
|
|||||||
dispatch(panelModelAndPluginReady({ panelId: panel.id, plugin }));
|
dispatch(panelModelAndPluginReady({ panelId: panel.id, plugin }));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const cleanUpDashboardAndVariables = (): ThunkResult<void> => dispatch => {
|
||||||
|
dispatch(cleanUpDashboard());
|
||||||
|
dispatch(cancelVariables());
|
||||||
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import configureMockStore from 'redux-mock-store';
|
import configureMockStore from 'redux-mock-store';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { initDashboard, InitDashboardArgs } from './initDashboard';
|
import { initDashboard, InitDashboardArgs } from './initDashboard';
|
||||||
import { DashboardRouteInfo } from 'app/types';
|
import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { dashboardInitCompleted, dashboardInitFetching, dashboardInitServices } from './reducers';
|
import { dashboardInitCompleted, dashboardInitFetching, dashboardInitServices } from './reducers';
|
||||||
import { updateLocation } from '../../../core/actions';
|
import { updateLocation } from '../../../core/actions';
|
||||||
@ -10,8 +10,8 @@ import { Echo } from '../../../core/services/echo/Echo';
|
|||||||
import { getConfig } from 'app/core/config';
|
import { getConfig } from 'app/core/config';
|
||||||
import { variableAdapters } from 'app/features/variables/adapters';
|
import { variableAdapters } from 'app/features/variables/adapters';
|
||||||
import { createConstantVariableAdapter } from 'app/features/variables/constant/adapter';
|
import { createConstantVariableAdapter } from 'app/features/variables/constant/adapter';
|
||||||
import { addVariable } from 'app/features/variables/state/sharedReducer';
|
|
||||||
import { constantBuilder } from 'app/features/variables/shared/testing/builders';
|
import { constantBuilder } from 'app/features/variables/shared/testing/builders';
|
||||||
|
import { TransactionStatus, variablesInitTransaction } from '../../variables/state/transactionReducer';
|
||||||
|
|
||||||
jest.mock('app/core/services/backend_srv');
|
jest.mock('app/core/services/backend_srv');
|
||||||
jest.mock('app/features/dashboard/services/TimeSrv', () => {
|
jest.mock('app/features/dashboard/services/TimeSrv', () => {
|
||||||
@ -116,6 +116,7 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
|||||||
|
|
||||||
const ctx: ScenarioContext = {
|
const ctx: ScenarioContext = {
|
||||||
args: {
|
args: {
|
||||||
|
urlUid: 'DGmvKKxZz',
|
||||||
$injector: injectorMock,
|
$injector: injectorMock,
|
||||||
$scope: {},
|
$scope: {},
|
||||||
fixUrl: false,
|
fixUrl: false,
|
||||||
@ -134,7 +135,9 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
|||||||
location: {
|
location: {
|
||||||
query: {},
|
query: {},
|
||||||
},
|
},
|
||||||
dashboard: {},
|
dashboard: {
|
||||||
|
initPhase: DashboardInitPhase.Services,
|
||||||
|
},
|
||||||
user: {},
|
user: {},
|
||||||
explore: {
|
explore: {
|
||||||
left: {
|
left: {
|
||||||
@ -144,6 +147,7 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
|||||||
},
|
},
|
||||||
templating: {
|
templating: {
|
||||||
variables: {},
|
variables: {},
|
||||||
|
transaction: { uid: 'DGmvKKxZz', status: TransactionStatus.Completed },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup: (fn: () => void) => {
|
setup: (fn: () => void) => {
|
||||||
@ -186,8 +190,8 @@ describeInitScenario('Initializing new dashboard', ctx => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should send action dashboardInitCompleted', () => {
|
it('Should send action dashboardInitCompleted', () => {
|
||||||
expect(ctx.actions[3].type).toBe(dashboardInitCompleted.type);
|
expect(ctx.actions[5].type).toBe(dashboardInitCompleted.type);
|
||||||
expect(ctx.actions[3].payload.title).toBe('New dashboard');
|
expect(ctx.actions[5].payload.title).toBe('New dashboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should initialize services', () => {
|
it('Should initialize services', () => {
|
||||||
@ -209,11 +213,10 @@ describeInitScenario('Initializing new dashboard', ctx => {
|
|||||||
describeInitScenario('Initializing home dashboard', ctx => {
|
describeInitScenario('Initializing home dashboard', ctx => {
|
||||||
ctx.setup(() => {
|
ctx.setup(() => {
|
||||||
ctx.args.routeInfo = DashboardRouteInfo.Home;
|
ctx.args.routeInfo = DashboardRouteInfo.Home;
|
||||||
ctx.backendSrv.get.mockReturnValue(
|
ctx.backendSrv.get.mockResolvedValue({
|
||||||
Promise.resolve({
|
meta: {},
|
||||||
redirectUri: '/u/123/my-home',
|
redirectUri: '/u/123/my-home',
|
||||||
})
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should redirect to custom home dashboard', () => {
|
it('Should redirect to custom home dashboard', () => {
|
||||||
@ -257,7 +260,7 @@ describeInitScenario('Initializing existing dashboard', ctx => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should send action dashboardInitCompleted', () => {
|
it('Should send action dashboardInitCompleted', () => {
|
||||||
const index = getConfig().featureToggles.newVariables ? 4 : 3;
|
const index = getConfig().featureToggles.newVariables ? 6 : 5;
|
||||||
expect(ctx.actions[index].type).toBe(dashboardInitCompleted.type);
|
expect(ctx.actions[index].type).toBe(dashboardInitCompleted.type);
|
||||||
expect(ctx.actions[index].payload.title).toBe('My cool dashboard');
|
expect(ctx.actions[index].payload.title).toBe('My cool dashboard');
|
||||||
});
|
});
|
||||||
@ -281,6 +284,38 @@ describeInitScenario('Initializing existing dashboard', ctx => {
|
|||||||
if (!getConfig().featureToggles.newVariables) {
|
if (!getConfig().featureToggles.newVariables) {
|
||||||
return expect.assertions(0);
|
return expect.assertions(0);
|
||||||
}
|
}
|
||||||
expect(ctx.actions[3].type).toBe(addVariable.type);
|
expect(ctx.actions[3].type).toBe(variablesInitTransaction.type);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describeInitScenario('Initializing previously canceled dashboard initialization', ctx => {
|
||||||
|
ctx.setup(() => {
|
||||||
|
ctx.storeState.dashboard.initPhase = DashboardInitPhase.Fetching;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should send action dashboardInitFetching', () => {
|
||||||
|
expect(ctx.actions[0].type).toBe(dashboardInitFetching.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should send action dashboardInitServices ', () => {
|
||||||
|
expect(ctx.actions[1].type).toBe(dashboardInitServices.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not send action dashboardInitCompleted', () => {
|
||||||
|
const dashboardInitCompletedAction = ctx.actions.find(a => {
|
||||||
|
return a.type === dashboardInitCompleted.type;
|
||||||
|
});
|
||||||
|
expect(dashboardInitCompletedAction).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should initialize timeSrv and annotationsSrv', () => {
|
||||||
|
expect(ctx.timeSrv.init).toBeCalled();
|
||||||
|
expect(ctx.annotationsSrv.init).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not initialize other services', () => {
|
||||||
|
expect(ctx.unsavedChangesSrv.init).not.toBeCalled();
|
||||||
|
expect(ctx.keybindingSrv.setupDashboardBindings).not.toBeCalled();
|
||||||
|
expect(ctx.dashboardSrv.setCurrent).not.toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -18,11 +18,17 @@ import {
|
|||||||
dashboardInitSlow,
|
dashboardInitSlow,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
// Types
|
// Types
|
||||||
import { DashboardDTO, DashboardRouteInfo, StoreState, ThunkDispatch, ThunkResult } from 'app/types';
|
import {
|
||||||
|
DashboardDTO,
|
||||||
|
DashboardRouteInfo,
|
||||||
|
StoreState,
|
||||||
|
ThunkDispatch,
|
||||||
|
ThunkResult,
|
||||||
|
DashboardInitPhase,
|
||||||
|
} from 'app/types';
|
||||||
import { DashboardModel } from './DashboardModel';
|
import { DashboardModel } from './DashboardModel';
|
||||||
import { DataQuery, locationUtil } from '@grafana/data';
|
import { DataQuery, locationUtil } from '@grafana/data';
|
||||||
import { getConfig } from '../../../core/config';
|
import { initVariablesTransaction } from '../../variables/state/actions';
|
||||||
import { initDashboardTemplating, processVariables, completeDashboardTemplating } from '../../variables/state/actions';
|
|
||||||
import { emitDashboardViewEvent } from './analyticsProcessor';
|
import { emitDashboardViewEvent } from './analyticsProcessor';
|
||||||
|
|
||||||
export interface InitDashboardArgs {
|
export interface InitDashboardArgs {
|
||||||
@ -63,6 +69,11 @@ async function fetchDashboard(
|
|||||||
// load home dash
|
// load home dash
|
||||||
const dashDTO: DashboardDTO = await backendSrv.get('/api/dashboards/home');
|
const dashDTO: DashboardDTO = await backendSrv.get('/api/dashboards/home');
|
||||||
|
|
||||||
|
// if above all is cancelled it will return an array
|
||||||
|
if (!dashDTO.meta) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// if user specified a custom home dashboard redirect to that
|
// if user specified a custom home dashboard redirect to that
|
||||||
if (dashDTO.redirectUri) {
|
if (dashDTO.redirectUri) {
|
||||||
const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri);
|
const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri);
|
||||||
@ -177,20 +188,19 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
|||||||
dashboard.meta.fromExplore = !!(panelId && queries);
|
dashboard.meta.fromExplore = !!(panelId && queries);
|
||||||
}
|
}
|
||||||
|
|
||||||
// template values service needs to initialize completely before
|
// template values service needs to initialize completely before the rest of the dashboard can load
|
||||||
// the rest of the dashboard can load
|
await dispatch(initVariablesTransaction(args.urlUid, dashboard, variableSrv));
|
||||||
try {
|
|
||||||
if (!getConfig().featureToggles.newVariables) {
|
if (getState().templating.transaction.uid !== args.urlUid) {
|
||||||
await variableSrv.init(dashboard);
|
// if a previous dashboard has slow running variable queries the batch uid will be the new one
|
||||||
}
|
// but the args.urlUid will be the same as before initVariablesTransaction was called so then we can't continue initializing
|
||||||
if (getConfig().featureToggles.newVariables) {
|
// the previous dashboard.
|
||||||
dispatch(initDashboardTemplating(dashboard.templating.list));
|
return;
|
||||||
await dispatch(processVariables());
|
}
|
||||||
dispatch(completeDashboardTemplating(dashboard));
|
|
||||||
}
|
// If dashboard is in a different init phase it means it cancelled during service init
|
||||||
} catch (err) {
|
if (getState().dashboard.initPhase !== DashboardInitPhase.Services) {
|
||||||
dispatch(notifyApp(createErrorNotification('Templating init failed', err)));
|
return;
|
||||||
console.log(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import {
|
import {
|
||||||
DashboardInitPhase,
|
|
||||||
DashboardState,
|
|
||||||
DashboardAclDTO,
|
DashboardAclDTO,
|
||||||
DashboardInitError,
|
DashboardInitError,
|
||||||
|
DashboardInitPhase,
|
||||||
|
DashboardState,
|
||||||
PanelState,
|
PanelState,
|
||||||
QueriesToUpdateOnDashboardLoad,
|
QueriesToUpdateOnDashboardLoad,
|
||||||
} from 'app/types';
|
} from 'app/types';
|
||||||
|
@ -22,6 +22,7 @@ export const updateQueryVariableOptions = (
|
|||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const variableInState = getVariable<QueryVariableModel>(identifier.id!, getState());
|
const variableInState = getVariable<QueryVariableModel>(identifier.id!, getState());
|
||||||
try {
|
try {
|
||||||
|
const beforeUid = getState().templating.transaction.uid;
|
||||||
if (getState().templating.editor.id === variableInState.id) {
|
if (getState().templating.editor.id === variableInState.id) {
|
||||||
dispatch(removeVariableEditorError({ errorProp: 'update' }));
|
dispatch(removeVariableEditorError({ errorProp: 'update' }));
|
||||||
}
|
}
|
||||||
@ -36,6 +37,13 @@ export const updateQueryVariableOptions = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const results = await dataSource.metricFindQuery(variableInState.query, queryOptions);
|
const results = await dataSource.metricFindQuery(variableInState.query, queryOptions);
|
||||||
|
|
||||||
|
const afterUid = getState().templating.transaction.uid;
|
||||||
|
if (beforeUid !== afterUid) {
|
||||||
|
// we started another batch before this metricFindQuery finished let's abort
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const templatedRegex = getTemplatedRegex(variableInState);
|
const templatedRegex = getTemplatedRegex(variableInState);
|
||||||
await dispatch(updateVariableOptions(toVariablePayload(variableInState, { results, templatedRegex })));
|
await dispatch(updateVariableOptions(toVariablePayload(variableInState, { results, templatedRegex })));
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { AnyAction } from 'redux';
|
import { AnyAction } from 'redux';
|
||||||
import { UrlQueryMap } from '@grafana/data';
|
import { UrlQueryMap } from '@grafana/data';
|
||||||
|
|
||||||
import { getTemplatingAndLocationRootReducer, getTemplatingRootReducer } from './helpers';
|
import { getRootReducer, getTemplatingAndLocationRootReducer, getTemplatingRootReducer } from './helpers';
|
||||||
import { variableAdapters } from '../adapters';
|
import { variableAdapters } from '../adapters';
|
||||||
import { createQueryVariableAdapter } from '../query/adapter';
|
import { createQueryVariableAdapter } from '../query/adapter';
|
||||||
import { createCustomVariableAdapter } from '../custom/adapter';
|
import { createCustomVariableAdapter } from '../custom/adapter';
|
||||||
@ -10,8 +10,11 @@ import { createConstantVariableAdapter } from '../constant/adapter';
|
|||||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||||
import { TemplatingState } from 'app/features/variables/state/reducers';
|
import { TemplatingState } from 'app/features/variables/state/reducers';
|
||||||
import {
|
import {
|
||||||
|
cancelVariables,
|
||||||
changeVariableMultiValue,
|
changeVariableMultiValue,
|
||||||
|
cleanUpVariables,
|
||||||
initDashboardTemplating,
|
initDashboardTemplating,
|
||||||
|
initVariablesTransaction,
|
||||||
processVariables,
|
processVariables,
|
||||||
setOptionFromUrl,
|
setOptionFromUrl,
|
||||||
validateVariableSelectionState,
|
validateVariableSelectionState,
|
||||||
@ -34,7 +37,22 @@ import {
|
|||||||
textboxBuilder,
|
textboxBuilder,
|
||||||
} from '../shared/testing/builders';
|
} from '../shared/testing/builders';
|
||||||
import { changeVariableName } from '../editor/actions';
|
import { changeVariableName } from '../editor/actions';
|
||||||
import { changeVariableNameFailed, changeVariableNameSucceeded, setIdInEditor } from '../editor/reducer';
|
import {
|
||||||
|
changeVariableNameFailed,
|
||||||
|
changeVariableNameSucceeded,
|
||||||
|
initialVariableEditorState,
|
||||||
|
setIdInEditor,
|
||||||
|
} from '../editor/reducer';
|
||||||
|
import { DashboardState, LocationState } from '../../../types';
|
||||||
|
import {
|
||||||
|
TransactionStatus,
|
||||||
|
variablesClearTransaction,
|
||||||
|
variablesCompleteTransaction,
|
||||||
|
variablesInitTransaction,
|
||||||
|
} from './transactionReducer';
|
||||||
|
import { initialState } from '../pickers/OptionsPicker/reducer';
|
||||||
|
import { cleanVariables } from './variablesReducer';
|
||||||
|
import { expect } from '../../../../test/lib/common';
|
||||||
|
|
||||||
variableAdapters.setInit(() => [
|
variableAdapters.setInit(() => [
|
||||||
createQueryVariableAdapter(),
|
createQueryVariableAdapter(),
|
||||||
@ -527,4 +545,96 @@ describe('shared actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('initVariablesTransaction', () => {
|
||||||
|
type ReducersUsedInContext = {
|
||||||
|
templating: TemplatingState;
|
||||||
|
dashboard: DashboardState;
|
||||||
|
location: LocationState;
|
||||||
|
};
|
||||||
|
const constant = constantBuilder()
|
||||||
|
.withId('constant')
|
||||||
|
.withName('constant')
|
||||||
|
.build();
|
||||||
|
const templating: any = { list: [constant] };
|
||||||
|
const uid = 'uid';
|
||||||
|
const dashboard: any = { title: 'Some dash', uid, templating };
|
||||||
|
const variableSrv: any = {};
|
||||||
|
|
||||||
|
describe('when called and the previous dashboard has completed', () => {
|
||||||
|
it('then correct actions are dispatched', async () => {
|
||||||
|
const tester = await reduxTester<ReducersUsedInContext>()
|
||||||
|
.givenRootReducer(getRootReducer())
|
||||||
|
.whenAsyncActionIsDispatched(initVariablesTransaction(uid, dashboard, variableSrv));
|
||||||
|
|
||||||
|
tester.thenDispatchedActionsShouldEqual(
|
||||||
|
variablesInitTransaction({ uid }),
|
||||||
|
addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant })),
|
||||||
|
addInitLock(toVariablePayload(constant)),
|
||||||
|
resolveInitLock(toVariablePayload(constant)),
|
||||||
|
removeInitLock(toVariablePayload(constant)),
|
||||||
|
variablesCompleteTransaction({ uid })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when called and the previous dashboard is still processing variables', () => {
|
||||||
|
it('then correct actions are dispatched', async () => {
|
||||||
|
const transactionState = { uid: 'previous-uid', status: TransactionStatus.Fetching };
|
||||||
|
|
||||||
|
const tester = await reduxTester<ReducersUsedInContext>({
|
||||||
|
preloadedState: ({
|
||||||
|
templating: {
|
||||||
|
transaction: transactionState,
|
||||||
|
variables: {},
|
||||||
|
optionsPicker: { ...initialState },
|
||||||
|
editor: { ...initialVariableEditorState },
|
||||||
|
},
|
||||||
|
} as unknown) as ReducersUsedInContext,
|
||||||
|
})
|
||||||
|
.givenRootReducer(getRootReducer())
|
||||||
|
.whenAsyncActionIsDispatched(initVariablesTransaction(uid, dashboard, variableSrv));
|
||||||
|
|
||||||
|
tester.thenDispatchedActionsShouldEqual(
|
||||||
|
cleanVariables(),
|
||||||
|
variablesClearTransaction(),
|
||||||
|
variablesInitTransaction({ uid }),
|
||||||
|
addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant })),
|
||||||
|
addInitLock(toVariablePayload(constant)),
|
||||||
|
resolveInitLock(toVariablePayload(constant)),
|
||||||
|
removeInitLock(toVariablePayload(constant)),
|
||||||
|
variablesCompleteTransaction({ uid })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cleanUpVariables', () => {
|
||||||
|
describe('when called', () => {
|
||||||
|
it('then correct actions are dispatched', async () => {
|
||||||
|
reduxTester<{ templating: TemplatingState }>()
|
||||||
|
.givenRootReducer(getTemplatingRootReducer())
|
||||||
|
.whenActionIsDispatched(cleanUpVariables())
|
||||||
|
.thenDispatchedActionsShouldEqual(cleanVariables(), variablesClearTransaction());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cancelVariables', () => {
|
||||||
|
const cancelAllInFlightRequestsMock = jest.fn();
|
||||||
|
const backendSrvMock: any = {
|
||||||
|
cancelAllInFlightRequests: cancelAllInFlightRequestsMock,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('when called', () => {
|
||||||
|
it('then cancelAllInFlightRequests should be called and correct actions are dispatched', async () => {
|
||||||
|
reduxTester<{ templating: TemplatingState }>()
|
||||||
|
.givenRootReducer(getTemplatingRootReducer())
|
||||||
|
.whenActionIsDispatched(cancelVariables({ getBackendSrv: () => backendSrvMock }))
|
||||||
|
.thenDispatchedActionsShouldEqual(cleanVariables(), variablesClearTransaction());
|
||||||
|
|
||||||
|
expect(cancelAllInFlightRequestsMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@ import { StoreState, ThunkResult } from '../../../types';
|
|||||||
import { getVariable, getVariables } from './selectors';
|
import { getVariable, getVariables } from './selectors';
|
||||||
import { variableAdapters } from '../adapters';
|
import { variableAdapters } from '../adapters';
|
||||||
import { Graph } from '../../../core/utils/dag';
|
import { Graph } from '../../../core/utils/dag';
|
||||||
import { updateLocation } from 'app/core/actions';
|
import { notifyApp, updateLocation } from 'app/core/actions';
|
||||||
import {
|
import {
|
||||||
addInitLock,
|
addInitLock,
|
||||||
addVariable,
|
addVariable,
|
||||||
@ -31,6 +31,17 @@ import { alignCurrentWithMulti } from '../shared/multiOptions';
|
|||||||
import { isMulti } from '../guard';
|
import { isMulti } from '../guard';
|
||||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
|
import { getConfig } from '../../../core/config';
|
||||||
|
import { createErrorNotification } from '../../../core/copy/appNotification';
|
||||||
|
import { VariableSrv } from '../../templating/variable_srv';
|
||||||
|
import {
|
||||||
|
TransactionStatus,
|
||||||
|
variablesClearTransaction,
|
||||||
|
variablesCompleteTransaction,
|
||||||
|
variablesInitTransaction,
|
||||||
|
} from './transactionReducer';
|
||||||
|
import { getBackendSrv } from '../../../core/services/backend_srv';
|
||||||
|
import { cleanVariables } from './variablesReducer';
|
||||||
|
|
||||||
// process flow queryVariable
|
// process flow queryVariable
|
||||||
// thunk => processVariables
|
// thunk => processVariables
|
||||||
@ -457,3 +468,49 @@ const getQueryWithVariables = (getState: () => StoreState): UrlQueryMap => {
|
|||||||
|
|
||||||
return queryParamsNew;
|
return queryParamsNew;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initVariablesTransaction = (
|
||||||
|
dashboardUid: string,
|
||||||
|
dashboard: DashboardModel,
|
||||||
|
variableSrv: VariableSrv
|
||||||
|
): ThunkResult<void> => async (dispatch, getState) => {
|
||||||
|
try {
|
||||||
|
const transactionState = getState().templating.transaction;
|
||||||
|
if (transactionState.status === TransactionStatus.Fetching) {
|
||||||
|
// previous dashboard is still fetching variables, cancel all requests
|
||||||
|
dispatch(cancelVariables());
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(variablesInitTransaction({ uid: dashboardUid }));
|
||||||
|
|
||||||
|
const newVariables = getConfig().featureToggles.newVariables;
|
||||||
|
|
||||||
|
if (!newVariables) {
|
||||||
|
await variableSrv.init(dashboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newVariables) {
|
||||||
|
dispatch(initDashboardTemplating(dashboard.templating.list));
|
||||||
|
await dispatch(processVariables());
|
||||||
|
dispatch(completeDashboardTemplating(dashboard));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(variablesCompleteTransaction({ uid: dashboardUid }));
|
||||||
|
} catch (err) {
|
||||||
|
dispatch(notifyApp(createErrorNotification('Templating init failed', err)));
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanUpVariables = (): ThunkResult<void> => dispatch => {
|
||||||
|
dispatch(cleanVariables());
|
||||||
|
dispatch(variablesClearTransaction());
|
||||||
|
};
|
||||||
|
|
||||||
|
type CancelVariablesDependencies = { getBackendSrv: typeof getBackendSrv };
|
||||||
|
export const cancelVariables = (
|
||||||
|
dependencies: CancelVariablesDependencies = { getBackendSrv: getBackendSrv }
|
||||||
|
): ThunkResult<void> => dispatch => {
|
||||||
|
dependencies.getBackendSrv().cancelAllInFlightRequests();
|
||||||
|
dispatch(cleanUpVariables());
|
||||||
|
};
|
||||||
|
@ -2,12 +2,11 @@ import { combineReducers } from '@reduxjs/toolkit';
|
|||||||
|
|
||||||
import { NEW_VARIABLE_ID } from './types';
|
import { NEW_VARIABLE_ID } from './types';
|
||||||
import { VariableHide, VariableModel } from '../../templating/types';
|
import { VariableHide, VariableModel } from '../../templating/types';
|
||||||
import { variablesReducer, VariablesState } from './variablesReducer';
|
import { VariablesState } from './variablesReducer';
|
||||||
import { optionsPickerReducer } from '../pickers/OptionsPicker/reducer';
|
|
||||||
import { variableEditorReducer } from '../editor/reducer';
|
|
||||||
import { locationReducer } from '../../../core/reducers/location';
|
import { locationReducer } from '../../../core/reducers/location';
|
||||||
import { VariableAdapter } from '../adapters';
|
import { VariableAdapter } from '../adapters';
|
||||||
import { dashboardReducer } from 'app/features/dashboard/state/reducers';
|
import { dashboardReducer } from 'app/features/dashboard/state/reducers';
|
||||||
|
import { templatingReducers } from './reducers';
|
||||||
|
|
||||||
export const getVariableState = (
|
export const getVariableState = (
|
||||||
noOfVariables: number,
|
noOfVariables: number,
|
||||||
@ -65,28 +64,16 @@ export const getRootReducer = () =>
|
|||||||
combineReducers({
|
combineReducers({
|
||||||
location: locationReducer,
|
location: locationReducer,
|
||||||
dashboard: dashboardReducer,
|
dashboard: dashboardReducer,
|
||||||
templating: combineReducers({
|
templating: templatingReducers,
|
||||||
optionsPicker: optionsPickerReducer,
|
|
||||||
editor: variableEditorReducer,
|
|
||||||
variables: variablesReducer,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getTemplatingRootReducer = () =>
|
export const getTemplatingRootReducer = () =>
|
||||||
combineReducers({
|
combineReducers({
|
||||||
templating: combineReducers({
|
templating: templatingReducers,
|
||||||
optionsPicker: optionsPickerReducer,
|
|
||||||
editor: variableEditorReducer,
|
|
||||||
variables: variablesReducer,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getTemplatingAndLocationRootReducer = () =>
|
export const getTemplatingAndLocationRootReducer = () =>
|
||||||
combineReducers({
|
combineReducers({
|
||||||
templating: combineReducers({
|
templating: templatingReducers,
|
||||||
optionsPicker: optionsPickerReducer,
|
|
||||||
editor: variableEditorReducer,
|
|
||||||
variables: variablesReducer,
|
|
||||||
}),
|
|
||||||
location: locationReducer,
|
location: locationReducer,
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||||
import { cleanUpDashboard } from 'app/features/dashboard/state/reducers';
|
|
||||||
import { QueryVariableModel, VariableHide } from '../../templating/types';
|
import { QueryVariableModel, VariableHide } from '../../templating/types';
|
||||||
import { VariableAdapter, variableAdapters } from '../adapters';
|
import { VariableAdapter, variableAdapters } from '../adapters';
|
||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { variablesReducer, VariablesState } from './variablesReducer';
|
import { cleanVariables, variablesReducer, VariablesState } from './variablesReducer';
|
||||||
import { toVariablePayload, VariablePayload } from './types';
|
import { toVariablePayload, VariablePayload } from './types';
|
||||||
import { VariableType } from '@grafana/data';
|
import { VariableType } from '@grafana/data';
|
||||||
|
|
||||||
@ -71,7 +70,7 @@ describe('variablesReducer', () => {
|
|||||||
|
|
||||||
reducerTester<VariablesState>()
|
reducerTester<VariablesState>()
|
||||||
.givenReducer(variablesReducer, initialState)
|
.givenReducer(variablesReducer, initialState)
|
||||||
.whenActionIsDispatched(cleanUpDashboard())
|
.whenActionIsDispatched(cleanVariables())
|
||||||
.thenStateShouldEqual({
|
.thenStateShouldEqual({
|
||||||
'1': {
|
'1': {
|
||||||
id: '1',
|
id: '1',
|
||||||
|
@ -3,17 +3,22 @@ import { optionsPickerReducer, OptionsPickerState } from '../pickers/OptionsPick
|
|||||||
import { variableEditorReducer, VariableEditorState } from '../editor/reducer';
|
import { variableEditorReducer, VariableEditorState } from '../editor/reducer';
|
||||||
import { variablesReducer } from './variablesReducer';
|
import { variablesReducer } from './variablesReducer';
|
||||||
import { VariableModel } from '../../templating/types';
|
import { VariableModel } from '../../templating/types';
|
||||||
|
import { transactionReducer, TransactionState } from './transactionReducer';
|
||||||
|
|
||||||
export interface TemplatingState {
|
export interface TemplatingState {
|
||||||
variables: Record<string, VariableModel>;
|
variables: Record<string, VariableModel>;
|
||||||
optionsPicker: OptionsPickerState;
|
optionsPicker: OptionsPickerState;
|
||||||
editor: VariableEditorState;
|
editor: VariableEditorState;
|
||||||
|
transaction: TransactionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const templatingReducers = combineReducers({
|
||||||
|
editor: variableEditorReducer,
|
||||||
|
variables: variablesReducer,
|
||||||
|
optionsPicker: optionsPickerReducer,
|
||||||
|
transaction: transactionReducer,
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
templating: combineReducers({
|
templating: templatingReducers,
|
||||||
editor: variableEditorReducer,
|
|
||||||
variables: variablesReducer,
|
|
||||||
optionsPicker: optionsPickerReducer,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
@ -35,10 +35,22 @@ const sharedReducerSlice = createSlice({
|
|||||||
},
|
},
|
||||||
resolveInitLock: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
|
resolveInitLock: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
|
||||||
const instanceState = getInstanceState(state, action.payload.id!);
|
const instanceState = getInstanceState(state, action.payload.id!);
|
||||||
|
|
||||||
|
if (!instanceState) {
|
||||||
|
// we might have cancelled a batch so then this state has been removed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
instanceState.initLock?.resolve();
|
instanceState.initLock?.resolve();
|
||||||
},
|
},
|
||||||
removeInitLock: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
|
removeInitLock: (state: VariablesState, action: PayloadAction<VariablePayload>) => {
|
||||||
const instanceState = getInstanceState(state, action.payload.id!);
|
const instanceState = getInstanceState(state, action.payload.id!);
|
||||||
|
|
||||||
|
if (!instanceState) {
|
||||||
|
// we might have cancelled a batch so then this state has been removed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
instanceState.initLock = null;
|
instanceState.initLock = null;
|
||||||
},
|
},
|
||||||
removeVariable: (state: VariablesState, action: PayloadAction<VariablePayload<{ reIndex: boolean }>>) => {
|
removeVariable: (state: VariablesState, action: PayloadAction<VariablePayload<{ reIndex: boolean }>>) => {
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||||
|
import {
|
||||||
|
initialTransactionState,
|
||||||
|
transactionReducer,
|
||||||
|
TransactionStatus,
|
||||||
|
variablesClearTransaction,
|
||||||
|
variablesCompleteTransaction,
|
||||||
|
variablesInitTransaction,
|
||||||
|
} from './transactionReducer';
|
||||||
|
|
||||||
|
describe('transactionReducer', () => {
|
||||||
|
describe('when variablesInitTransaction is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(transactionReducer, { ...initialTransactionState })
|
||||||
|
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||||
|
.thenStateShouldEqual({ ...initialTransactionState, uid: 'a uid', status: TransactionStatus.Fetching });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when variablesCompleteTransaction is dispatched', () => {
|
||||||
|
describe('and transaction uid is the same', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(transactionReducer, {
|
||||||
|
...initialTransactionState,
|
||||||
|
uid: 'before',
|
||||||
|
status: TransactionStatus.Fetching,
|
||||||
|
})
|
||||||
|
.whenActionIsDispatched(variablesCompleteTransaction({ uid: 'before' }))
|
||||||
|
.thenStateShouldEqual({ ...initialTransactionState, uid: 'before', status: TransactionStatus.Completed });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and transaction uid is not the same', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(transactionReducer, {
|
||||||
|
...initialTransactionState,
|
||||||
|
uid: 'before',
|
||||||
|
status: TransactionStatus.Fetching,
|
||||||
|
})
|
||||||
|
.whenActionIsDispatched(variablesCompleteTransaction({ uid: 'after' }))
|
||||||
|
.thenStateShouldEqual({ ...initialTransactionState, uid: 'before', status: TransactionStatus.Fetching });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when variablesClearTransaction is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester()
|
||||||
|
.givenReducer(transactionReducer, {
|
||||||
|
...initialTransactionState,
|
||||||
|
uid: 'before',
|
||||||
|
status: TransactionStatus.Completed,
|
||||||
|
})
|
||||||
|
.whenActionIsDispatched(variablesClearTransaction())
|
||||||
|
.thenStateShouldEqual({ ...initialTransactionState });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
45
public/app/features/variables/state/transactionReducer.ts
Normal file
45
public/app/features/variables/state/transactionReducer.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export enum TransactionStatus {
|
||||||
|
NotStarted = 'Not started',
|
||||||
|
Fetching = 'Fetching',
|
||||||
|
Completed = 'Completed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionState {
|
||||||
|
uid: string | undefined | null;
|
||||||
|
status: TransactionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialTransactionState: TransactionState = { uid: null, status: TransactionStatus.NotStarted };
|
||||||
|
|
||||||
|
const transactionSlice = createSlice({
|
||||||
|
name: 'templating/transaction',
|
||||||
|
initialState: initialTransactionState,
|
||||||
|
reducers: {
|
||||||
|
variablesInitTransaction: (state, action: PayloadAction<{ uid: string | undefined | null }>) => {
|
||||||
|
state.uid = action.payload.uid;
|
||||||
|
state.status = TransactionStatus.Fetching;
|
||||||
|
},
|
||||||
|
variablesCompleteTransaction: (state, action: PayloadAction<{ uid: string | undefined | null }>) => {
|
||||||
|
if (state.uid !== action.payload.uid) {
|
||||||
|
// this might be an action from a cancelled batch
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.status = TransactionStatus.Completed;
|
||||||
|
},
|
||||||
|
variablesClearTransaction: (state, action: PayloadAction<undefined>) => {
|
||||||
|
state.uid = null;
|
||||||
|
state.status = TransactionStatus.NotStarted;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
variablesInitTransaction,
|
||||||
|
variablesClearTransaction,
|
||||||
|
variablesCompleteTransaction,
|
||||||
|
} = transactionSlice.actions;
|
||||||
|
|
||||||
|
export const transactionReducer = transactionSlice.reducer;
|
@ -1,5 +1,4 @@
|
|||||||
import { PayloadAction } from '@reduxjs/toolkit';
|
import { createAction, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { cleanUpDashboard } from '../../dashboard/state/reducers';
|
|
||||||
import { variableAdapters } from '../adapters';
|
import { variableAdapters } from '../adapters';
|
||||||
import { sharedReducer } from './sharedReducer';
|
import { sharedReducer } from './sharedReducer';
|
||||||
import { VariableModel } from '../../templating/types';
|
import { VariableModel } from '../../templating/types';
|
||||||
@ -9,11 +8,13 @@ export interface VariablesState extends Record<string, VariableModel> {}
|
|||||||
|
|
||||||
export const initialVariablesState: VariablesState = {};
|
export const initialVariablesState: VariablesState = {};
|
||||||
|
|
||||||
|
export const cleanVariables = createAction<undefined>('templating/cleanVariables');
|
||||||
|
|
||||||
export const variablesReducer = (
|
export const variablesReducer = (
|
||||||
state: VariablesState = initialVariablesState,
|
state: VariablesState = initialVariablesState,
|
||||||
action: PayloadAction<VariablePayload>
|
action: PayloadAction<VariablePayload>
|
||||||
): VariablesState => {
|
): VariablesState => {
|
||||||
if (cleanUpDashboard.match(action)) {
|
if (cleanVariables.match(action)) {
|
||||||
const globalVariables = Object.values(state).filter(v => v.global);
|
const globalVariables = Object.values(state).filter(v => v.global);
|
||||||
if (!globalVariables) {
|
if (!globalVariables) {
|
||||||
return initialVariablesState;
|
return initialVariablesState;
|
||||||
|
Loading…
Reference in New Issue
Block a user