mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Migrates reducers and actions to Redux Toolkit (#21287)
* Refactor: Adds Redux Toolkit package * Refactor: Uses configureStore from Redux Toolkit * Refactor: Migrates applicationReducer * Refactor: Migrates appNotificationsReducer * Refactor: Migrates locationReducer * Refactor: Migrates navModelReducer * Refactor: Migrates teamsReducer and teamReducer * Refactor: Migrates cleanUpAction * Refactor: Migrates alertRulesReducer * Refactor: Cleans up recursiveCleanState * Refactor: Switched to Angular compatible reducers * Refactor: Migrates folderReducer * Refactor: Migrates dashboardReducer * Migrates panelEditorReducer * Refactor: Migrates dataSourcesReducer * Refactor: Migrates usersReducer * Refactor: Migrates organizationReducer * Refactor: Migrates pluginsReducer * Refactor: Migrates ldapReducer and ldapUserReducer * Refactor: Migrates apiKeysReducer * Refactor: Migrates exploreReducer and itemReducer * Refactor: Removes actionCreatorFactory and reducerFactory * Refactor: Moves mocks to test section * Docs: Removes sections about home grown framework * Update contribute/style-guides/redux.md Co-Authored-By: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Refactor: Cleans up some code * Refactor: Adds state typings * Refactor: Cleans up typings * Refactor: Adds comment about ImmerJs autoFreeze Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
This commit is contained in:
parent
b0515f46cc
commit
4f0fa776be
@ -1,121 +1,7 @@
|
||||
# Redux framework
|
||||
|
||||
To reduce the amount of boilerplate code used to create a strongly typed redux solution with actions, action creators, reducers and tests we've introduced a small framework around Redux.
|
||||
|
||||
`+` Much less boilerplate code
|
||||
`-` Non Redux standard api
|
||||
|
||||
## Core functionality
|
||||
|
||||
### actionCreatorFactory
|
||||
|
||||
Used to create an action creator with the following signature
|
||||
|
||||
```typescript
|
||||
{ type: string , (payload: T): {type: string; payload: T;} }
|
||||
```
|
||||
|
||||
where the `type` string will be ensured to be unique and `T` is the type supplied to the factory.
|
||||
|
||||
#### Example
|
||||
|
||||
```typescript
|
||||
export const someAction = actionCreatorFactory<string>('SOME_ACTION').create();
|
||||
|
||||
// later when dispatched
|
||||
someAction('this rocks!');
|
||||
```
|
||||
|
||||
```typescript
|
||||
// best practices, always use an interface as type
|
||||
interface SomeAction {
|
||||
data: string;
|
||||
}
|
||||
export const someAction = actionCreatorFactory<SomeAction>('SOME_ACTION').create();
|
||||
|
||||
// later when dispatched
|
||||
someAction({ data: 'best practices' });
|
||||
```
|
||||
|
||||
```typescript
|
||||
// declaring an action creator with a type string that has already been defined will throw
|
||||
export const someAction = actionCreatorFactory<string>('SOME_ACTION').create();
|
||||
export const theAction = actionCreatorFactory<string>('SOME_ACTION').create(); // will throw
|
||||
```
|
||||
|
||||
### reducerFactory
|
||||
|
||||
Fluent API used to create a reducer. (same as implementing the standard switch statement in Redux)
|
||||
|
||||
#### Example
|
||||
|
||||
```typescript
|
||||
interface ExampleReducerState {
|
||||
data: string[];
|
||||
}
|
||||
|
||||
const intialState: ExampleReducerState = { data: [] };
|
||||
|
||||
export const someAction = actionCreatorFactory<string>('SOME_ACTION').create();
|
||||
export const otherAction = actionCreatorFactory<string[]>('Other_ACTION').create();
|
||||
|
||||
export const exampleReducer = reducerFactory<ExampleReducerState>(intialState)
|
||||
// addMapper is the function that ties an action creator to a state change
|
||||
.addMapper({
|
||||
// action creator to filter out which mapper to use
|
||||
filter: someAction,
|
||||
// mapper function where the state change occurs
|
||||
mapper: (state, action) => ({ ...state, data: state.data.concat(action.payload) }),
|
||||
})
|
||||
// a developer can just chain addMapper functions until reducer is done
|
||||
.addMapper({
|
||||
filter: otherAction,
|
||||
mapper: (state, action) => ({ ...state, data: action.payload }),
|
||||
})
|
||||
.create(); // this will return the reducer
|
||||
```
|
||||
|
||||
#### Typing limitations
|
||||
|
||||
There is a challenge left with the mapper function that I can not solve with TypeScript. The signature of a mapper is
|
||||
|
||||
```typescript
|
||||
<State, Payload>(state: State, action: ActionOf<Payload>) => State;
|
||||
```
|
||||
|
||||
If you would to return an object that is not of the state type like the following mapper
|
||||
|
||||
```typescript
|
||||
mapper: (state, action) => ({ nonExistingProperty: ''}),
|
||||
```
|
||||
|
||||
Then you would receive the following compile error
|
||||
|
||||
```shell
|
||||
[ts] Property 'data' is missing in type '{ nonExistingProperty: string; }' but required in type 'ExampleReducerState'. [2741]
|
||||
```
|
||||
|
||||
But if you return an object that is spreading state and add a non existing property type like the following mapper
|
||||
|
||||
```typescript
|
||||
mapper: (state, action) => ({ ...state, nonExistingProperty: ''}),
|
||||
```
|
||||
|
||||
Then you would not receive any compile error.
|
||||
|
||||
If you want to make sure that never happens you can just supply the State type to the mapper callback like the following mapper:
|
||||
|
||||
```typescript
|
||||
mapper: (state, action): ExampleReducerState => ({ ...state, nonExistingProperty: 'kalle' }),
|
||||
```
|
||||
|
||||
Then you would receive the following compile error
|
||||
|
||||
```shell
|
||||
[ts]
|
||||
Type '{ nonExistingProperty: string; data: string[]; }' is not assignable to type 'ExampleReducerState'.
|
||||
Object literal may only specify known properties, and 'nonExistingProperty' does not exist in type 'ExampleReducerState'. [2322]
|
||||
```
|
||||
Grafana uses [Redux Toolkit](https://redux-toolkit.js.org/) to handle Redux boilerplate code.
|
||||
> Some of our Reducers are used by Angular and therefore state is to be considered as mutable for those reducers.
|
||||
|
||||
## Test functionality
|
||||
|
||||
|
@ -204,6 +204,7 @@
|
||||
"@babel/polyfill": "7.6.0",
|
||||
"@braintree/sanitize-url": "4.0.0",
|
||||
"@grafana/slate-react": "0.22.9-grafana",
|
||||
"@reduxjs/toolkit": "1.2.1",
|
||||
"@torkelo/react-select": "2.4.1",
|
||||
"@types/react-loadable": "5.5.2",
|
||||
"angular": "1.6.9",
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { AppNotification } from 'app/types/';
|
||||
|
||||
export enum ActionTypes {
|
||||
AddAppNotification = 'ADD_APP_NOTIFICATION',
|
||||
ClearAppNotification = 'CLEAR_APP_NOTIFICATION',
|
||||
}
|
||||
|
||||
interface AddAppNotificationAction {
|
||||
type: ActionTypes.AddAppNotification;
|
||||
payload: AppNotification;
|
||||
}
|
||||
|
||||
interface ClearAppNotificationAction {
|
||||
type: ActionTypes.ClearAppNotification;
|
||||
payload: number;
|
||||
}
|
||||
|
||||
export type Action = AddAppNotificationAction | ClearAppNotificationAction;
|
||||
|
||||
export const clearAppNotification = (appNotificationId: number) => ({
|
||||
type: ActionTypes.ClearAppNotification,
|
||||
payload: appNotificationId,
|
||||
});
|
||||
|
||||
export const notifyApp = (appNotification: AppNotification) => ({
|
||||
type: ActionTypes.AddAppNotification,
|
||||
payload: appNotification,
|
||||
});
|
@ -1,3 +0,0 @@
|
||||
import { actionCreatorFactory } from 'app/core/redux';
|
||||
|
||||
export const toggleLogActions = actionCreatorFactory('TOGGLE_LOG_ACTIONS').create();
|
@ -1,10 +1,11 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { StoreState } from '../../types';
|
||||
import { actionCreatorFactory } from '../redux';
|
||||
|
||||
export type StateSelector<T extends object> = (state: StoreState) => T;
|
||||
export type StateSelector<T> = (state: StoreState) => T;
|
||||
|
||||
export interface CleanUp<T extends object> {
|
||||
stateSelector: StateSelector<T>;
|
||||
export interface CleanUp<T> {
|
||||
stateSelector: (state: StoreState) => T;
|
||||
}
|
||||
|
||||
export const cleanUpAction = actionCreatorFactory<CleanUp<{}>>('CORE_CLEAN_UP_STATE').create();
|
||||
export const cleanUpAction = createAction<CleanUp<{}>>('core/cleanUpState');
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { updateLocation } from './location';
|
||||
import { updateNavIndex, UpdateNavIndexAction } from './navModel';
|
||||
import { notifyApp, clearAppNotification } from './appNotification';
|
||||
import { clearAppNotification, notifyApp } from '../reducers/appNotification';
|
||||
import { updateLocation } from '../reducers/location';
|
||||
import { updateNavIndex } from '../reducers/navModel';
|
||||
|
||||
export { updateLocation, updateNavIndex, UpdateNavIndexAction, notifyApp, clearAppNotification };
|
||||
export { updateLocation, updateNavIndex, notifyApp, clearAppNotification };
|
||||
|
@ -1,4 +0,0 @@
|
||||
import { LocationUpdate } from '@grafana/runtime';
|
||||
import { actionCreatorFactory } from 'app/core/redux';
|
||||
|
||||
export const updateLocation = actionCreatorFactory<LocationUpdate>('UPDATE_LOCATION').create();
|
@ -1,17 +0,0 @@
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
|
||||
export enum ActionTypes {
|
||||
UpdateNavIndex = 'UPDATE_NAV_INDEX',
|
||||
}
|
||||
|
||||
export type Action = UpdateNavIndexAction;
|
||||
|
||||
export interface UpdateNavIndexAction {
|
||||
type: ActionTypes.UpdateNavIndex;
|
||||
payload: NavModelItem;
|
||||
}
|
||||
|
||||
export const updateNavIndex = (item: NavModelItem): UpdateNavIndexAction => ({
|
||||
type: ActionTypes.UpdateNavIndex,
|
||||
payload: item,
|
||||
});
|
@ -1,9 +1,9 @@
|
||||
import { Store, Dispatch } from 'redux';
|
||||
import { StoreState } from 'app/types/store';
|
||||
import { ActionOf } from '../redux/actionCreatorFactory';
|
||||
import { toggleLogActions } from '../actions/application';
|
||||
import { AnyAction, Dispatch, Store } from 'redux';
|
||||
|
||||
export const toggleLogActionsMiddleware = (store: Store<StoreState>) => (next: Dispatch) => (action: ActionOf<any>) => {
|
||||
import { StoreState } from 'app/types/store';
|
||||
import { toggleLogActions } from '../reducers/application';
|
||||
|
||||
export const toggleLogActionsMiddleware = (store: Store<StoreState>) => (next: Dispatch) => (action: AnyAction) => {
|
||||
const isLogActionsAction = action.type === toggleLogActions.type;
|
||||
if (isLogActionsAction) {
|
||||
return next(action);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { appNotificationsReducer } from './appNotification';
|
||||
import { ActionTypes } from '../actions/appNotification';
|
||||
import { appNotificationsReducer, clearAppNotification, notifyApp } from './appNotification';
|
||||
import { AppNotificationSeverity, AppNotificationTimeout } from 'app/types/';
|
||||
|
||||
describe('clear alert', () => {
|
||||
@ -28,10 +27,7 @@ describe('clear alert', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const result = appNotificationsReducer(initialState, {
|
||||
type: ActionTypes.ClearAppNotification,
|
||||
payload: id2,
|
||||
});
|
||||
const result = appNotificationsReducer(initialState, clearAppNotification(id2));
|
||||
|
||||
const expectedResult = {
|
||||
appNotifications: [
|
||||
@ -49,3 +45,75 @@ describe('clear alert', () => {
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('notify', () => {
|
||||
it('create notify message', () => {
|
||||
const id1 = 1540301236048;
|
||||
const id2 = 1540301248293;
|
||||
const id3 = 1540301248203;
|
||||
|
||||
const initialState = {
|
||||
appNotifications: [
|
||||
{
|
||||
id: id1,
|
||||
severity: AppNotificationSeverity.Success,
|
||||
icon: 'success',
|
||||
title: 'test',
|
||||
text: 'test alert',
|
||||
timeout: AppNotificationTimeout.Success,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
severity: AppNotificationSeverity.Warning,
|
||||
icon: 'warning',
|
||||
title: 'test2',
|
||||
text: 'test alert fail 2',
|
||||
timeout: AppNotificationTimeout.Warning,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = appNotificationsReducer(
|
||||
initialState,
|
||||
notifyApp({
|
||||
id: id3,
|
||||
severity: AppNotificationSeverity.Info,
|
||||
icon: 'info',
|
||||
title: 'test3',
|
||||
text: 'test alert info 3',
|
||||
timeout: AppNotificationTimeout.Success,
|
||||
})
|
||||
);
|
||||
|
||||
const expectedResult = {
|
||||
appNotifications: [
|
||||
{
|
||||
id: id1,
|
||||
severity: AppNotificationSeverity.Success,
|
||||
icon: 'success',
|
||||
title: 'test',
|
||||
text: 'test alert',
|
||||
timeout: AppNotificationTimeout.Success,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
severity: AppNotificationSeverity.Warning,
|
||||
icon: 'warning',
|
||||
title: 'test2',
|
||||
text: 'test alert fail 2',
|
||||
timeout: AppNotificationTimeout.Warning,
|
||||
},
|
||||
{
|
||||
id: id3,
|
||||
severity: AppNotificationSeverity.Info,
|
||||
icon: 'info',
|
||||
title: 'test3',
|
||||
text: 'test alert info 3',
|
||||
timeout: AppNotificationTimeout.Success,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
@ -1,19 +1,25 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { AppNotification, AppNotificationsState } from 'app/types/';
|
||||
import { Action, ActionTypes } from '../actions/appNotification';
|
||||
|
||||
export const initialState: AppNotificationsState = {
|
||||
appNotifications: [] as AppNotification[],
|
||||
};
|
||||
|
||||
export const appNotificationsReducer = (state = initialState, action: Action): AppNotificationsState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.AddAppNotification:
|
||||
return { ...state, appNotifications: state.appNotifications.concat([action.payload]) };
|
||||
case ActionTypes.ClearAppNotification:
|
||||
return {
|
||||
...state,
|
||||
appNotifications: state.appNotifications.filter(appNotification => appNotification.id !== action.payload),
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
const appNotificationsSlice = createSlice({
|
||||
name: 'appNotifications',
|
||||
initialState,
|
||||
reducers: {
|
||||
notifyApp: (state, action: PayloadAction<AppNotification>): AppNotificationsState => ({
|
||||
...state,
|
||||
appNotifications: state.appNotifications.concat([action.payload]),
|
||||
}),
|
||||
clearAppNotification: (state, action: PayloadAction<number>): AppNotificationsState => ({
|
||||
...state,
|
||||
appNotifications: state.appNotifications.filter(appNotification => appNotification.id !== action.payload),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const { notifyApp, clearAppNotification } = appNotificationsSlice.actions;
|
||||
|
||||
export const appNotificationsReducer = appNotificationsSlice.reducer;
|
||||
|
16
public/app/core/reducers/application.test.ts
Normal file
16
public/app/core/reducers/application.test.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { reducerTester } from '../../../test/core/redux/reducerTester';
|
||||
import { applicationReducer, toggleLogActions } from './application';
|
||||
import { ApplicationState } from '../../types/application';
|
||||
|
||||
describe('applicationReducer', () => {
|
||||
describe('when toggleLogActions is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<ApplicationState>()
|
||||
.givenReducer(applicationReducer, { logActions: false })
|
||||
.whenActionIsDispatched(toggleLogActions())
|
||||
.thenStateShouldEqual({ logActions: true })
|
||||
.whenActionIsDispatched(toggleLogActions())
|
||||
.thenStateShouldEqual({ logActions: false });
|
||||
});
|
||||
});
|
||||
});
|
@ -1,17 +1,17 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { ApplicationState } from 'app/types/application';
|
||||
import { reducerFactory } from 'app/core/redux';
|
||||
import { toggleLogActions } from '../actions/application';
|
||||
|
||||
export const initialState: ApplicationState = {
|
||||
logActions: false,
|
||||
};
|
||||
|
||||
export const applicationReducer = reducerFactory<ApplicationState>(initialState)
|
||||
.addMapper({
|
||||
filter: toggleLogActions,
|
||||
mapper: (state): ApplicationState => ({
|
||||
...state,
|
||||
logActions: !state.logActions,
|
||||
}),
|
||||
})
|
||||
.create();
|
||||
const applicationSlice = createSlice({
|
||||
name: 'application',
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleLogActions: state => ({ ...state, logActions: !state.logActions }),
|
||||
},
|
||||
});
|
||||
|
||||
export const { toggleLogActions } = applicationSlice.actions;
|
||||
export const applicationReducer = applicationSlice.reducer;
|
||||
|
154
public/app/core/reducers/location.test.ts
Normal file
154
public/app/core/reducers/location.test.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import { reducerTester } from '../../../test/core/redux/reducerTester';
|
||||
import { initialState, locationReducer, updateLocation } from './location';
|
||||
import { LocationState } from '../../types';
|
||||
|
||||
describe('locationReducer', () => {
|
||||
describe('when updateLocation is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LocationState>()
|
||||
.givenReducer(locationReducer, { ...initialState, query: { queryParam: 3, queryParam2: 2 } })
|
||||
.whenActionIsDispatched(
|
||||
updateLocation({
|
||||
query: { queryParam: 1 },
|
||||
partial: false,
|
||||
path: '/api/dashboard',
|
||||
replace: false,
|
||||
routeParams: { routeParam: 2 },
|
||||
})
|
||||
)
|
||||
.thenStatePredicateShouldEqual(resultingState => {
|
||||
expect(resultingState.path).toEqual('/api/dashboard');
|
||||
expect(resultingState.url).toEqual('/api/dashboard?queryParam=1');
|
||||
expect(resultingState.query).toEqual({ queryParam: 1 });
|
||||
expect(resultingState.routeParams).toEqual({ routeParam: 2 });
|
||||
expect(resultingState.replace).toEqual(false);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateLocation is dispatched with replace', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LocationState>()
|
||||
.givenReducer(locationReducer, { ...initialState, query: { queryParam: 3, queryParam2: 2 } })
|
||||
.whenActionIsDispatched(
|
||||
updateLocation({
|
||||
query: { queryParam: 1 },
|
||||
partial: false,
|
||||
path: '/api/dashboard',
|
||||
replace: true,
|
||||
routeParams: { routeParam: 2 },
|
||||
})
|
||||
)
|
||||
.thenStatePredicateShouldEqual(resultingState => {
|
||||
expect(resultingState.path).toEqual('/api/dashboard');
|
||||
expect(resultingState.url).toEqual('/api/dashboard?queryParam=1');
|
||||
expect(resultingState.query).toEqual({ queryParam: 1 });
|
||||
expect(resultingState.routeParams).toEqual({ routeParam: 2 });
|
||||
expect(resultingState.replace).toEqual(true);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateLocation is dispatched with partial', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LocationState>()
|
||||
.givenReducer(locationReducer, { ...initialState, query: { queryParam: 3, queryParam2: 2 } })
|
||||
.whenActionIsDispatched(
|
||||
updateLocation({
|
||||
query: { queryParam: 1 },
|
||||
partial: true,
|
||||
path: '/api/dashboard',
|
||||
replace: false,
|
||||
routeParams: { routeParam: 2 },
|
||||
})
|
||||
)
|
||||
.thenStatePredicateShouldEqual(resultingState => {
|
||||
expect(resultingState.path).toEqual('/api/dashboard');
|
||||
expect(resultingState.url).toEqual('/api/dashboard?queryParam=1&queryParam2=2');
|
||||
expect(resultingState.query).toEqual({ queryParam: 1, queryParam2: 2 });
|
||||
expect(resultingState.routeParams).toEqual({ routeParam: 2 });
|
||||
expect(resultingState.replace).toEqual(false);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateLocation is dispatched without query', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LocationState>()
|
||||
.givenReducer(locationReducer, { ...initialState, query: { queryParam: 3, queryParam2: 2 } })
|
||||
.whenActionIsDispatched(
|
||||
updateLocation({
|
||||
partial: false,
|
||||
path: '/api/dashboard',
|
||||
replace: false,
|
||||
routeParams: { routeParam: 2 },
|
||||
})
|
||||
)
|
||||
.thenStatePredicateShouldEqual(resultingState => {
|
||||
expect(resultingState.path).toEqual('/api/dashboard');
|
||||
expect(resultingState.url).toEqual('/api/dashboard?queryParam=3&queryParam2=2');
|
||||
expect(resultingState.query).toEqual({ queryParam: 3, queryParam2: 2 });
|
||||
expect(resultingState.routeParams).toEqual({ routeParam: 2 });
|
||||
expect(resultingState.replace).toEqual(false);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateLocation is dispatched without routeParams', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LocationState>()
|
||||
.givenReducer(locationReducer, {
|
||||
...initialState,
|
||||
query: { queryParam: 3, queryParam2: 2 },
|
||||
routeParams: { routeStateParam: 4 },
|
||||
})
|
||||
.whenActionIsDispatched(
|
||||
updateLocation({
|
||||
query: { queryParam: 1 },
|
||||
partial: false,
|
||||
path: '/api/dashboard',
|
||||
replace: false,
|
||||
})
|
||||
)
|
||||
.thenStatePredicateShouldEqual(resultingState => {
|
||||
expect(resultingState.path).toEqual('/api/dashboard');
|
||||
expect(resultingState.url).toEqual('/api/dashboard?queryParam=1');
|
||||
expect(resultingState.query).toEqual({ queryParam: 1 });
|
||||
expect(resultingState.routeParams).toEqual({ routeStateParam: 4 });
|
||||
expect(resultingState.replace).toEqual(false);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateLocation is dispatched without path', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LocationState>()
|
||||
.givenReducer(locationReducer, {
|
||||
...initialState,
|
||||
query: { queryParam: 3, queryParam2: 2 },
|
||||
path: '/api/state/path',
|
||||
})
|
||||
.whenActionIsDispatched(
|
||||
updateLocation({
|
||||
query: { queryParam: 1 },
|
||||
partial: false,
|
||||
replace: false,
|
||||
routeParams: { routeParam: 2 },
|
||||
})
|
||||
)
|
||||
.thenStatePredicateShouldEqual(resultingState => {
|
||||
expect(resultingState.path).toEqual('/api/state/path');
|
||||
expect(resultingState.url).toEqual('/api/state/path?queryParam=1');
|
||||
expect(resultingState.query).toEqual({ queryParam: 1 });
|
||||
expect(resultingState.routeParams).toEqual({ routeParam: 2 });
|
||||
expect(resultingState.replace).toEqual(false);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,8 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import { Action, createAction } from '@reduxjs/toolkit';
|
||||
import { LocationUpdate } from '@grafana/runtime';
|
||||
|
||||
import { LocationState } from 'app/types';
|
||||
import { renderUrl } from 'app/core/utils/url';
|
||||
import _ from 'lodash';
|
||||
import { reducerFactory } from 'app/core/redux';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
export const initialState: LocationState = {
|
||||
url: '',
|
||||
@ -13,26 +14,33 @@ export const initialState: LocationState = {
|
||||
lastUpdated: 0,
|
||||
};
|
||||
|
||||
export const locationReducer = reducerFactory<LocationState>(initialState)
|
||||
.addMapper({
|
||||
filter: updateLocation,
|
||||
mapper: (state, action): LocationState => {
|
||||
const { path, routeParams, replace } = action.payload;
|
||||
let query = action.payload.query || state.query;
|
||||
export const updateLocation = createAction<LocationUpdate>('location/updateLocation');
|
||||
|
||||
if (action.payload.partial) {
|
||||
query = _.defaults(query, state.query);
|
||||
query = _.omitBy(query, _.isNull);
|
||||
}
|
||||
// Redux Toolkit uses ImmerJs as part of their solution to ensure that state objects are not mutated.
|
||||
// ImmerJs has an autoFreeze option that freezes objects from change which means this reducer can't be migrated to createSlice
|
||||
// because the state would become frozen and during run time we would get errors because Angular would try to mutate
|
||||
// the frozen state.
|
||||
// https://github.com/reduxjs/redux-toolkit/issues/242
|
||||
export const locationReducer = (state: LocationState = initialState, action: Action<unknown>) => {
|
||||
if (updateLocation.match(action)) {
|
||||
const payload: LocationUpdate = action.payload;
|
||||
const { path, routeParams, replace } = payload;
|
||||
let query = payload.query || state.query;
|
||||
|
||||
return {
|
||||
url: renderUrl(path || state.path, query),
|
||||
path: path || state.path,
|
||||
query: { ...query },
|
||||
routeParams: routeParams || state.routeParams,
|
||||
replace: replace === true,
|
||||
lastUpdated: new Date().getTime(),
|
||||
};
|
||||
},
|
||||
})
|
||||
.create();
|
||||
if (payload.partial) {
|
||||
query = _.defaults(query, state.query);
|
||||
query = _.omitBy(query, _.isNull);
|
||||
}
|
||||
|
||||
return {
|
||||
url: renderUrl(path || state.path, query),
|
||||
path: path || state.path,
|
||||
query: { ...query },
|
||||
routeParams: routeParams || state.routeParams,
|
||||
replace: replace === true,
|
||||
lastUpdated: new Date().getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
40
public/app/core/reducers/navModel.test.ts
Normal file
40
public/app/core/reducers/navModel.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { reducerTester } from '../../../test/core/redux/reducerTester';
|
||||
import { initialState, navIndexReducer, updateNavIndex } from './navModel';
|
||||
|
||||
describe('applicationReducer', () => {
|
||||
describe('when updateNavIndex is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester()
|
||||
.givenReducer(navIndexReducer, { ...initialState })
|
||||
.whenActionIsDispatched(
|
||||
updateNavIndex({
|
||||
id: 'parent',
|
||||
text: 'Some Text',
|
||||
children: [
|
||||
{
|
||||
id: 'child',
|
||||
text: 'Child',
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
child: {
|
||||
id: 'child',
|
||||
text: 'Child',
|
||||
parentItem: {
|
||||
id: 'parent',
|
||||
text: 'Some Text',
|
||||
children: [
|
||||
{
|
||||
id: 'child',
|
||||
text: 'Child',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import { Action, ActionTypes } from 'app/core/actions/navModel';
|
||||
import { AnyAction, createAction } from '@reduxjs/toolkit';
|
||||
import { NavIndex, NavModelItem } from '@grafana/data';
|
||||
|
||||
import config from 'app/core/config';
|
||||
|
||||
export function buildInitialState(): NavIndex {
|
||||
@ -22,22 +23,29 @@ function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?
|
||||
}
|
||||
}
|
||||
|
||||
export const initialState: NavIndex = buildInitialState();
|
||||
export const initialState: NavIndex = {};
|
||||
|
||||
export const navIndexReducer = (state = initialState, action: Action): NavIndex => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.UpdateNavIndex:
|
||||
const newPages: NavIndex = {};
|
||||
const payload = action.payload;
|
||||
export const updateNavIndex = createAction<NavModelItem>('navIndex/updateNavIndex');
|
||||
|
||||
for (const node of payload.children) {
|
||||
newPages[node.id] = {
|
||||
...node,
|
||||
parentItem: payload,
|
||||
};
|
||||
}
|
||||
// Redux Toolkit uses ImmerJs as part of their solution to ensure that state objects are not mutated.
|
||||
// ImmerJs has an autoFreeze option that freezes objects from change which means this reducer can't be migrated to createSlice
|
||||
// because the state would become frozen and during run time we would get errors because Angular would try to mutate
|
||||
// the frozen state.
|
||||
// https://github.com/reduxjs/redux-toolkit/issues/242
|
||||
export const navIndexReducer = (state: NavIndex = initialState, action: AnyAction): NavIndex => {
|
||||
if (updateNavIndex.match(action)) {
|
||||
const newPages: NavIndex = {};
|
||||
const payload = action.payload;
|
||||
|
||||
return { ...state, ...newPages };
|
||||
for (const node of payload.children) {
|
||||
newPages[node.id] = {
|
||||
...node,
|
||||
parentItem: payload,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...state, ...newPages };
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
@ -3,10 +3,9 @@ import { describe, expect } from '../../../test/lib/common';
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { reducerTester } from '../../../test/core/redux/reducerTester';
|
||||
import { StoreState } from '../../types/store';
|
||||
import { ActionTypes } from '../../features/teams/state/actions';
|
||||
import { Team } from '../../types';
|
||||
import { cleanUpAction } from '../actions/cleanUp';
|
||||
import { initialTeamsState } from '../../features/teams/state/reducers';
|
||||
import { initialTeamsState, teamsLoaded } from '../../features/teams/state/reducers';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
config: {
|
||||
@ -56,17 +55,14 @@ describe('rootReducer', () => {
|
||||
|
||||
describe('when called with any action except cleanUpAction', () => {
|
||||
it('then it should not clean state', () => {
|
||||
const teams = [{ id: 1 }];
|
||||
const teams = [{ id: 1 } as Team];
|
||||
const state = {
|
||||
teams: { ...initialTeamsState },
|
||||
} as StoreState;
|
||||
|
||||
reducerTester<StoreState>()
|
||||
.givenReducer(rootReducer, state)
|
||||
.whenActionIsDispatched({
|
||||
type: ActionTypes.LoadTeams,
|
||||
payload: teams,
|
||||
})
|
||||
.whenActionIsDispatched(teamsLoaded(teams))
|
||||
.thenStatePredicateShouldEqual(resultingState => {
|
||||
expect(resultingState.teams).toEqual({
|
||||
hasFetched: true,
|
||||
@ -91,7 +87,7 @@ describe('rootReducer', () => {
|
||||
|
||||
reducerTester<StoreState>()
|
||||
.givenReducer(rootReducer, state, true)
|
||||
.whenActionIsDispatched(cleanUpAction({ stateSelector: storeState => storeState.teams }))
|
||||
.whenActionIsDispatched(cleanUpAction({ stateSelector: (storeState: StoreState) => storeState.teams }))
|
||||
.thenStatePredicateShouldEqual(resultingState => {
|
||||
expect(resultingState.teams).toEqual({ ...initialTeamsState });
|
||||
return true;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { ActionOf } from '../redux';
|
||||
import { AnyAction, combineReducers } from 'redux';
|
||||
import { CleanUp, cleanUpAction } from '../actions/cleanUp';
|
||||
import sharedReducers from 'app/core/reducers';
|
||||
import alertingReducers from 'app/features/alerting/state/reducers';
|
||||
@ -43,7 +42,7 @@ export const createRootReducer = () => {
|
||||
...addedReducers,
|
||||
});
|
||||
|
||||
return (state: any, action: ActionOf<any>): any => {
|
||||
return (state: any, action: AnyAction): any => {
|
||||
if (action.type !== cleanUpAction.type) {
|
||||
return appReducer(state, action);
|
||||
}
|
||||
@ -58,13 +57,18 @@ export const createRootReducer = () => {
|
||||
|
||||
export const recursiveCleanState = (state: any, stateSlice: any): boolean => {
|
||||
for (const stateKey in state) {
|
||||
if (state[stateKey] === stateSlice) {
|
||||
if (!state.hasOwnProperty(stateKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const slice = state[stateKey];
|
||||
if (slice === stateSlice) {
|
||||
state[stateKey] = undefined;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof state[stateKey] === 'object') {
|
||||
const cleaned = recursiveCleanState(state[stateKey], stateSlice);
|
||||
if (typeof slice === 'object') {
|
||||
const cleaned = recursiveCleanState(slice, stateSlice);
|
||||
if (cleaned) {
|
||||
return true;
|
||||
}
|
||||
|
@ -1,79 +0,0 @@
|
||||
import { actionCreatorFactory, resetAllActionCreatorTypes } from './actionCreatorFactory';
|
||||
|
||||
interface Dummy {
|
||||
n: number;
|
||||
s: string;
|
||||
o: {
|
||||
n: number;
|
||||
s: string;
|
||||
b: boolean;
|
||||
};
|
||||
b: boolean;
|
||||
}
|
||||
|
||||
const setup = (payload?: Dummy) => {
|
||||
resetAllActionCreatorTypes();
|
||||
const actionCreator = actionCreatorFactory<Dummy>('dummy').create();
|
||||
const noPayloadactionCreator = actionCreatorFactory('NoPayload').create();
|
||||
const result = actionCreator(payload);
|
||||
const noPayloadResult = noPayloadactionCreator();
|
||||
|
||||
return { actionCreator, noPayloadactionCreator, result, noPayloadResult };
|
||||
};
|
||||
|
||||
describe('actionCreatorFactory', () => {
|
||||
describe('when calling create', () => {
|
||||
it('then it should create correct type string', () => {
|
||||
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } };
|
||||
const { actionCreator, result } = setup(payload);
|
||||
|
||||
expect(actionCreator.type).toEqual('dummy');
|
||||
expect(result.type).toEqual('dummy');
|
||||
});
|
||||
|
||||
it('then it should create correct payload', () => {
|
||||
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } };
|
||||
const { result } = setup(payload);
|
||||
|
||||
expect(result.payload).toEqual(payload);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when calling create with existing type', () => {
|
||||
it('then it should throw error', () => {
|
||||
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } };
|
||||
setup(payload);
|
||||
|
||||
expect(() => {
|
||||
actionCreatorFactory('DuMmY').create();
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('noPayloadActionCreatorFactory', () => {
|
||||
describe('when calling create', () => {
|
||||
it('then it should create correct type string', () => {
|
||||
const { noPayloadResult, noPayloadactionCreator } = setup();
|
||||
|
||||
expect(noPayloadactionCreator.type).toEqual('NoPayload');
|
||||
expect(noPayloadResult.type).toEqual('NoPayload');
|
||||
});
|
||||
|
||||
it('then it should create correct payload', () => {
|
||||
const { noPayloadResult } = setup();
|
||||
|
||||
expect(noPayloadResult.payload).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when calling create with existing type', () => {
|
||||
it('then it should throw error', () => {
|
||||
setup();
|
||||
|
||||
expect(() => {
|
||||
actionCreatorFactory<Dummy>('nOpAyLoAd').create();
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,64 +0,0 @@
|
||||
import { Action } from 'redux';
|
||||
|
||||
const allActionCreators = new Set<string>();
|
||||
|
||||
export interface ActionOf<Payload> extends Action {
|
||||
readonly type: string;
|
||||
readonly payload: Payload;
|
||||
}
|
||||
|
||||
export interface ActionCreator<Payload> {
|
||||
readonly type: string;
|
||||
(payload: Payload): ActionOf<Payload>;
|
||||
}
|
||||
|
||||
export interface NoPayloadActionCreator {
|
||||
readonly type: string;
|
||||
(): ActionOf<undefined>;
|
||||
}
|
||||
|
||||
export interface ActionCreatorFactory<Payload> {
|
||||
create: () => ActionCreator<Payload>;
|
||||
}
|
||||
|
||||
export interface NoPayloadActionCreatorFactory {
|
||||
create: () => NoPayloadActionCreator;
|
||||
}
|
||||
|
||||
export function actionCreatorFactory<Payload extends undefined>(type: string): NoPayloadActionCreatorFactory;
|
||||
export function actionCreatorFactory<Payload>(type: string): ActionCreatorFactory<Payload>;
|
||||
export function actionCreatorFactory<Payload>(type: string): ActionCreatorFactory<Payload> {
|
||||
const upperCaseType = type.toLocaleUpperCase();
|
||||
if (allActionCreators.has(upperCaseType)) {
|
||||
throw new Error(`An actionCreator with type '${type}' has already been defined.`);
|
||||
}
|
||||
|
||||
allActionCreators.add(upperCaseType);
|
||||
|
||||
const create = (): ActionCreator<Payload> => {
|
||||
return Object.assign((payload: Payload): ActionOf<Payload> => ({ type, payload }), { type });
|
||||
};
|
||||
return { create };
|
||||
}
|
||||
|
||||
export interface NoPayloadActionCreatorMock extends NoPayloadActionCreator {
|
||||
calls: number;
|
||||
}
|
||||
|
||||
export const getNoPayloadActionCreatorMock = (creator: NoPayloadActionCreator): NoPayloadActionCreatorMock => {
|
||||
const mock: NoPayloadActionCreatorMock = Object.assign(
|
||||
(): ActionOf<undefined> => {
|
||||
mock.calls++;
|
||||
return { type: creator.type, payload: undefined };
|
||||
},
|
||||
{ type: creator.type, calls: 0 }
|
||||
);
|
||||
return mock;
|
||||
};
|
||||
|
||||
export const mockActionCreator = (creator: ActionCreator<any>) => {
|
||||
return Object.assign(jest.fn(), creator);
|
||||
};
|
||||
|
||||
// Should only be used by tests
|
||||
export const resetAllActionCreatorTypes = () => allActionCreators.clear();
|
@ -1,2 +0,0 @@
|
||||
export * from './actionCreatorFactory';
|
||||
export * from './reducerFactory';
|
@ -1,97 +0,0 @@
|
||||
import { reducerFactory } from './reducerFactory';
|
||||
import { actionCreatorFactory, ActionOf } from './actionCreatorFactory';
|
||||
|
||||
interface DummyReducerState {
|
||||
n: number;
|
||||
s: string;
|
||||
b: boolean;
|
||||
o: {
|
||||
n: number;
|
||||
s: string;
|
||||
b: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const dummyReducerIntialState: DummyReducerState = {
|
||||
n: 1,
|
||||
s: 'One',
|
||||
b: true,
|
||||
o: {
|
||||
n: 2,
|
||||
s: 'two',
|
||||
b: false,
|
||||
},
|
||||
};
|
||||
|
||||
const dummyActionCreator = actionCreatorFactory<DummyReducerState>('dummy').create();
|
||||
|
||||
const dummyReducer = reducerFactory(dummyReducerIntialState)
|
||||
.addMapper({
|
||||
filter: dummyActionCreator,
|
||||
mapper: (state, action) => ({ ...state, ...action.payload }),
|
||||
})
|
||||
.create();
|
||||
|
||||
describe('reducerFactory', () => {
|
||||
describe('given it is created with a defined handler', () => {
|
||||
describe('when reducer is called with no state', () => {
|
||||
describe('and with an action that the handler can not handle', () => {
|
||||
it('then the resulting state should be intial state', () => {
|
||||
const result = dummyReducer(undefined as DummyReducerState, {} as ActionOf<any>);
|
||||
|
||||
expect(result).toEqual(dummyReducerIntialState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with an action that the handler can handle', () => {
|
||||
it('then the resulting state should correct', () => {
|
||||
const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } };
|
||||
const result = dummyReducer(undefined as DummyReducerState, dummyActionCreator(payload));
|
||||
|
||||
expect(result).toEqual(payload);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when reducer is called with a state', () => {
|
||||
describe('and with an action that the handler can not handle', () => {
|
||||
it('then the resulting state should be intial state', () => {
|
||||
const result = dummyReducer(dummyReducerIntialState, {} as ActionOf<any>);
|
||||
|
||||
expect(result).toEqual(dummyReducerIntialState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with an action that the handler can handle', () => {
|
||||
it('then the resulting state should correct', () => {
|
||||
const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } };
|
||||
const result = dummyReducer(dummyReducerIntialState, dummyActionCreator(payload));
|
||||
|
||||
expect(result).toEqual(payload);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a handler is added', () => {
|
||||
describe('when a handler with the same creator is added', () => {
|
||||
it('then is should throw', () => {
|
||||
const faultyReducer = reducerFactory(dummyReducerIntialState).addMapper({
|
||||
filter: dummyActionCreator,
|
||||
mapper: (state, action) => {
|
||||
return { ...state, ...action.payload };
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
faultyReducer.addMapper({
|
||||
filter: dummyActionCreator,
|
||||
mapper: state => {
|
||||
return state;
|
||||
},
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,45 +0,0 @@
|
||||
import { ActionOf, ActionCreator } from './actionCreatorFactory';
|
||||
import { Reducer } from 'redux';
|
||||
|
||||
export type Mapper<State, Payload> = (state: State, action: ActionOf<Payload>) => State;
|
||||
|
||||
export interface MapperConfig<State, Payload> {
|
||||
filter: ActionCreator<Payload>;
|
||||
mapper: Mapper<State, Payload>;
|
||||
}
|
||||
|
||||
export interface AddMapper<State> {
|
||||
addMapper: <Payload>(config: MapperConfig<State, Payload>) => CreateReducer<State>;
|
||||
}
|
||||
|
||||
export interface CreateReducer<State> extends AddMapper<State> {
|
||||
create: () => Reducer<State, ActionOf<any>>;
|
||||
}
|
||||
|
||||
export const reducerFactory = <State>(initialState: State): AddMapper<State> => {
|
||||
const allMappers: { [key: string]: Mapper<State, any> } = {};
|
||||
|
||||
const addMapper = <Payload>(config: MapperConfig<State, Payload>): CreateReducer<State> => {
|
||||
if (allMappers[config.filter.type]) {
|
||||
throw new Error(`There is already a mapper defined with the type ${config.filter.type}`);
|
||||
}
|
||||
|
||||
allMappers[config.filter.type] = config.mapper;
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
const create = (): Reducer<State, ActionOf<any>> => (state: State = initialState, action: ActionOf<any>): State => {
|
||||
const mapper = allMappers[action.type];
|
||||
|
||||
if (mapper) {
|
||||
return mapper(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const instance: CreateReducer<State> = { addMapper, create };
|
||||
|
||||
return instance;
|
||||
};
|
@ -1,34 +1,27 @@
|
||||
import { actionCreatorFactory } from 'app/core/redux';
|
||||
import config from 'app/core/config';
|
||||
import { ThunkResult, SyncInfo, LdapUser, LdapConnectionInfo, LdapError, UserSession, User } from 'app/types';
|
||||
import { ThunkResult } from 'app/types';
|
||||
import {
|
||||
getUserInfo,
|
||||
getLdapState,
|
||||
syncLdapUser,
|
||||
getUser,
|
||||
getUserSessions,
|
||||
revokeUserSession,
|
||||
revokeAllUserSessions,
|
||||
getLdapSyncStatus,
|
||||
getUser,
|
||||
getUserInfo,
|
||||
getUserSessions,
|
||||
revokeAllUserSessions,
|
||||
revokeUserSession,
|
||||
syncLdapUser,
|
||||
} from './apis';
|
||||
|
||||
// Action types
|
||||
|
||||
export const ldapConnectionInfoLoadedAction = actionCreatorFactory<LdapConnectionInfo>(
|
||||
'ldap/CONNECTION_INFO_LOADED'
|
||||
).create();
|
||||
export const ldapSyncStatusLoadedAction = actionCreatorFactory<SyncInfo>('ldap/SYNC_STATUS_LOADED').create();
|
||||
export const userMappingInfoLoadedAction = actionCreatorFactory<LdapUser>('ldap/USER_INFO_LOADED').create();
|
||||
export const userMappingInfoFailedAction = actionCreatorFactory<LdapError>('ldap/USER_INFO_FAILED').create();
|
||||
export const clearUserMappingInfoAction = actionCreatorFactory('ldap/CLEAR_USER_MAPPING_INFO').create();
|
||||
export const clearUserErrorAction = actionCreatorFactory('ldap/CLEAR_USER_ERROR').create();
|
||||
export const ldapFailedAction = actionCreatorFactory<LdapError>('ldap/LDAP_FAILED').create();
|
||||
|
||||
export const userLoadedAction = actionCreatorFactory<User>('USER_LOADED').create();
|
||||
export const userSessionsLoadedAction = actionCreatorFactory<UserSession[]>('USER_SESSIONS_LOADED').create();
|
||||
export const userSyncFailedAction = actionCreatorFactory('USER_SYNC_FAILED').create();
|
||||
export const revokeUserSessionAction = actionCreatorFactory('REVOKE_USER_SESSION').create();
|
||||
export const revokeAllUserSessionsAction = actionCreatorFactory('REVOKE_ALL_USER_SESSIONS').create();
|
||||
import {
|
||||
clearUserErrorAction,
|
||||
clearUserMappingInfoAction,
|
||||
ldapConnectionInfoLoadedAction,
|
||||
ldapFailedAction,
|
||||
ldapSyncStatusLoadedAction,
|
||||
userLoadedAction,
|
||||
userMappingInfoFailedAction,
|
||||
userMappingInfoLoadedAction,
|
||||
userSessionsLoadedAction,
|
||||
userSyncFailedAction,
|
||||
} from './reducers';
|
||||
|
||||
// Actions
|
||||
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { Reducer } from 'redux';
|
||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
import { ldapReducer, ldapUserReducer } from './reducers';
|
||||
import {
|
||||
clearUserErrorAction,
|
||||
clearUserMappingInfoAction,
|
||||
ldapConnectionInfoLoadedAction,
|
||||
ldapSyncStatusLoadedAction,
|
||||
userMappingInfoLoadedAction,
|
||||
userMappingInfoFailedAction,
|
||||
ldapFailedAction,
|
||||
ldapReducer,
|
||||
ldapSyncStatusLoadedAction,
|
||||
ldapUserReducer,
|
||||
userLoadedAction,
|
||||
} from './actions';
|
||||
import { LdapState, LdapUserState, LdapUser, User } from 'app/types';
|
||||
userMappingInfoFailedAction,
|
||||
userMappingInfoLoadedAction,
|
||||
userSessionsLoadedAction,
|
||||
} from './reducers';
|
||||
import { LdapState, LdapUser, LdapUserState, User } from 'app/types';
|
||||
|
||||
const makeInitialLdapState = (): LdapState => ({
|
||||
connectionInfo: [],
|
||||
@ -56,12 +58,12 @@ describe('LDAP page reducer', () => {
|
||||
describe('When page loaded', () => {
|
||||
describe('When connection info loaded', () => {
|
||||
it('should set connection info and clear error', () => {
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
...makeInitialLdapState(),
|
||||
};
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(ldapReducer as Reducer<LdapState, ActionOf<any>>, initalState)
|
||||
reducerTester<LdapState>()
|
||||
.givenReducer(ldapReducer, initialState)
|
||||
.whenActionIsDispatched(
|
||||
ldapConnectionInfoLoadedAction([
|
||||
{
|
||||
@ -89,12 +91,12 @@ describe('LDAP page reducer', () => {
|
||||
|
||||
describe('When connection failed', () => {
|
||||
it('should set ldap error', () => {
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
...makeInitialLdapState(),
|
||||
};
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(ldapReducer as Reducer<LdapState, ActionOf<any>>, initalState)
|
||||
reducerTester<LdapState>()
|
||||
.givenReducer(ldapReducer, initialState)
|
||||
.whenActionIsDispatched(
|
||||
ldapFailedAction({
|
||||
title: 'LDAP error',
|
||||
@ -113,12 +115,12 @@ describe('LDAP page reducer', () => {
|
||||
|
||||
describe('When LDAP sync status loaded', () => {
|
||||
it('should set sync info', () => {
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
...makeInitialLdapState(),
|
||||
};
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(ldapReducer as Reducer<LdapState, ActionOf<any>>, initalState)
|
||||
reducerTester<LdapState>()
|
||||
.givenReducer(ldapReducer, initialState)
|
||||
.whenActionIsDispatched(
|
||||
ldapSyncStatusLoadedAction({
|
||||
enabled: true,
|
||||
@ -140,7 +142,7 @@ describe('LDAP page reducer', () => {
|
||||
|
||||
describe('When user mapping info loaded', () => {
|
||||
it('should set sync info and clear user error', () => {
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
...makeInitialLdapState(),
|
||||
userError: {
|
||||
title: 'User not found',
|
||||
@ -148,8 +150,8 @@ describe('LDAP page reducer', () => {
|
||||
},
|
||||
};
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(ldapReducer as Reducer<LdapState, ActionOf<any>>, initalState)
|
||||
reducerTester<LdapState>()
|
||||
.givenReducer(ldapReducer, initialState)
|
||||
.whenActionIsDispatched(userMappingInfoLoadedAction(getTestUserMapping()))
|
||||
.thenStateShouldEqual({
|
||||
...makeInitialLdapState(),
|
||||
@ -161,13 +163,13 @@ describe('LDAP page reducer', () => {
|
||||
|
||||
describe('When user not found', () => {
|
||||
it('should set user error and clear user info', () => {
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
...makeInitialLdapState(),
|
||||
user: getTestUserMapping(),
|
||||
};
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(ldapReducer as Reducer<LdapState, ActionOf<any>>, initalState)
|
||||
reducerTester<LdapState>()
|
||||
.givenReducer(ldapReducer, initialState)
|
||||
.whenActionIsDispatched(
|
||||
userMappingInfoFailedAction({
|
||||
title: 'User not found',
|
||||
@ -184,12 +186,27 @@ describe('LDAP page reducer', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when clearUserMappingInfoAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LdapState>()
|
||||
.givenReducer(ldapReducer, {
|
||||
...makeInitialLdapState(),
|
||||
user: getTestUserMapping(),
|
||||
})
|
||||
.whenActionIsDispatched(clearUserMappingInfoAction())
|
||||
.thenStateShouldEqual({
|
||||
...makeInitialLdapState(),
|
||||
user: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit LDAP user page reducer', () => {
|
||||
describe('When user loaded', () => {
|
||||
it('should set user and clear user error', () => {
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
...makeInitialLdapUserState(),
|
||||
userError: {
|
||||
title: 'User not found',
|
||||
@ -197,8 +214,8 @@ describe('Edit LDAP user page reducer', () => {
|
||||
},
|
||||
};
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(ldapUserReducer as Reducer<LdapUserState, ActionOf<any>>, initalState)
|
||||
reducerTester<LdapUserState>()
|
||||
.givenReducer(ldapUserReducer, initialState)
|
||||
.whenActionIsDispatched(userLoadedAction(getTestUser()))
|
||||
.thenStateShouldEqual({
|
||||
...makeInitialLdapUserState(),
|
||||
@ -207,4 +224,120 @@ describe('Edit LDAP user page reducer', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when userSessionsLoadedAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LdapUserState>()
|
||||
.givenReducer(ldapUserReducer, { ...makeInitialLdapUserState() })
|
||||
.whenActionIsDispatched(
|
||||
userSessionsLoadedAction([
|
||||
{
|
||||
browser: 'Chrome',
|
||||
id: 1,
|
||||
browserVersion: '79',
|
||||
clientIp: '127.0.0.1',
|
||||
createdAt: '2020-01-01 00:00:00',
|
||||
device: 'a device',
|
||||
isActive: true,
|
||||
os: 'MacOS',
|
||||
osVersion: '15',
|
||||
seenAt: '2020-01-01 00:00:00',
|
||||
},
|
||||
])
|
||||
)
|
||||
.thenStateShouldEqual({
|
||||
...makeInitialLdapUserState(),
|
||||
sessions: [
|
||||
{
|
||||
browser: 'Chrome',
|
||||
id: 1,
|
||||
browserVersion: '79',
|
||||
clientIp: '127.0.0.1',
|
||||
createdAt: '2020-01-01 00:00:00',
|
||||
device: 'a device',
|
||||
isActive: true,
|
||||
os: 'MacOS',
|
||||
osVersion: '15',
|
||||
seenAt: '2020-01-01 00:00:00',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when userMappingInfoLoadedAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LdapUserState>()
|
||||
.givenReducer(ldapUserReducer, {
|
||||
...makeInitialLdapUserState(),
|
||||
})
|
||||
.whenActionIsDispatched(userMappingInfoLoadedAction(getTestUserMapping()))
|
||||
.thenStateShouldEqual({
|
||||
...makeInitialLdapUserState(),
|
||||
ldapUser: getTestUserMapping(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when userMappingInfoFailedAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LdapUserState>()
|
||||
.givenReducer(ldapUserReducer, { ...makeInitialLdapUserState() })
|
||||
.whenActionIsDispatched(
|
||||
userMappingInfoFailedAction({
|
||||
title: 'User not found',
|
||||
body: 'Cannot find user',
|
||||
})
|
||||
)
|
||||
.thenStateShouldEqual({
|
||||
...makeInitialLdapUserState(),
|
||||
userError: {
|
||||
title: 'User not found',
|
||||
body: 'Cannot find user',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when clearUserErrorAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LdapUserState>()
|
||||
.givenReducer(ldapUserReducer, {
|
||||
...makeInitialLdapUserState(),
|
||||
userError: {
|
||||
title: 'User not found',
|
||||
body: 'Cannot find user',
|
||||
},
|
||||
})
|
||||
.whenActionIsDispatched(clearUserErrorAction())
|
||||
.thenStateShouldEqual({
|
||||
...makeInitialLdapUserState(),
|
||||
userError: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when ldapSyncStatusLoadedAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<LdapUserState>()
|
||||
.givenReducer(ldapUserReducer, {
|
||||
...makeInitialLdapUserState(),
|
||||
})
|
||||
.whenActionIsDispatched(
|
||||
ldapSyncStatusLoadedAction({
|
||||
enabled: true,
|
||||
schedule: '0 0 * * * *',
|
||||
nextSync: '2019-01-01T12:00:00Z',
|
||||
})
|
||||
)
|
||||
.thenStateShouldEqual({
|
||||
...makeInitialLdapUserState(),
|
||||
ldapSyncInfo: {
|
||||
enabled: true,
|
||||
schedule: '0 0 * * * *',
|
||||
nextSync: '2019-01-01T12:00:00Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,16 +1,14 @@
|
||||
import { reducerFactory } from 'app/core/redux';
|
||||
import { LdapState, LdapUserState } from 'app/types';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import {
|
||||
ldapConnectionInfoLoadedAction,
|
||||
ldapFailedAction,
|
||||
userMappingInfoLoadedAction,
|
||||
userMappingInfoFailedAction,
|
||||
clearUserErrorAction,
|
||||
userLoadedAction,
|
||||
userSessionsLoadedAction,
|
||||
ldapSyncStatusLoadedAction,
|
||||
clearUserMappingInfoAction,
|
||||
} from './actions';
|
||||
LdapConnectionInfo,
|
||||
LdapError,
|
||||
LdapState,
|
||||
LdapUser,
|
||||
LdapUserState,
|
||||
SyncInfo,
|
||||
User,
|
||||
UserSession,
|
||||
} from 'app/types';
|
||||
|
||||
const initialLdapState: LdapState = {
|
||||
connectionInfo: [],
|
||||
@ -27,107 +25,107 @@ const initialLdapUserState: LdapUserState = {
|
||||
sessions: [],
|
||||
};
|
||||
|
||||
export const ldapReducer = reducerFactory(initialLdapState)
|
||||
.addMapper({
|
||||
filter: ldapConnectionInfoLoadedAction,
|
||||
mapper: (state, action) => ({
|
||||
const ldapSlice = createSlice({
|
||||
name: 'ldap',
|
||||
initialState: initialLdapState,
|
||||
reducers: {
|
||||
ldapConnectionInfoLoadedAction: (state, action: PayloadAction<LdapConnectionInfo>): LdapState => ({
|
||||
...state,
|
||||
ldapError: null,
|
||||
connectionInfo: action.payload,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: ldapFailedAction,
|
||||
mapper: (state, action) => ({
|
||||
ldapFailedAction: (state, action: PayloadAction<LdapError>): LdapState => ({
|
||||
...state,
|
||||
ldapError: action.payload,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: ldapSyncStatusLoadedAction,
|
||||
mapper: (state, action) => ({
|
||||
ldapSyncStatusLoadedAction: (state, action: PayloadAction<SyncInfo>): LdapState => ({
|
||||
...state,
|
||||
syncInfo: action.payload,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: userMappingInfoLoadedAction,
|
||||
mapper: (state, action) => ({
|
||||
userMappingInfoLoadedAction: (state, action: PayloadAction<LdapUser>): LdapState => ({
|
||||
...state,
|
||||
user: action.payload,
|
||||
userError: null,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: userMappingInfoFailedAction,
|
||||
mapper: (state, action) => ({
|
||||
userMappingInfoFailedAction: (state, action: PayloadAction<LdapError>): LdapState => ({
|
||||
...state,
|
||||
user: null,
|
||||
userError: action.payload,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: clearUserMappingInfoAction,
|
||||
mapper: (state, action) => ({
|
||||
clearUserMappingInfoAction: (state, action: PayloadAction<undefined>): LdapState => ({
|
||||
...state,
|
||||
user: null,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: clearUserErrorAction,
|
||||
mapper: state => ({
|
||||
clearUserErrorAction: (state, action: PayloadAction<undefined>): LdapState => ({
|
||||
...state,
|
||||
userError: null,
|
||||
}),
|
||||
})
|
||||
.create();
|
||||
},
|
||||
});
|
||||
|
||||
export const ldapUserReducer = reducerFactory(initialLdapUserState)
|
||||
.addMapper({
|
||||
filter: userMappingInfoLoadedAction,
|
||||
mapper: (state, action) => ({
|
||||
...state,
|
||||
ldapUser: action.payload,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: userMappingInfoFailedAction,
|
||||
mapper: (state, action) => ({
|
||||
...state,
|
||||
ldapUser: null,
|
||||
userError: action.payload,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: clearUserErrorAction,
|
||||
mapper: state => ({
|
||||
...state,
|
||||
userError: null,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: ldapSyncStatusLoadedAction,
|
||||
mapper: (state, action) => ({
|
||||
...state,
|
||||
ldapSyncInfo: action.payload,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: userLoadedAction,
|
||||
mapper: (state, action) => ({
|
||||
export const {
|
||||
clearUserErrorAction,
|
||||
clearUserMappingInfoAction,
|
||||
ldapConnectionInfoLoadedAction,
|
||||
ldapFailedAction,
|
||||
ldapSyncStatusLoadedAction,
|
||||
userMappingInfoFailedAction,
|
||||
userMappingInfoLoadedAction,
|
||||
} = ldapSlice.actions;
|
||||
|
||||
export const ldapReducer = ldapSlice.reducer;
|
||||
|
||||
const ldapUserSlice = createSlice({
|
||||
name: 'ldapUser',
|
||||
initialState: initialLdapUserState,
|
||||
reducers: {
|
||||
userLoadedAction: (state, action: PayloadAction<User>): LdapUserState => ({
|
||||
...state,
|
||||
user: action.payload,
|
||||
userError: null,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: userSessionsLoadedAction,
|
||||
mapper: (state, action) => ({
|
||||
userSessionsLoadedAction: (state, action: PayloadAction<UserSession[]>): LdapUserState => ({
|
||||
...state,
|
||||
sessions: action.payload,
|
||||
}),
|
||||
})
|
||||
.create();
|
||||
userSyncFailedAction: (state, action: PayloadAction<undefined>): LdapUserState => state,
|
||||
},
|
||||
extraReducers: builder =>
|
||||
builder
|
||||
.addCase(
|
||||
userMappingInfoLoadedAction,
|
||||
(state, action): LdapUserState => ({
|
||||
...state,
|
||||
ldapUser: action.payload,
|
||||
})
|
||||
)
|
||||
.addCase(
|
||||
userMappingInfoFailedAction,
|
||||
(state, action): LdapUserState => ({
|
||||
...state,
|
||||
ldapUser: null,
|
||||
userError: action.payload,
|
||||
})
|
||||
)
|
||||
.addCase(
|
||||
clearUserErrorAction,
|
||||
(state, action): LdapUserState => ({
|
||||
...state,
|
||||
userError: null,
|
||||
})
|
||||
)
|
||||
.addCase(
|
||||
ldapSyncStatusLoadedAction,
|
||||
(state, action): LdapUserState => ({
|
||||
...state,
|
||||
ldapSyncInfo: action.payload,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export const { userLoadedAction, userSessionsLoadedAction, userSyncFailedAction } = ldapUserSlice.actions;
|
||||
|
||||
export const ldapUserReducer = ldapUserSlice.reducer;
|
||||
|
||||
export default {
|
||||
ldap: ldapReducer,
|
||||
|
@ -3,10 +3,11 @@ import { shallow } from 'enzyme';
|
||||
import { AlertRuleList, Props } from './AlertRuleList';
|
||||
import { AlertRule } from '../../types';
|
||||
import appEvents from '../../core/app_events';
|
||||
import { mockActionCreator } from 'app/core/redux';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { updateLocation } from '../../core/actions';
|
||||
import { setSearchQuery } from './state/reducers';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
|
||||
jest.mock('../../core/app_events', () => ({
|
||||
emit: jest.fn(),
|
||||
@ -16,9 +17,9 @@ const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
navModel: {} as NavModel,
|
||||
alertRules: [] as AlertRule[],
|
||||
updateLocation: mockActionCreator(updateLocation),
|
||||
updateLocation: mockToolkitActionCreator(updateLocation),
|
||||
getAlertRulesAsync: jest.fn(),
|
||||
setSearchQuery: jest.fn(),
|
||||
setSearchQuery: mockToolkitActionCreator(setSearchQuery),
|
||||
togglePauseAlertRule: jest.fn(),
|
||||
stateFilter: '',
|
||||
search: '',
|
||||
|
@ -6,11 +6,12 @@ import AlertRuleItem from './AlertRuleItem';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { StoreState, AlertRule, CoreEvents } from 'app/types';
|
||||
import { getAlertRulesAsync, setSearchQuery, togglePauseAlertRule } from './state/actions';
|
||||
import { AlertRule, CoreEvents, StoreState } from 'app/types';
|
||||
import { getAlertRulesAsync, togglePauseAlertRule } from './state/actions';
|
||||
import { getAlertRuleItems, getSearchQuery } from './state/selectors';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { setSearchQuery } from './state/reducers';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
|
@ -1,44 +1,6 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { AlertRuleDTO, StoreState } from 'app/types';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadAlertRules = 'LOAD_ALERT_RULES',
|
||||
LoadedAlertRules = 'LOADED_ALERT_RULES',
|
||||
SetSearchQuery = 'SET_ALERT_SEARCH_QUERY',
|
||||
}
|
||||
|
||||
export interface LoadAlertRulesAction {
|
||||
type: ActionTypes.LoadAlertRules;
|
||||
}
|
||||
|
||||
export interface LoadedAlertRulesAction {
|
||||
type: ActionTypes.LoadedAlertRules;
|
||||
payload: AlertRuleDTO[];
|
||||
}
|
||||
|
||||
export interface SetSearchQueryAction {
|
||||
type: ActionTypes.SetSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export const loadAlertRules = (): LoadAlertRulesAction => ({
|
||||
type: ActionTypes.LoadAlertRules,
|
||||
});
|
||||
|
||||
export const loadedAlertRules = (rules: AlertRuleDTO[]): LoadedAlertRulesAction => ({
|
||||
type: ActionTypes.LoadedAlertRules,
|
||||
payload: rules,
|
||||
});
|
||||
|
||||
export const setSearchQuery = (query: string): SetSearchQueryAction => ({
|
||||
type: ActionTypes.SetSearchQuery,
|
||||
payload: query,
|
||||
});
|
||||
|
||||
export type Action = LoadAlertRulesAction | LoadedAlertRulesAction | SetSearchQueryAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||
import { AlertRuleDTO, ThunkResult } from 'app/types';
|
||||
import { loadAlertRules, loadedAlertRules } from './reducers';
|
||||
|
||||
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ActionTypes, Action } from './actions';
|
||||
import { alertRulesReducer, initialState } from './reducers';
|
||||
import { AlertRuleDTO } from 'app/types';
|
||||
import { alertRulesReducer, initialState, loadAlertRules, loadedAlertRules, setSearchQuery } from './reducers';
|
||||
import { AlertRuleDTO, AlertRulesState } from 'app/types';
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
|
||||
describe('Alert rules', () => {
|
||||
const payload: AlertRuleDTO[] = [
|
||||
@ -78,14 +78,137 @@ describe('Alert rules', () => {
|
||||
},
|
||||
];
|
||||
|
||||
it('should set alert rules', () => {
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadedAlertRules,
|
||||
payload: payload,
|
||||
};
|
||||
describe('when loadAlertRules is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<AlertRulesState>()
|
||||
.givenReducer(alertRulesReducer, { ...initialState })
|
||||
.whenActionIsDispatched(loadAlertRules())
|
||||
.thenStateShouldEqual({ ...initialState, isLoading: true });
|
||||
});
|
||||
});
|
||||
|
||||
const result = alertRulesReducer(initialState, action);
|
||||
expect(result.items.length).toEqual(payload.length);
|
||||
expect(result.items[0].stateClass).toEqual('alert-state-critical');
|
||||
describe('when setSearchQuery is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<AlertRulesState>()
|
||||
.givenReducer(alertRulesReducer, { ...initialState })
|
||||
.whenActionIsDispatched(setSearchQuery('query'))
|
||||
.thenStateShouldEqual({ ...initialState, searchQuery: 'query' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loadedAlertRules is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<AlertRulesState>()
|
||||
.givenReducer(alertRulesReducer, { ...initialState, isLoading: true })
|
||||
.whenActionIsDispatched(loadedAlertRules(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
isLoading: false,
|
||||
items: [
|
||||
{
|
||||
dashboardId: 7,
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
evalData: {
|
||||
evalMatches: [
|
||||
{
|
||||
metric: 'A-series',
|
||||
tags: null,
|
||||
value: 215,
|
||||
},
|
||||
],
|
||||
},
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
executionError: '',
|
||||
id: 2,
|
||||
name: 'TestData - Always Alerting',
|
||||
newStateDate: '2018-09-04T10:00:30+02:00',
|
||||
panelId: 4,
|
||||
state: 'alerting',
|
||||
stateAge: 'a year',
|
||||
stateClass: 'alert-state-critical',
|
||||
stateIcon: 'icon-gf icon-gf-critical',
|
||||
stateText: 'ALERTING',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
{
|
||||
dashboardId: 7,
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
evalData: {},
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
executionError: '',
|
||||
id: 1,
|
||||
name: 'TestData - Always OK',
|
||||
newStateDate: '2018-09-04T10:01:01+02:00',
|
||||
panelId: 3,
|
||||
state: 'ok',
|
||||
stateAge: 'a year',
|
||||
stateClass: 'alert-state-ok',
|
||||
stateIcon: 'icon-gf icon-gf-online',
|
||||
stateText: 'OK',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
{
|
||||
dashboardId: 7,
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
evalData: {},
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
executionError: 'error',
|
||||
id: 3,
|
||||
info: 'Execution Error: error',
|
||||
name: 'TestData - ok',
|
||||
newStateDate: '2018-09-04T10:01:01+02:00',
|
||||
panelId: 3,
|
||||
state: 'ok',
|
||||
stateAge: 'a year',
|
||||
stateClass: 'alert-state-ok',
|
||||
stateIcon: 'icon-gf icon-gf-online',
|
||||
stateText: 'OK',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
{
|
||||
dashboardId: 7,
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
evalData: {},
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
executionError: 'error',
|
||||
id: 4,
|
||||
name: 'TestData - Paused',
|
||||
newStateDate: '2018-09-04T10:01:01+02:00',
|
||||
panelId: 3,
|
||||
state: 'paused',
|
||||
stateAge: 'a year',
|
||||
stateClass: 'alert-state-paused',
|
||||
stateIcon: 'fa fa-pause',
|
||||
stateText: 'PAUSED',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
{
|
||||
dashboardId: 7,
|
||||
dashboardSlug: 'alerting-with-testdata',
|
||||
dashboardUid: 'ggHbN42mk',
|
||||
evalData: {
|
||||
noData: true,
|
||||
},
|
||||
evalDate: '0001-01-01T00:00:00Z',
|
||||
executionError: 'error',
|
||||
id: 5,
|
||||
info: 'Query returned no data',
|
||||
name: 'TestData - Ok',
|
||||
newStateDate: '2018-09-04T10:01:01+02:00',
|
||||
panelId: 3,
|
||||
state: 'ok',
|
||||
stateAge: 'a year',
|
||||
stateClass: 'alert-state-ok',
|
||||
stateIcon: 'icon-gf icon-gf-online',
|
||||
stateText: 'OK',
|
||||
url: '/d/ggHbN42mk/alerting-with-testdata',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AlertRuleDTO, AlertRule, AlertRulesState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { AlertRule, AlertRuleDTO, AlertRulesState } from 'app/types';
|
||||
import alertDef from './alertDef';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const initialState: AlertRulesState = { items: [], searchQuery: '', isLoading: false };
|
||||
|
||||
@ -28,13 +28,14 @@ function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule {
|
||||
return rule;
|
||||
}
|
||||
|
||||
export const alertRulesReducer = (state = initialState, action: Action): AlertRulesState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadAlertRules: {
|
||||
const alertRulesSlice = createSlice({
|
||||
name: 'alertRules',
|
||||
initialState,
|
||||
reducers: {
|
||||
loadAlertRules: state => {
|
||||
return { ...state, isLoading: true };
|
||||
}
|
||||
|
||||
case ActionTypes.LoadedAlertRules: {
|
||||
},
|
||||
loadedAlertRules: (state, action: PayloadAction<AlertRuleDTO[]>): AlertRulesState => {
|
||||
const alertRules: AlertRuleDTO[] = action.payload;
|
||||
|
||||
const alertRulesViewModel: AlertRule[] = alertRules.map(rule => {
|
||||
@ -42,14 +43,16 @@ export const alertRulesReducer = (state = initialState, action: Action): AlertRu
|
||||
});
|
||||
|
||||
return { ...state, items: alertRulesViewModel, isLoading: false };
|
||||
}
|
||||
|
||||
case ActionTypes.SetSearchQuery:
|
||||
},
|
||||
setSearchQuery: (state, action: PayloadAction<string>): AlertRulesState => {
|
||||
return { ...state, searchQuery: action.payload };
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
export const { loadAlertRules, loadedAlertRules, setSearchQuery } = alertRulesSlice.actions;
|
||||
|
||||
export const alertRulesReducer = alertRulesSlice.reducer;
|
||||
|
||||
export default {
|
||||
alertRules: alertRulesReducer,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Props, ApiKeysPage } from './ApiKeysPage';
|
||||
import { ApiKeysPage, Props } from './ApiKeysPage';
|
||||
import { ApiKey } from 'app/types';
|
||||
import { getMultipleMockKeys, getMockKey } from './__mocks__/apiKeysMock';
|
||||
import { getMockKey, getMultipleMockKeys } from './__mocks__/apiKeysMock';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { setSearchQuery } from './state/reducers';
|
||||
import { mockToolkitActionCreator } from '../../../test/core/redux/mocks';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
@ -20,7 +22,7 @@ const setup = (propOverrides?: object) => {
|
||||
hasFetched: false,
|
||||
loadApiKeys: jest.fn(),
|
||||
deleteApiKey: jest.fn(),
|
||||
setSearchQuery: jest.fn(),
|
||||
setSearchQuery: mockToolkitActionCreator(setSearchQuery),
|
||||
addApiKey: jest.fn(),
|
||||
apiKeysCount: 0,
|
||||
includeExpired: false,
|
||||
|
@ -2,25 +2,24 @@ import React, { PureComponent } from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { ApiKey, NewApiKey, OrgRole } from 'app/types';
|
||||
// Utils
|
||||
import { ApiKey, CoreEvents, NewApiKey, OrgRole } from 'app/types';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { getApiKeys, getApiKeysCount } from './state/selectors';
|
||||
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
|
||||
import { addApiKey, deleteApiKey, loadApiKeys } from './state/actions';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { SlideDown } from 'app/core/components/Animations/SlideDown';
|
||||
import ApiKeysAddedModal from './ApiKeysAddedModal';
|
||||
import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { EventsWithValidation, FormLabel, Input, Switch, ValidationEvents, DeleteButton } from '@grafana/ui';
|
||||
import { NavModel, dateTime, isDateTime } from '@grafana/data';
|
||||
import { DeleteButton, EventsWithValidation, FormLabel, Input, Switch, ValidationEvents } from '@grafana/ui';
|
||||
import { dateTime, isDateTime, NavModel } from '@grafana/data';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { store } from 'app/store/store';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
// Utils
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||
import { setSearchQuery } from './state/reducers';
|
||||
|
||||
const timeRangeValidationEvents: ValidationEvents = {
|
||||
[EventsWithValidation.onBlur]: [
|
||||
|
@ -1,30 +1,6 @@
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { StoreState, ApiKey } from 'app/types';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadApiKeys = 'LOAD_API_KEYS',
|
||||
SetApiKeysSearchQuery = 'SET_API_KEYS_SEARCH_QUERY',
|
||||
}
|
||||
|
||||
export interface LoadApiKeysAction {
|
||||
type: ActionTypes.LoadApiKeys;
|
||||
payload: ApiKey[];
|
||||
}
|
||||
|
||||
export interface SetSearchQueryAction {
|
||||
type: ActionTypes.SetApiKeysSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export type Action = LoadApiKeysAction | SetSearchQueryAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||
|
||||
const apiKeysLoaded = (apiKeys: ApiKey[]): LoadApiKeysAction => ({
|
||||
type: ActionTypes.LoadApiKeys,
|
||||
payload: apiKeys,
|
||||
});
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { ApiKey, ThunkResult } from 'app/types';
|
||||
import { apiKeysLoaded, setSearchQuery } from './reducers';
|
||||
|
||||
export function addApiKey(
|
||||
apiKey: ApiKey,
|
||||
@ -53,8 +29,3 @@ export function deleteApiKey(id: number, includeExpired: boolean): ThunkResult<v
|
||||
.then(dispatch(loadApiKeys(includeExpired)));
|
||||
};
|
||||
}
|
||||
|
||||
export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({
|
||||
type: ActionTypes.SetApiKeysSearchQuery,
|
||||
payload: searchQuery,
|
||||
});
|
||||
|
@ -1,31 +1,27 @@
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { initialApiKeysState, apiKeysReducer } from './reducers';
|
||||
import { apiKeysLoaded, apiKeysReducer, initialApiKeysState, setSearchQuery } from './reducers';
|
||||
import { getMultipleMockKeys } from '../__mocks__/apiKeysMock';
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { ApiKeysState } from '../../../types';
|
||||
|
||||
describe('API Keys reducer', () => {
|
||||
it('should set keys', () => {
|
||||
const payload = getMultipleMockKeys(4);
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadApiKeys,
|
||||
payload,
|
||||
};
|
||||
|
||||
const result = apiKeysReducer(initialApiKeysState, action);
|
||||
|
||||
expect(result.keys).toEqual(payload);
|
||||
reducerTester<ApiKeysState>()
|
||||
.givenReducer(apiKeysReducer, { ...initialApiKeysState })
|
||||
.whenActionIsDispatched(apiKeysLoaded(getMultipleMockKeys(4)))
|
||||
.thenStateShouldEqual({
|
||||
...initialApiKeysState,
|
||||
keys: getMultipleMockKeys(4),
|
||||
hasFetched: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set search query', () => {
|
||||
const payload = 'test query';
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.SetApiKeysSearchQuery,
|
||||
payload,
|
||||
};
|
||||
|
||||
const result = apiKeysReducer(initialApiKeysState, action);
|
||||
|
||||
expect(result.searchQuery).toEqual('test query');
|
||||
reducerTester<ApiKeysState>()
|
||||
.givenReducer(apiKeysReducer, { ...initialApiKeysState })
|
||||
.whenActionIsDispatched(setSearchQuery('test query'))
|
||||
.thenStateShouldEqual({
|
||||
...initialApiKeysState,
|
||||
searchQuery: 'test query',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ApiKeysState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { ApiKeysState } from 'app/types';
|
||||
|
||||
export const initialApiKeysState: ApiKeysState = {
|
||||
keys: [],
|
||||
@ -8,15 +9,22 @@ export const initialApiKeysState: ApiKeysState = {
|
||||
includeExpired: false,
|
||||
};
|
||||
|
||||
export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadApiKeys:
|
||||
const apiKeysSlice = createSlice({
|
||||
name: 'apiKeys',
|
||||
initialState: initialApiKeysState,
|
||||
reducers: {
|
||||
apiKeysLoaded: (state, action): ApiKeysState => {
|
||||
return { ...state, hasFetched: true, keys: action.payload };
|
||||
case ActionTypes.SetApiKeysSearchQuery:
|
||||
},
|
||||
setSearchQuery: (state, action): ApiKeysState => {
|
||||
return { ...state, searchQuery: action.payload };
|
||||
}
|
||||
return state;
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setSearchQuery, apiKeysLoaded } = apiKeysSlice.actions;
|
||||
|
||||
export const apiKeysReducer = apiKeysSlice.reducer;
|
||||
|
||||
export default {
|
||||
apiKeys: apiKeysReducer,
|
||||
|
@ -1,16 +1,20 @@
|
||||
import React from 'react';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { DashboardPage, Props, State, mapStateToProps } from './DashboardPage';
|
||||
import { DashboardPage, mapStateToProps, Props, State } from './DashboardPage';
|
||||
import { DashboardModel } from '../state';
|
||||
import { cleanUpDashboard } from '../state/actions';
|
||||
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock, mockActionCreator } from 'app/core/redux';
|
||||
import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import {
|
||||
mockToolkitActionCreator,
|
||||
mockToolkitActionCreatorWithoutPayload,
|
||||
ToolkitActionCreatorWithoutPayloadMockType,
|
||||
} from 'test/core/redux/mocks';
|
||||
import { DashboardInitPhase, DashboardRouteInfo } from 'app/types';
|
||||
import { notifyApp, updateLocation } from 'app/core/actions';
|
||||
|
||||
jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({}));
|
||||
|
||||
interface ScenarioContext {
|
||||
cleanUpDashboardMock: NoPayloadActionCreatorMock;
|
||||
cleanUpDashboardMock: ToolkitActionCreatorWithoutPayloadMockType;
|
||||
dashboard?: DashboardModel;
|
||||
setDashboardProp: (overrides?: any, metaOverrides?: any) => void;
|
||||
wrapper?: ShallowWrapper<Props, State, DashboardPage>;
|
||||
@ -43,7 +47,7 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
|
||||
let setupFn: () => void;
|
||||
|
||||
const ctx: ScenarioContext = {
|
||||
cleanUpDashboardMock: getNoPayloadActionCreatorMock(cleanUpDashboard),
|
||||
cleanUpDashboardMock: mockToolkitActionCreatorWithoutPayload(cleanUpDashboard),
|
||||
setup: fn => {
|
||||
setupFn = fn;
|
||||
},
|
||||
@ -63,8 +67,8 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
|
||||
initPhase: DashboardInitPhase.NotStarted,
|
||||
isInitSlow: false,
|
||||
initDashboard: jest.fn(),
|
||||
updateLocation: mockActionCreator(updateLocation),
|
||||
notifyApp: jest.fn(),
|
||||
updateLocation: mockToolkitActionCreator(updateLocation),
|
||||
notifyApp: mockToolkitActionCreator(notifyApp),
|
||||
cleanUpDashboard: ctx.cleanUpDashboardMock,
|
||||
dashboard: null,
|
||||
};
|
||||
@ -243,7 +247,7 @@ describe('DashboardPage', () => {
|
||||
});
|
||||
|
||||
it('Should call clean up action', () => {
|
||||
expect(ctx.cleanUpDashboardMock.calls).toBe(1);
|
||||
expect(ctx.cleanUpDashboardMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -14,8 +14,8 @@ import { AlertTab } from '../../alerting/AlertTab';
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
import { StoreState } from '../../../types';
|
||||
import { PanelEditorTab, PanelEditorTabIds } from './state/reducers';
|
||||
import { changePanelEditorTab, panelEditorCleanUp, refreshPanelEditor } from './state/actions';
|
||||
import { panelEditorCleanUp, PanelEditorTab, PanelEditorTabIds } from './state/reducers';
|
||||
import { changePanelEditorTab, refreshPanelEditor } from './state/actions';
|
||||
import { getActiveTabAndTabs } from './state/selectors';
|
||||
|
||||
interface PanelEditorProps {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { thunkTester } from '../../../../../test/core/thunk/thunkTester';
|
||||
import { initialState, getPanelEditorTab, PanelEditorTabIds } from './reducers';
|
||||
import { refreshPanelEditor, panelEditorInitCompleted, changePanelEditorTab } from './actions';
|
||||
import { getPanelEditorTab, initialState, panelEditorInitCompleted, PanelEditorTabIds } from './reducers';
|
||||
import { changePanelEditorTab, refreshPanelEditor } from './actions';
|
||||
import { updateLocation } from '../../../../core/actions';
|
||||
|
||||
describe('refreshPanelEditor', () => {
|
||||
|
@ -1,19 +1,7 @@
|
||||
import { actionCreatorFactory } from '../../../../core/redux';
|
||||
import { PanelEditorTabIds, PanelEditorTab, getPanelEditorTab } from './reducers';
|
||||
import { getPanelEditorTab, panelEditorInitCompleted, PanelEditorTab, PanelEditorTabIds } from './reducers';
|
||||
import { ThunkResult } from '../../../../types';
|
||||
import { updateLocation } from '../../../../core/actions';
|
||||
|
||||
export interface PanelEditorInitCompleted {
|
||||
activeTab: PanelEditorTabIds;
|
||||
tabs: PanelEditorTab[];
|
||||
}
|
||||
|
||||
export const panelEditorInitCompleted = actionCreatorFactory<PanelEditorInitCompleted>(
|
||||
'PANEL_EDITOR_INIT_COMPLETED'
|
||||
).create();
|
||||
|
||||
export const panelEditorCleanUp = actionCreatorFactory('PANEL_EDITOR_CLEAN_UP').create();
|
||||
|
||||
export const refreshPanelEditor = (props: {
|
||||
hasQueriesTab?: boolean;
|
||||
usesGraphPlugin?: boolean;
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { reducerTester } from '../../../../../test/core/redux/reducerTester';
|
||||
import { initialState, panelEditorReducer, PanelEditorTabIds, PanelEditorTab, getPanelEditorTab } from './reducers';
|
||||
import { panelEditorInitCompleted, panelEditorCleanUp } from './actions';
|
||||
import {
|
||||
getPanelEditorTab,
|
||||
initialState,
|
||||
panelEditorCleanUp,
|
||||
panelEditorInitCompleted,
|
||||
panelEditorReducer,
|
||||
PanelEditorTab,
|
||||
PanelEditorTabIds,
|
||||
} from './reducers';
|
||||
|
||||
describe('panelEditorReducer', () => {
|
||||
describe('when panelEditorInitCompleted is dispatched', () => {
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { reducerFactory } from '../../../../core/redux';
|
||||
import { panelEditorCleanUp, panelEditorInitCompleted } from './actions';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export interface PanelEditorInitCompleted {
|
||||
activeTab: PanelEditorTabIds;
|
||||
tabs: PanelEditorTab[];
|
||||
}
|
||||
|
||||
export interface PanelEditorTab {
|
||||
id: string;
|
||||
@ -37,10 +41,11 @@ export const initialState: PanelEditorState = {
|
||||
tabs: [],
|
||||
};
|
||||
|
||||
export const panelEditorReducer = reducerFactory<PanelEditorState>(initialState)
|
||||
.addMapper({
|
||||
filter: panelEditorInitCompleted,
|
||||
mapper: (state, action): PanelEditorState => {
|
||||
const panelEditorSlice = createSlice({
|
||||
name: 'panelEditor',
|
||||
initialState,
|
||||
reducers: {
|
||||
panelEditorInitCompleted: (state, action: PayloadAction<PanelEditorInitCompleted>): PanelEditorState => {
|
||||
const { activeTab, tabs } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
@ -48,9 +53,10 @@ export const panelEditorReducer = reducerFactory<PanelEditorState>(initialState)
|
||||
tabs,
|
||||
};
|
||||
},
|
||||
})
|
||||
.addMapper({
|
||||
filter: panelEditorCleanUp,
|
||||
mapper: (): PanelEditorState => initialState,
|
||||
})
|
||||
.create();
|
||||
panelEditorCleanUp: (state, action: PayloadAction<undefined>): PanelEditorState => initialState,
|
||||
},
|
||||
});
|
||||
|
||||
export const { panelEditorCleanUp, panelEditorInitCompleted } = panelEditorSlice.actions;
|
||||
|
||||
export const panelEditorReducer = panelEditorSlice.reducer;
|
||||
|
@ -1,41 +1,41 @@
|
||||
// Services & Utils
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { actionCreatorFactory } from 'app/core/redux';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
// Actions
|
||||
import { loadPluginDashboards } from '../../plugins/state/actions';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
// Types
|
||||
import {
|
||||
ThunkResult,
|
||||
DashboardAcl,
|
||||
DashboardAclDTO,
|
||||
PermissionLevel,
|
||||
DashboardAclUpdateDTO,
|
||||
NewDashboardAclItem,
|
||||
MutableDashboard,
|
||||
DashboardInitError,
|
||||
MutableDashboard,
|
||||
NewDashboardAclItem,
|
||||
PermissionLevel,
|
||||
ThunkResult,
|
||||
} from 'app/types';
|
||||
|
||||
export const loadDashboardPermissions = actionCreatorFactory<DashboardAclDTO[]>('LOAD_DASHBOARD_PERMISSIONS').create();
|
||||
export const loadDashboardPermissions = createAction<DashboardAclDTO[]>('dashboard/loadDashboardPermissions');
|
||||
|
||||
export const dashboardInitFetching = actionCreatorFactory('DASHBOARD_INIT_FETCHING').create();
|
||||
export const dashboardInitFetching = createAction('dashboard/dashboardInitFetching');
|
||||
|
||||
export const dashboardInitServices = actionCreatorFactory('DASHBOARD_INIT_SERVICES').create();
|
||||
export const dashboardInitServices = createAction('dashboard/dashboardInitServices');
|
||||
|
||||
export const dashboardInitSlow = actionCreatorFactory('SET_DASHBOARD_INIT_SLOW').create();
|
||||
export const dashboardInitSlow = createAction('dashboard/dashboardInitSlow');
|
||||
|
||||
export const dashboardInitCompleted = actionCreatorFactory<MutableDashboard>('DASHBOARD_INIT_COMLETED').create();
|
||||
export const dashboardInitCompleted = createAction<MutableDashboard>('dashboard/dashboardInitCompleted');
|
||||
|
||||
/*
|
||||
* Unrecoverable init failure (fetch or model creation failed)
|
||||
*/
|
||||
export const dashboardInitFailed = actionCreatorFactory<DashboardInitError>('DASHBOARD_INIT_FAILED').create();
|
||||
export const dashboardInitFailed = createAction<DashboardInitError>('dashboard/dashboardInitFailed');
|
||||
|
||||
/*
|
||||
* When leaving dashboard, resets state
|
||||
* */
|
||||
export const cleanUpDashboard = actionCreatorFactory('DASHBOARD_CLEAN_UP').create();
|
||||
export const cleanUpDashboard = createAction('dashboard/cleanUpDashboard');
|
||||
|
||||
export function getDashboardPermissions(id: number): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
|
@ -3,8 +3,9 @@ import thunk from 'redux-thunk';
|
||||
import { initDashboard, InitDashboardArgs } from './initDashboard';
|
||||
import { DashboardRouteInfo } from 'app/types';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { dashboardInitFetching, dashboardInitCompleted, dashboardInitServices } from './actions';
|
||||
import { dashboardInitCompleted, dashboardInitFetching, dashboardInitServices } from './actions';
|
||||
import { resetExploreAction } from 'app/features/explore/state/actionTypes';
|
||||
import { updateLocation } from '../../../core/actions';
|
||||
|
||||
jest.mock('app/core/services/backend_srv');
|
||||
|
||||
@ -150,7 +151,7 @@ describeInitScenario('Initializing new dashboard', ctx => {
|
||||
});
|
||||
|
||||
it('Should update location with orgId query param', () => {
|
||||
expect(ctx.actions[2].type).toBe('UPDATE_LOCATION');
|
||||
expect(ctx.actions[2].type).toBe(updateLocation.type);
|
||||
expect(ctx.actions[2].payload.query.orgId).toBe(12);
|
||||
});
|
||||
|
||||
@ -180,7 +181,7 @@ describeInitScenario('Initializing home dashboard', ctx => {
|
||||
});
|
||||
|
||||
it('Should redirect to custom home dashboard', () => {
|
||||
expect(ctx.actions[1].type).toBe('UPDATE_LOCATION');
|
||||
expect(ctx.actions[1].type).toBe(updateLocation.type);
|
||||
expect(ctx.actions[1].payload.path).toBe('/u/123/my-home');
|
||||
});
|
||||
});
|
||||
@ -217,7 +218,7 @@ describeInitScenario('Initializing existing dashboard', ctx => {
|
||||
});
|
||||
|
||||
it('Should update location with orgId query param', () => {
|
||||
expect(ctx.actions[2].type).toBe('UPDATE_LOCATION');
|
||||
expect(ctx.actions[2].type).toBe(updateLocation.type);
|
||||
expect(ctx.actions[2].payload.query.orgId).toBe(12);
|
||||
});
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { DashboardState, DashboardInitPhase } from 'app/types';
|
||||
import { Action } from 'redux';
|
||||
import { DashboardInitPhase, DashboardState } from 'app/types';
|
||||
import {
|
||||
loadDashboardPermissions,
|
||||
dashboardInitFetching,
|
||||
dashboardInitSlow,
|
||||
dashboardInitServices,
|
||||
dashboardInitFailed,
|
||||
dashboardInitCompleted,
|
||||
cleanUpDashboard,
|
||||
dashboardInitCompleted,
|
||||
dashboardInitFailed,
|
||||
dashboardInitFetching,
|
||||
dashboardInitServices,
|
||||
dashboardInitSlow,
|
||||
loadDashboardPermissions,
|
||||
} from './actions';
|
||||
import { reducerFactory } from 'app/core/redux';
|
||||
import { processAclItems } from 'app/core/utils/acl';
|
||||
import { DashboardModel } from './DashboardModel';
|
||||
import { panelEditorReducer } from '../panel_editor/state/reducers';
|
||||
import { DashboardModel } from './DashboardModel';
|
||||
|
||||
export const initialState: DashboardState = {
|
||||
initPhase: DashboardInitPhase.NotStarted,
|
||||
@ -20,71 +20,75 @@ export const initialState: DashboardState = {
|
||||
permissions: [],
|
||||
};
|
||||
|
||||
export const dashboardReducer = reducerFactory(initialState)
|
||||
.addMapper({
|
||||
filter: loadDashboardPermissions,
|
||||
mapper: (state, action) => ({
|
||||
// Redux Toolkit uses ImmerJs as part of their solution to ensure that state objects are not mutated.
|
||||
// ImmerJs has an autoFreeze option that freezes objects from change which means this reducer can't be migrated to createSlice
|
||||
// because the state would become frozen and during run time we would get errors because Angular would try to mutate
|
||||
// the frozen state.
|
||||
// https://github.com/reduxjs/redux-toolkit/issues/242
|
||||
export const dashboardReducer = (state: DashboardState = initialState, action: Action<unknown>): DashboardState => {
|
||||
if (loadDashboardPermissions.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
permissions: processAclItems(action.payload),
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dashboardInitFetching,
|
||||
mapper: state => ({
|
||||
};
|
||||
}
|
||||
|
||||
if (dashboardInitFetching.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
initPhase: DashboardInitPhase.Fetching,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dashboardInitServices,
|
||||
mapper: state => ({
|
||||
};
|
||||
}
|
||||
|
||||
if (dashboardInitServices.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
initPhase: DashboardInitPhase.Services,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dashboardInitSlow,
|
||||
mapper: state => ({
|
||||
};
|
||||
}
|
||||
|
||||
if (dashboardInitSlow.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
isInitSlow: true,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dashboardInitFailed,
|
||||
mapper: (state, action) => ({
|
||||
};
|
||||
}
|
||||
|
||||
if (dashboardInitFailed.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
initPhase: DashboardInitPhase.Failed,
|
||||
isInitSlow: false,
|
||||
initError: action.payload,
|
||||
model: new DashboardModel({ title: 'Dashboard init failed' }, { canSave: false, canEdit: false }),
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dashboardInitCompleted,
|
||||
mapper: (state, action) => ({
|
||||
};
|
||||
}
|
||||
|
||||
if (dashboardInitCompleted.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
initPhase: DashboardInitPhase.Completed,
|
||||
model: action.payload,
|
||||
isInitSlow: false,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: cleanUpDashboard,
|
||||
mapper: (state, action) => {
|
||||
// Destroy current DashboardModel
|
||||
// Very important as this removes all dashboard event listeners
|
||||
state.model.destroy();
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
initPhase: DashboardInitPhase.NotStarted,
|
||||
model: null,
|
||||
isInitSlow: false,
|
||||
initError: null,
|
||||
};
|
||||
},
|
||||
})
|
||||
.create();
|
||||
if (cleanUpDashboard.match(action)) {
|
||||
// Destroy current DashboardModel
|
||||
// Very important as this removes all dashboard event listeners
|
||||
state.model.destroy();
|
||||
|
||||
return {
|
||||
...state,
|
||||
initPhase: DashboardInitPhase.NotStarted,
|
||||
model: null,
|
||||
isInitSlow: false,
|
||||
initError: null,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default {
|
||||
dashboard: dashboardReducer,
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DataSourceSettings, NavModel } from '@grafana/data';
|
||||
|
||||
import { DataSourcesListPage, Props } from './DataSourcesListPage';
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||
import { getMockDataSources } from './__mocks__/dataSourcesMocks';
|
||||
import { setDataSourcesSearchQuery, setDataSourcesLayoutMode } from './state/actions';
|
||||
import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/reducers';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
|
@ -2,21 +2,17 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
|
||||
// Components
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import DataSourcesList from './DataSourcesList';
|
||||
|
||||
// Types
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { DataSourceSettings, NavModel } from '@grafana/data';
|
||||
import { StoreState } from 'app/types';
|
||||
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||
|
||||
// Actions
|
||||
import { loadDataSources, setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions';
|
||||
import { loadDataSources } from './state/actions';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
|
||||
import {
|
||||
@ -25,6 +21,7 @@ import {
|
||||
getDataSourcesLayoutMode,
|
||||
getDataSourcesSearchQuery,
|
||||
} from './state/selectors';
|
||||
import { setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/reducers';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
|
@ -7,10 +7,11 @@ import { List } from '@grafana/ui';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { StoreState, DataSourcePluginCategory } from 'app/types';
|
||||
import { addDataSource, loadDataSourcePlugins, setDataSourceTypeSearchQuery } from './state/actions';
|
||||
import { DataSourcePluginCategory, StoreState } from 'app/types';
|
||||
import { addDataSource, loadDataSourcePlugins } from './state/actions';
|
||||
import { getDataSourcePlugins } from './state/selectors';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { setDataSourceTypeSearchQuery } from './state/reducers';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DataSourceSettingsPage, Props } from './DataSourceSettingsPage';
|
||||
import { DataSourceSettings, DataSourcePlugin, DataSourceConstructor, NavModel } from '@grafana/data';
|
||||
import { DataSourceConstructor, DataSourcePlugin, DataSourceSettings, NavModel } from '@grafana/data';
|
||||
import { getMockDataSource } from '../__mocks__/dataSourcesMocks';
|
||||
import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
|
||||
import { setDataSourceName, setIsDefault, dataSourceLoaded } from '../state/actions';
|
||||
import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
|
||||
|
||||
const pluginMock = new DataSourcePlugin({} as DataSourceConstructor<any>);
|
||||
|
||||
|
@ -15,14 +15,7 @@ import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
// Actions & selectors
|
||||
import { getDataSource, getDataSourceMeta } from '../state/selectors';
|
||||
import {
|
||||
dataSourceLoaded,
|
||||
deleteDataSource,
|
||||
loadDataSource,
|
||||
setDataSourceName,
|
||||
setIsDefault,
|
||||
updateDataSource,
|
||||
} from '../state/actions';
|
||||
import { deleteDataSource, loadDataSource, updateDataSource } from '../state/actions';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { getRouteParamsId } from 'app/core/selectors/location';
|
||||
// Types
|
||||
@ -32,6 +25,7 @@ import { DataSourcePluginMeta, DataSourceSettings, NavModel } from '@grafana/dat
|
||||
import { getDataSourceLoadingNav } from '../state/navModel';
|
||||
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
|
||||
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
|
||||
import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
|
@ -1,29 +1,21 @@
|
||||
import config from '../../../core/config';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||
import { updateLocation, updateNavIndex } from 'app/core/actions';
|
||||
import { buildNavModel } from './navModel';
|
||||
import { DataSourceSettings, DataSourcePluginMeta } from '@grafana/data';
|
||||
import { ThunkResult, DataSourcePluginCategory } from 'app/types';
|
||||
import { actionCreatorFactory } from 'app/core/redux';
|
||||
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
||||
import { DataSourcePluginCategory, ThunkResult } from 'app/types';
|
||||
import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
|
||||
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
|
||||
import {
|
||||
dataSourceLoaded,
|
||||
dataSourceMetaLoaded,
|
||||
dataSourcePluginsLoad,
|
||||
dataSourcePluginsLoaded,
|
||||
dataSourcesLoaded,
|
||||
} from './reducers';
|
||||
import { buildCategories } from './buildCategories';
|
||||
|
||||
export const dataSourceLoaded = actionCreatorFactory<DataSourceSettings>('LOAD_DATA_SOURCE').create();
|
||||
export const dataSourcesLoaded = actionCreatorFactory<DataSourceSettings[]>('LOAD_DATA_SOURCES').create();
|
||||
export const dataSourceMetaLoaded = actionCreatorFactory<DataSourcePluginMeta>('LOAD_DATA_SOURCE_META').create();
|
||||
export const dataSourcePluginsLoad = actionCreatorFactory('LOAD_DATA_SOURCE_PLUGINS').create();
|
||||
export const dataSourcePluginsLoaded = actionCreatorFactory<DataSourceTypesLoadedPayload>(
|
||||
'LOADED_DATA_SOURCE_PLUGINS'
|
||||
).create();
|
||||
export const setDataSourcesSearchQuery = actionCreatorFactory<string>('SET_DATA_SOURCES_SEARCH_QUERY').create();
|
||||
export const setDataSourcesLayoutMode = actionCreatorFactory<LayoutMode>('SET_DATA_SOURCES_LAYOUT_MODE').create();
|
||||
export const setDataSourceTypeSearchQuery = actionCreatorFactory<string>('SET_DATA_SOURCE_TYPE_SEARCH_QUERY').create();
|
||||
export const setDataSourceName = actionCreatorFactory<string>('SET_DATA_SOURCE_NAME').create();
|
||||
export const setIsDefault = actionCreatorFactory<boolean>('SET_IS_DEFAULT').create();
|
||||
|
||||
export interface DataSourceTypesLoadedPayload {
|
||||
plugins: DataSourcePluginMeta[];
|
||||
categories: DataSourcePluginCategory[];
|
||||
|
@ -1,21 +1,22 @@
|
||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||
import { dataSourcesReducer, initialState } from './reducers';
|
||||
import {
|
||||
dataSourcesLoaded,
|
||||
dataSourceLoaded,
|
||||
setDataSourcesSearchQuery,
|
||||
setDataSourcesLayoutMode,
|
||||
dataSourceMetaLoaded,
|
||||
dataSourcePluginsLoad,
|
||||
dataSourcePluginsLoaded,
|
||||
setDataSourceTypeSearchQuery,
|
||||
dataSourceMetaLoaded,
|
||||
dataSourcesLoaded,
|
||||
dataSourcesReducer,
|
||||
initialState,
|
||||
setDataSourceName,
|
||||
setDataSourcesLayoutMode,
|
||||
setDataSourcesSearchQuery,
|
||||
setDataSourceTypeSearchQuery,
|
||||
setIsDefault,
|
||||
} from './actions';
|
||||
import { getMockDataSources, getMockDataSource } from '../__mocks__/dataSourcesMocks';
|
||||
} from './reducers';
|
||||
import { getMockDataSource, getMockDataSources } from '../__mocks__/dataSourcesMocks';
|
||||
import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||
import { DataSourcesState } from 'app/types';
|
||||
import { PluginMetaInfo, PluginType, PluginMeta } from '@grafana/data';
|
||||
import { PluginMeta, PluginMetaInfo, PluginType } from '@grafana/data';
|
||||
|
||||
const mockPlugin = () =>
|
||||
({
|
||||
@ -74,7 +75,7 @@ describe('dataSourcesReducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dataSourceTypesLoad is dispatched', () => {
|
||||
describe('when dataSourcePluginsLoad is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
const state: DataSourcesState = { ...initialState, plugins: [mockPlugin()] };
|
||||
|
||||
@ -85,7 +86,7 @@ describe('dataSourcesReducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dataSourceTypesLoaded is dispatched', () => {
|
||||
describe('when dataSourcePluginsLoaded is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
const dataSourceTypes = [mockPlugin()];
|
||||
const state: DataSourcesState = { ...initialState, isLoadingDataSources: true };
|
||||
|
@ -1,19 +1,9 @@
|
||||
import { AnyAction, createAction } from '@reduxjs/toolkit';
|
||||
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
||||
|
||||
import { DataSourcesState } from 'app/types';
|
||||
import { DataSourceSettings, DataSourcePluginMeta } from '@grafana/data';
|
||||
import {
|
||||
dataSourceLoaded,
|
||||
dataSourcesLoaded,
|
||||
setDataSourcesSearchQuery,
|
||||
setDataSourcesLayoutMode,
|
||||
dataSourcePluginsLoad,
|
||||
dataSourcePluginsLoaded,
|
||||
setDataSourceTypeSearchQuery,
|
||||
dataSourceMetaLoaded,
|
||||
setDataSourceName,
|
||||
setIsDefault,
|
||||
} from './actions';
|
||||
import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||
import { reducerFactory } from 'app/core/redux';
|
||||
import { LayoutMode, LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||
import { DataSourceTypesLoadedPayload } from './actions';
|
||||
|
||||
export const initialState: DataSourcesState = {
|
||||
dataSources: [],
|
||||
@ -29,61 +19,80 @@ export const initialState: DataSourcesState = {
|
||||
dataSourceMeta: {} as DataSourcePluginMeta,
|
||||
};
|
||||
|
||||
export const dataSourcesReducer = reducerFactory(initialState)
|
||||
.addMapper({
|
||||
filter: dataSourcesLoaded,
|
||||
mapper: (state, action) => ({
|
||||
export const dataSourceLoaded = createAction<DataSourceSettings>('dataSources/dataSourceLoaded');
|
||||
export const dataSourcesLoaded = createAction<DataSourceSettings[]>('dataSources/dataSourcesLoaded');
|
||||
export const dataSourceMetaLoaded = createAction<DataSourcePluginMeta>('dataSources/dataSourceMetaLoaded');
|
||||
export const dataSourcePluginsLoad = createAction('dataSources/dataSourcePluginsLoad');
|
||||
export const dataSourcePluginsLoaded = createAction<DataSourceTypesLoadedPayload>(
|
||||
'dataSources/dataSourcePluginsLoaded'
|
||||
);
|
||||
export const setDataSourcesSearchQuery = createAction<string>('dataSources/setDataSourcesSearchQuery');
|
||||
export const setDataSourcesLayoutMode = createAction<LayoutMode>('dataSources/setDataSourcesLayoutMode');
|
||||
export const setDataSourceTypeSearchQuery = createAction<string>('dataSources/setDataSourceTypeSearchQuery');
|
||||
export const setDataSourceName = createAction<string>('dataSources/setDataSourceName');
|
||||
export const setIsDefault = createAction<boolean>('dataSources/setIsDefault');
|
||||
|
||||
// Redux Toolkit uses ImmerJs as part of their solution to ensure that state objects are not mutated.
|
||||
// ImmerJs has an autoFreeze option that freezes objects from change which means this reducer can't be migrated to createSlice
|
||||
// because the state would become frozen and during run time we would get errors because Angular would try to mutate
|
||||
// the frozen state.
|
||||
// https://github.com/reduxjs/redux-toolkit/issues/242
|
||||
export const dataSourcesReducer = (state: DataSourcesState = initialState, action: AnyAction): DataSourcesState => {
|
||||
if (dataSourcesLoaded.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
hasFetched: true,
|
||||
dataSources: action.payload,
|
||||
dataSourcesCount: action.payload.length,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dataSourceLoaded,
|
||||
mapper: (state, action) => ({ ...state, dataSource: action.payload }),
|
||||
})
|
||||
.addMapper({
|
||||
filter: setDataSourcesSearchQuery,
|
||||
mapper: (state, action) => ({ ...state, searchQuery: action.payload }),
|
||||
})
|
||||
.addMapper({
|
||||
filter: setDataSourcesLayoutMode,
|
||||
mapper: (state, action) => ({ ...state, layoutMode: action.payload }),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dataSourcePluginsLoad,
|
||||
mapper: state => ({ ...state, plugins: [], isLoadingDataSources: true }),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dataSourcePluginsLoaded,
|
||||
mapper: (state, action) => ({
|
||||
};
|
||||
}
|
||||
|
||||
if (dataSourceLoaded.match(action)) {
|
||||
return { ...state, dataSource: action.payload };
|
||||
}
|
||||
|
||||
if (setDataSourcesSearchQuery.match(action)) {
|
||||
return { ...state, searchQuery: action.payload };
|
||||
}
|
||||
|
||||
if (setDataSourcesLayoutMode.match(action)) {
|
||||
return { ...state, layoutMode: action.payload };
|
||||
}
|
||||
|
||||
if (dataSourcePluginsLoad.match(action)) {
|
||||
return { ...state, plugins: [], isLoadingDataSources: true };
|
||||
}
|
||||
|
||||
if (dataSourcePluginsLoaded.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
plugins: action.payload.plugins,
|
||||
categories: action.payload.categories,
|
||||
isLoadingDataSources: false,
|
||||
}),
|
||||
})
|
||||
.addMapper({
|
||||
filter: setDataSourceTypeSearchQuery,
|
||||
mapper: (state, action) => ({ ...state, dataSourceTypeSearchQuery: action.payload }),
|
||||
})
|
||||
.addMapper({
|
||||
filter: dataSourceMetaLoaded,
|
||||
mapper: (state, action) => ({ ...state, dataSourceMeta: action.payload }),
|
||||
})
|
||||
.addMapper({
|
||||
filter: setDataSourceName,
|
||||
mapper: (state, action) => ({ ...state, dataSource: { ...state.dataSource, name: action.payload } }),
|
||||
})
|
||||
.addMapper({
|
||||
filter: setIsDefault,
|
||||
mapper: (state, action) => ({
|
||||
};
|
||||
}
|
||||
|
||||
if (setDataSourceTypeSearchQuery.match(action)) {
|
||||
return { ...state, dataSourceTypeSearchQuery: action.payload };
|
||||
}
|
||||
|
||||
if (dataSourceMetaLoaded.match(action)) {
|
||||
return { ...state, dataSourceMeta: action.payload };
|
||||
}
|
||||
|
||||
if (setDataSourceName.match(action)) {
|
||||
return { ...state, dataSource: { ...state.dataSource, name: action.payload } };
|
||||
}
|
||||
|
||||
if (setIsDefault.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
dataSource: { ...state.dataSource, isDefault: action.payload },
|
||||
}),
|
||||
})
|
||||
.create();
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default {
|
||||
dataSources: dataSourcesReducer,
|
||||
|
@ -1,48 +1,21 @@
|
||||
// Types
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
import { Emitter } from 'app/core/core';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { Emitter } from 'app/core/core';
|
||||
import {
|
||||
AbsoluteTimeRange,
|
||||
DataQuery,
|
||||
DataSourceApi,
|
||||
QueryFixAction,
|
||||
PanelData,
|
||||
HistoryItem,
|
||||
LogLevel,
|
||||
TimeRange,
|
||||
LoadingState,
|
||||
AbsoluteTimeRange,
|
||||
LogLevel,
|
||||
PanelData,
|
||||
QueryFixAction,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { ExploreId, ExploreItemState, ExploreUIState, ExploreMode } from 'app/types/explore';
|
||||
import { actionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
import { ExploreId, ExploreItemState, ExploreMode, ExploreUIState } from 'app/types/explore';
|
||||
|
||||
/** Higher order actions
|
||||
*
|
||||
*/
|
||||
export enum ActionTypes {
|
||||
SplitOpen = 'explore/SPLIT_OPEN',
|
||||
ResetExplore = 'explore/RESET_EXPLORE',
|
||||
SyncTimes = 'explore/SYNC_TIMES',
|
||||
}
|
||||
export interface SplitOpenAction {
|
||||
type: ActionTypes.SplitOpen;
|
||||
payload: {
|
||||
itemState: ExploreItemState;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResetExploreAction {
|
||||
type: ActionTypes.ResetExplore;
|
||||
payload: {};
|
||||
}
|
||||
|
||||
export interface SyncTimesAction {
|
||||
type: ActionTypes.SyncTimes;
|
||||
payload: { syncedTimes: boolean };
|
||||
}
|
||||
/** Lower order actions
|
||||
*
|
||||
*/
|
||||
export interface AddQueryRowPayload {
|
||||
exploreId: ExploreId;
|
||||
index: number;
|
||||
@ -218,77 +191,67 @@ export interface ResetExplorePayload {
|
||||
/**
|
||||
* Adds a query row after the row with the given index.
|
||||
*/
|
||||
export const addQueryRowAction = actionCreatorFactory<AddQueryRowPayload>('explore/ADD_QUERY_ROW').create();
|
||||
export const addQueryRowAction = createAction<AddQueryRowPayload>('explore/addQueryRow');
|
||||
|
||||
/**
|
||||
* Change the mode of Explore.
|
||||
*/
|
||||
export const changeModeAction = actionCreatorFactory<ChangeModePayload>('explore/CHANGE_MODE').create();
|
||||
export const changeModeAction = createAction<ChangeModePayload>('explore/changeMode');
|
||||
|
||||
/**
|
||||
* Query change handler for the query row with the given index.
|
||||
* If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
|
||||
*/
|
||||
export const changeQueryAction = actionCreatorFactory<ChangeQueryPayload>('explore/CHANGE_QUERY').create();
|
||||
export const changeQueryAction = createAction<ChangeQueryPayload>('explore/changeQuery');
|
||||
|
||||
/**
|
||||
* Keep track of the Explore container size, in particular the width.
|
||||
* The width will be used to calculate graph intervals (number of datapoints).
|
||||
*/
|
||||
export const changeSizeAction = actionCreatorFactory<ChangeSizePayload>('explore/CHANGE_SIZE').create();
|
||||
export const changeSizeAction = createAction<ChangeSizePayload>('explore/changeSize');
|
||||
|
||||
/**
|
||||
* Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
|
||||
*/
|
||||
export const changeRefreshIntervalAction = actionCreatorFactory<ChangeRefreshIntervalPayload>(
|
||||
'explore/CHANGE_REFRESH_INTERVAL'
|
||||
).create();
|
||||
export const changeRefreshIntervalAction = createAction<ChangeRefreshIntervalPayload>('explore/changeRefreshInterval');
|
||||
|
||||
/**
|
||||
* Clear all queries and results.
|
||||
*/
|
||||
export const clearQueriesAction = actionCreatorFactory<ClearQueriesPayload>('explore/CLEAR_QUERIES').create();
|
||||
export const clearQueriesAction = createAction<ClearQueriesPayload>('explore/clearQueries');
|
||||
|
||||
/**
|
||||
* Clear origin panel id.
|
||||
*/
|
||||
export const clearOriginAction = actionCreatorFactory<ClearOriginPayload>('explore/CLEAR_ORIGIN').create();
|
||||
export const clearOriginAction = createAction<ClearOriginPayload>('explore/clearOrigin');
|
||||
|
||||
/**
|
||||
* Highlight expressions in the log results
|
||||
*/
|
||||
export const highlightLogsExpressionAction = actionCreatorFactory<HighlightLogsExpressionPayload>(
|
||||
'explore/HIGHLIGHT_LOGS_EXPRESSION'
|
||||
).create();
|
||||
export const highlightLogsExpressionAction = createAction<HighlightLogsExpressionPayload>(
|
||||
'explore/highlightLogsExpression'
|
||||
);
|
||||
|
||||
/**
|
||||
* Initialize Explore state with state from the URL and the React component.
|
||||
* Call this only on components for with the Explore state has not been initialized.
|
||||
*/
|
||||
export const initializeExploreAction = actionCreatorFactory<InitializeExplorePayload>(
|
||||
'explore/INITIALIZE_EXPLORE'
|
||||
).create();
|
||||
export const initializeExploreAction = createAction<InitializeExplorePayload>('explore/initializeExplore');
|
||||
|
||||
/**
|
||||
* Display an error when no datasources have been configured
|
||||
*/
|
||||
export const loadDatasourceMissingAction = actionCreatorFactory<LoadDatasourceMissingPayload>(
|
||||
'explore/LOAD_DATASOURCE_MISSING'
|
||||
).create();
|
||||
export const loadDatasourceMissingAction = createAction<LoadDatasourceMissingPayload>('explore/loadDatasourceMissing');
|
||||
|
||||
/**
|
||||
* Start the async process of loading a datasource to display a loading indicator
|
||||
*/
|
||||
export const loadDatasourcePendingAction = actionCreatorFactory<LoadDatasourcePendingPayload>(
|
||||
'explore/LOAD_DATASOURCE_PENDING'
|
||||
).create();
|
||||
export const loadDatasourcePendingAction = createAction<LoadDatasourcePendingPayload>('explore/loadDatasourcePending');
|
||||
|
||||
/**
|
||||
* Datasource loading was completed.
|
||||
*/
|
||||
export const loadDatasourceReadyAction = actionCreatorFactory<LoadDatasourceReadyPayload>(
|
||||
'explore/LOAD_DATASOURCE_READY'
|
||||
).create();
|
||||
export const loadDatasourceReadyAction = createAction<LoadDatasourceReadyPayload>('explore/loadDatasourceReady');
|
||||
|
||||
/**
|
||||
* Action to modify a query given a datasource-specific modifier action.
|
||||
@ -297,97 +260,86 @@ export const loadDatasourceReadyAction = actionCreatorFactory<LoadDatasourceRead
|
||||
* @param index Optional query row index. If omitted, the modification is applied to all query rows.
|
||||
* @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
|
||||
*/
|
||||
export const modifyQueriesAction = actionCreatorFactory<ModifyQueriesPayload>('explore/MODIFY_QUERIES').create();
|
||||
export const modifyQueriesAction = createAction<ModifyQueriesPayload>('explore/modifyQueries');
|
||||
|
||||
export const queryStreamUpdatedAction = actionCreatorFactory<QueryEndedPayload>(
|
||||
'explore/QUERY_STREAM_UPDATED'
|
||||
).create();
|
||||
export const queryStreamUpdatedAction = createAction<QueryEndedPayload>('explore/queryStreamUpdated');
|
||||
|
||||
export const queryStoreSubscriptionAction = actionCreatorFactory<QueryStoreSubscriptionPayload>(
|
||||
'explore/QUERY_STORE_SUBSCRIPTION'
|
||||
).create();
|
||||
export const queryStoreSubscriptionAction = createAction<QueryStoreSubscriptionPayload>(
|
||||
'explore/queryStoreSubscription'
|
||||
);
|
||||
|
||||
/**
|
||||
* Remove query row of the given index, as well as associated query results.
|
||||
*/
|
||||
export const removeQueryRowAction = actionCreatorFactory<RemoveQueryRowPayload>('explore/REMOVE_QUERY_ROW').create();
|
||||
export const removeQueryRowAction = createAction<RemoveQueryRowPayload>('explore/removeQueryRow');
|
||||
|
||||
/**
|
||||
* Start a scan for more results using the given scanner.
|
||||
* @param exploreId Explore area
|
||||
* @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
|
||||
*/
|
||||
export const scanStartAction = actionCreatorFactory<ScanStartPayload>('explore/SCAN_START').create();
|
||||
export const scanStartAction = createAction<ScanStartPayload>('explore/scanStart');
|
||||
|
||||
/**
|
||||
* Stop any scanning for more results.
|
||||
*/
|
||||
export const scanStopAction = actionCreatorFactory<ScanStopPayload>('explore/SCAN_STOP').create();
|
||||
export const scanStopAction = createAction<ScanStopPayload>('explore/scanStop');
|
||||
|
||||
/**
|
||||
* Reset queries to the given queries. Any modifications will be discarded.
|
||||
* Use this action for clicks on query examples. Triggers a query run.
|
||||
*/
|
||||
export const setQueriesAction = actionCreatorFactory<SetQueriesPayload>('explore/SET_QUERIES').create();
|
||||
export const setQueriesAction = createAction<SetQueriesPayload>('explore/setQueries');
|
||||
|
||||
/**
|
||||
* Close the split view and save URL state.
|
||||
*/
|
||||
export const splitCloseAction = actionCreatorFactory<SplitCloseActionPayload>('explore/SPLIT_CLOSE').create();
|
||||
export const splitCloseAction = createAction<SplitCloseActionPayload>('explore/splitClose');
|
||||
|
||||
/**
|
||||
* Open the split view and copy the left state to be the right state.
|
||||
* The right state is automatically initialized.
|
||||
* The copy keeps all query modifications but wipes the query results.
|
||||
*/
|
||||
export const splitOpenAction = actionCreatorFactory<SplitOpenPayload>('explore/SPLIT_OPEN').create();
|
||||
export const splitOpenAction = createAction<SplitOpenPayload>('explore/splitOpen');
|
||||
|
||||
export const syncTimesAction = actionCreatorFactory<SyncTimesPayload>('explore/SYNC_TIMES').create();
|
||||
export const syncTimesAction = createAction<SyncTimesPayload>('explore/syncTimes');
|
||||
/**
|
||||
* Update state of Explores UI elements (panels visiblity and deduplication strategy)
|
||||
*/
|
||||
export const updateUIStateAction = actionCreatorFactory<UpdateUIStatePayload>('explore/UPDATE_UI_STATE').create();
|
||||
export const updateUIStateAction = createAction<UpdateUIStatePayload>('explore/updateUIState');
|
||||
|
||||
/**
|
||||
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
|
||||
*/
|
||||
export const toggleTableAction = actionCreatorFactory<ToggleTablePayload>('explore/TOGGLE_TABLE').create();
|
||||
export const toggleTableAction = createAction<ToggleTablePayload>('explore/toggleTable');
|
||||
|
||||
/**
|
||||
* Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
|
||||
*/
|
||||
export const toggleGraphAction = actionCreatorFactory<ToggleGraphPayload>('explore/TOGGLE_GRAPH').create();
|
||||
export const toggleGraphAction = createAction<ToggleGraphPayload>('explore/toggleGraph');
|
||||
|
||||
/**
|
||||
* Updates datasource instance before datasouce loading has started
|
||||
*/
|
||||
export const updateDatasourceInstanceAction = actionCreatorFactory<UpdateDatasourceInstancePayload>(
|
||||
'explore/UPDATE_DATASOURCE_INSTANCE'
|
||||
).create();
|
||||
export const updateDatasourceInstanceAction = createAction<UpdateDatasourceInstancePayload>(
|
||||
'explore/updateDatasourceInstance'
|
||||
);
|
||||
|
||||
export const toggleLogLevelAction = actionCreatorFactory<ToggleLogLevelPayload>('explore/TOGGLE_LOG_LEVEL').create();
|
||||
export const toggleLogLevelAction = createAction<ToggleLogLevelPayload>('explore/toggleLogLevel');
|
||||
|
||||
/**
|
||||
* Resets state for explore.
|
||||
*/
|
||||
export const resetExploreAction = actionCreatorFactory<ResetExplorePayload>('explore/RESET_EXPLORE').create();
|
||||
export const queriesImportedAction = actionCreatorFactory<QueriesImportedPayload>('explore/QueriesImported').create();
|
||||
export const resetExploreAction = createAction<ResetExplorePayload>('explore/resetExplore');
|
||||
export const queriesImportedAction = createAction<QueriesImportedPayload>('explore/queriesImported');
|
||||
|
||||
export const historyUpdatedAction = actionCreatorFactory<HistoryUpdatedPayload>('explore/HISTORY_UPDATED').create();
|
||||
export const historyUpdatedAction = createAction<HistoryUpdatedPayload>('explore/historyUpdated');
|
||||
|
||||
export const setUrlReplacedAction = actionCreatorFactory<SetUrlReplacedPayload>('explore/SET_URL_REPLACED').create();
|
||||
export const setUrlReplacedAction = createAction<SetUrlReplacedPayload>('explore/setUrlReplaced');
|
||||
|
||||
export const changeRangeAction = actionCreatorFactory<ChangeRangePayload>('explore/CHANGE_RANGE').create();
|
||||
export const changeRangeAction = createAction<ChangeRangePayload>('explore/changeRange');
|
||||
|
||||
export const changeLoadingStateAction = actionCreatorFactory<ChangeLoadingStatePayload>(
|
||||
'changeLoadingStateAction'
|
||||
).create();
|
||||
export const changeLoadingStateAction = createAction<ChangeLoadingStatePayload>('changeLoadingState');
|
||||
|
||||
export const setPausedStateAction = actionCreatorFactory<SetPausedStatePayload>('explore/SET_PAUSED_STATE').create();
|
||||
|
||||
export type HigherOrderAction =
|
||||
| ActionOf<SplitCloseActionPayload>
|
||||
| SplitOpenAction
|
||||
| ResetExploreAction
|
||||
| SyncTimesAction
|
||||
| ActionOf<any>;
|
||||
export const setPausedStateAction = createAction<SetPausedStatePayload>('explore/setPausedState');
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { changeDatasource, loadDatasource, navigateToExplore, refreshExplore } from './actions';
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { DataQuery, DefaultTimeZone, LogsDedupStrategy, RawTimeRange, toUtc } from '@grafana/data';
|
||||
|
||||
import * as Actions from './actions';
|
||||
import { changeDatasource, loadDatasource, navigateToExplore, refreshExplore } from './actions';
|
||||
import { ExploreId, ExploreMode, ExploreUpdateState, ExploreUrlState } from 'app/types';
|
||||
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||
import {
|
||||
@ -8,13 +11,11 @@ import {
|
||||
loadDatasourcePendingAction,
|
||||
loadDatasourceReadyAction,
|
||||
setQueriesAction,
|
||||
updateUIStateAction,
|
||||
updateDatasourceInstanceAction,
|
||||
updateUIStateAction,
|
||||
} from './actionTypes';
|
||||
import { Emitter } from 'app/core/core';
|
||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
import { makeInitialUpdateState } from './reducers';
|
||||
import { DataQuery, DefaultTimeZone, LogsDedupStrategy, RawTimeRange, toUtc } from '@grafana/data';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { updateLocation } from '../../../core/actions';
|
||||
import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv';
|
||||
@ -117,7 +118,7 @@ describe('refreshExplore', () => {
|
||||
.givenThunk(refreshExplore)
|
||||
.whenThunkIsDispatched(exploreId);
|
||||
|
||||
const initializeExplore = dispatchedActions[1] as ActionOf<InitializeExplorePayload>;
|
||||
const initializeExplore = dispatchedActions[1] as PayloadAction<InitializeExplorePayload>;
|
||||
const { type, payload } = initializeExplore;
|
||||
|
||||
expect(type).toEqual(initializeExploreAction.type);
|
||||
|
@ -1,6 +1,22 @@
|
||||
// Libraries
|
||||
import { map, throttleTime } from 'rxjs/operators';
|
||||
import { identity } from 'rxjs';
|
||||
import { ActionCreatorWithPayload, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { DataSourceSrv } from '@grafana/runtime';
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
import {
|
||||
AbsoluteTimeRange,
|
||||
DataQuery,
|
||||
DataSourceApi,
|
||||
dateTimeForTimeZone,
|
||||
isDateTime,
|
||||
LoadingState,
|
||||
LogsDedupStrategy,
|
||||
PanelData,
|
||||
QueryFixAction,
|
||||
RawTimeRange,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
// Services & Utils
|
||||
import store from 'app/core/store';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
@ -23,21 +39,7 @@ import {
|
||||
} from 'app/core/utils/explore';
|
||||
// Types
|
||||
import { ExploreItemState, ExploreUrlState, ThunkResult } from 'app/types';
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
AbsoluteTimeRange,
|
||||
DataQuery,
|
||||
DataSourceApi,
|
||||
dateTimeForTimeZone,
|
||||
isDateTime,
|
||||
LoadingState,
|
||||
LogsDedupStrategy,
|
||||
PanelData,
|
||||
QueryFixAction,
|
||||
RawTimeRange,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { ExploreId, ExploreMode, ExploreUIState, QueryOptions } from 'app/types/explore';
|
||||
import {
|
||||
addQueryRowAction,
|
||||
@ -74,14 +76,12 @@ import {
|
||||
updateDatasourceInstanceAction,
|
||||
updateUIStateAction,
|
||||
} from './actionTypes';
|
||||
import { ActionCreator, ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
|
||||
import { updateLocation } from '../../../core/actions';
|
||||
import { getTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { preProcessPanelData, runRequest } from '../../dashboard/state/runRequest';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { DataSourceSrv } from '@grafana/runtime';
|
||||
import { getExploreDatasources } from './selectors';
|
||||
|
||||
/**
|
||||
@ -189,7 +189,7 @@ export function changeQuery(
|
||||
export function changeSize(
|
||||
exploreId: ExploreId,
|
||||
{ height, width }: { height: number; width: number }
|
||||
): ActionOf<ChangeSizePayload> {
|
||||
): PayloadAction<ChangeSizePayload> {
|
||||
return changeSizeAction({ exploreId, height, width });
|
||||
}
|
||||
|
||||
@ -217,7 +217,7 @@ export const updateTimeRange = (options: {
|
||||
export function changeRefreshInterval(
|
||||
exploreId: ExploreId,
|
||||
refreshInterval: string
|
||||
): ActionOf<ChangeRefreshIntervalPayload> {
|
||||
): PayloadAction<ChangeRefreshIntervalPayload> {
|
||||
return changeRefreshIntervalAction({ exploreId, refreshInterval });
|
||||
}
|
||||
|
||||
@ -299,7 +299,7 @@ export const loadDatasourceReady = (
|
||||
exploreId: ExploreId,
|
||||
instance: DataSourceApi,
|
||||
orgId: number
|
||||
): ActionOf<LoadDatasourceReadyPayload> => {
|
||||
): PayloadAction<LoadDatasourceReadyPayload> => {
|
||||
const historyKey = `grafana.explore.history.${instance.meta.id}`;
|
||||
const history = store.getObject(historyKey, []);
|
||||
// Save last-used datasource
|
||||
@ -677,7 +677,7 @@ export function syncTimes(exploreId: ExploreId): ThunkResult<void> {
|
||||
* queries won't be run
|
||||
*/
|
||||
const togglePanelActionCreator = (
|
||||
actionCreator: ActionCreator<ToggleGraphPayload> | ActionCreator<ToggleTablePayload>
|
||||
actionCreator: ActionCreatorWithPayload<ToggleGraphPayload> | ActionCreatorWithPayload<ToggleTablePayload>
|
||||
) => (exploreId: ExploreId, isPanelVisible: boolean): ThunkResult<void> => {
|
||||
return dispatch => {
|
||||
let uiFragmentStateUpdate: Partial<ExploreUIState>;
|
||||
|
@ -1,41 +1,40 @@
|
||||
import { DataQuery, DataSourceApi, dateTime, LoadingState, LogsDedupStrategy, toDataFrame } from '@grafana/data';
|
||||
|
||||
import {
|
||||
createEmptyQueryResponse,
|
||||
exploreReducer,
|
||||
initialExploreState,
|
||||
itemReducer,
|
||||
makeExploreItemState,
|
||||
exploreReducer,
|
||||
makeInitialUpdateState,
|
||||
initialExploreState,
|
||||
createEmptyQueryResponse,
|
||||
} from './reducers';
|
||||
import { ExploreId, ExploreItemState, ExploreUrlState, ExploreState, ExploreMode } from 'app/types/explore';
|
||||
import { ExploreId, ExploreItemState, ExploreMode, ExploreState, ExploreUrlState } from 'app/types/explore';
|
||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||
import {
|
||||
scanStartAction,
|
||||
updateDatasourceInstanceAction,
|
||||
splitOpenAction,
|
||||
splitCloseAction,
|
||||
changeModeAction,
|
||||
scanStopAction,
|
||||
toggleGraphAction,
|
||||
toggleTableAction,
|
||||
changeRangeAction,
|
||||
changeRefreshIntervalAction,
|
||||
scanStartAction,
|
||||
scanStopAction,
|
||||
splitCloseAction,
|
||||
splitOpenAction,
|
||||
toggleGraphAction,
|
||||
toggleTableAction,
|
||||
updateDatasourceInstanceAction,
|
||||
} from './actionTypes';
|
||||
import { Reducer } from 'redux';
|
||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
import { updateLocation } from 'app/core/actions/location';
|
||||
import { serializeStateToUrlParam } from 'app/core/utils/explore';
|
||||
import { DataSourceApi, DataQuery, LogsDedupStrategy, dateTime, LoadingState, toDataFrame } from '@grafana/data';
|
||||
import { updateLocation } from '../../../core/actions';
|
||||
|
||||
describe('Explore item reducer', () => {
|
||||
describe('scanning', () => {
|
||||
it('should start scanning', () => {
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
...makeExploreItemState(),
|
||||
scanning: false,
|
||||
};
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
|
||||
.givenReducer(itemReducer, initialState)
|
||||
.whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left }))
|
||||
.thenStateShouldEqual({
|
||||
...makeExploreItemState(),
|
||||
@ -43,14 +42,14 @@ describe('Explore item reducer', () => {
|
||||
});
|
||||
});
|
||||
it('should stop scanning', () => {
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
...makeExploreItemState(),
|
||||
scanning: true,
|
||||
scanRange: {},
|
||||
};
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
|
||||
.givenReducer(itemReducer, initialState)
|
||||
.whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
|
||||
.thenStateShouldEqual({
|
||||
...makeExploreItemState(),
|
||||
@ -64,7 +63,7 @@ describe('Explore item reducer', () => {
|
||||
describe('when changeMode is dispatched', () => {
|
||||
it('then it should set correct state', () => {
|
||||
reducerTester()
|
||||
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, {})
|
||||
.givenReducer(itemReducer, {})
|
||||
.whenActionIsDispatched(changeModeAction({ exploreId: ExploreId.left, mode: ExploreMode.Logs }))
|
||||
.thenStatePredicateShouldEqual((resultingState: ExploreItemState) => {
|
||||
expect(resultingState.mode).toEqual(ExploreMode.Logs);
|
||||
@ -91,7 +90,7 @@ describe('Explore item reducer', () => {
|
||||
} as DataSourceApi;
|
||||
const queries: DataQuery[] = [];
|
||||
const queryKeys: string[] = [];
|
||||
const initalState: Partial<ExploreItemState> = {
|
||||
const initialState: Partial<ExploreItemState> = {
|
||||
datasourceInstance: null,
|
||||
queries,
|
||||
queryKeys,
|
||||
@ -111,7 +110,7 @@ describe('Explore item reducer', () => {
|
||||
};
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(itemReducer, initalState)
|
||||
.givenReducer(itemReducer, initialState)
|
||||
.whenActionIsDispatched(updateDatasourceInstanceAction({ exploreId: ExploreId.left, datasourceInstance }))
|
||||
.thenStateShouldEqual(expectedState);
|
||||
});
|
||||
@ -121,7 +120,7 @@ describe('Explore item reducer', () => {
|
||||
|
||||
describe('changing refresh intervals', () => {
|
||||
it("should result in 'streaming' state, when live-tailing is active", () => {
|
||||
const initalState = makeExploreItemState();
|
||||
const initialState = makeExploreItemState();
|
||||
const expectedState = {
|
||||
...makeExploreItemState(),
|
||||
refreshInterval: 'LIVE',
|
||||
@ -137,13 +136,13 @@ describe('Explore item reducer', () => {
|
||||
},
|
||||
};
|
||||
reducerTester()
|
||||
.givenReducer(itemReducer, initalState)
|
||||
.givenReducer(itemReducer, initialState)
|
||||
.whenActionIsDispatched(changeRefreshIntervalAction({ exploreId: ExploreId.left, refreshInterval: 'LIVE' }))
|
||||
.thenStateShouldEqual(expectedState);
|
||||
});
|
||||
|
||||
it("should result in 'done' state, when live-tailing is stopped", () => {
|
||||
const initalState = makeExploreItemState();
|
||||
const initialState = makeExploreItemState();
|
||||
const expectedState = {
|
||||
...makeExploreItemState(),
|
||||
refreshInterval: '',
|
||||
@ -157,7 +156,7 @@ describe('Explore item reducer', () => {
|
||||
},
|
||||
};
|
||||
reducerTester()
|
||||
.givenReducer(itemReducer, initalState)
|
||||
.givenReducer(itemReducer, initialState)
|
||||
.whenActionIsDispatched(changeRefreshIntervalAction({ exploreId: ExploreId.left, refreshInterval: '' }))
|
||||
.thenStateShouldEqual(expectedState);
|
||||
});
|
||||
@ -243,10 +242,10 @@ export const setup = (urlStateOverrides?: any) => {
|
||||
};
|
||||
const urlState: ExploreUrlState = { ...urlStateDefaults, ...urlStateOverrides };
|
||||
const serializedUrlState = serializeStateToUrlParam(urlState);
|
||||
const initalState = { split: false, left: { urlState, update }, right: { urlState, update } };
|
||||
const initialState = { split: false, left: { urlState, update }, right: { urlState, update } };
|
||||
|
||||
return {
|
||||
initalState,
|
||||
initialState,
|
||||
serializedUrlState,
|
||||
};
|
||||
};
|
||||
@ -258,14 +257,14 @@ describe('Explore reducer', () => {
|
||||
containerWidth: 100,
|
||||
} as ExploreItemState;
|
||||
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
split: null,
|
||||
left: leftItemMock as ExploreItemState,
|
||||
right: makeExploreItemState(),
|
||||
} as ExploreState;
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(exploreReducer as Reducer<ExploreState, ActionOf<any>>, initalState)
|
||||
.givenReducer(exploreReducer, initialState)
|
||||
.whenActionIsDispatched(splitOpenAction({ itemState: leftItemMock }))
|
||||
.thenStateShouldEqual({
|
||||
split: true,
|
||||
@ -284,7 +283,7 @@ describe('Explore reducer', () => {
|
||||
containerWidth: 200,
|
||||
} as ExploreItemState;
|
||||
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
split: null,
|
||||
left: leftItemMock,
|
||||
right: rightItemMock,
|
||||
@ -292,7 +291,7 @@ describe('Explore reducer', () => {
|
||||
|
||||
// closing left item
|
||||
reducerTester()
|
||||
.givenReducer(exploreReducer as Reducer<ExploreState, ActionOf<any>>, initalState)
|
||||
.givenReducer(exploreReducer, initialState)
|
||||
.whenActionIsDispatched(splitCloseAction({ itemId: ExploreId.left }))
|
||||
.thenStateShouldEqual({
|
||||
split: false,
|
||||
@ -309,7 +308,7 @@ describe('Explore reducer', () => {
|
||||
containerWidth: 200,
|
||||
} as ExploreItemState;
|
||||
|
||||
const initalState = {
|
||||
const initialState = {
|
||||
split: null,
|
||||
left: leftItemMock,
|
||||
right: rightItemMock,
|
||||
@ -317,7 +316,7 @@ describe('Explore reducer', () => {
|
||||
|
||||
// closing left item
|
||||
reducerTester()
|
||||
.givenReducer(exploreReducer as Reducer<ExploreState, ActionOf<any>>, initalState)
|
||||
.givenReducer(exploreReducer, initialState)
|
||||
.whenActionIsDispatched(splitCloseAction({ itemId: ExploreId.right }))
|
||||
.thenStateShouldEqual({
|
||||
split: false,
|
||||
@ -350,11 +349,11 @@ describe('Explore reducer', () => {
|
||||
|
||||
describe("and query contains a 'right'", () => {
|
||||
it('then it should add split in state', () => {
|
||||
const { initalState, serializedUrlState } = setup();
|
||||
const expectedState = { ...initalState, split: true };
|
||||
const { initialState, serializedUrlState } = setup();
|
||||
const expectedState = { ...initialState, split: true };
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(exploreReducer, initalState)
|
||||
.givenReducer(exploreReducer, initialState)
|
||||
.whenActionIsDispatched(
|
||||
updateLocation({
|
||||
query: {
|
||||
@ -370,10 +369,10 @@ describe('Explore reducer', () => {
|
||||
describe("and query contains a 'left'", () => {
|
||||
describe('but urlState is not set in state', () => {
|
||||
it('then it should just add urlState and update in state', () => {
|
||||
const { initalState, serializedUrlState } = setup();
|
||||
const { initialState, serializedUrlState } = setup();
|
||||
const urlState: ExploreUrlState = null;
|
||||
const stateWithoutUrlState = { ...initalState, left: { urlState } };
|
||||
const expectedState = { ...initalState };
|
||||
const stateWithoutUrlState = { ...initialState, left: { urlState } };
|
||||
const expectedState = { ...initialState };
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(exploreReducer, stateWithoutUrlState)
|
||||
@ -391,11 +390,11 @@ describe('Explore reducer', () => {
|
||||
|
||||
describe("but '/explore' is missing in path", () => {
|
||||
it('then it should just add urlState and update in state', () => {
|
||||
const { initalState, serializedUrlState } = setup();
|
||||
const expectedState = { ...initalState };
|
||||
const { initialState, serializedUrlState } = setup();
|
||||
const expectedState = { ...initialState };
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(exploreReducer, initalState)
|
||||
.givenReducer(exploreReducer, initialState)
|
||||
.whenActionIsDispatched(
|
||||
updateLocation({
|
||||
query: {
|
||||
@ -411,23 +410,23 @@ describe('Explore reducer', () => {
|
||||
describe("and '/explore' is in path", () => {
|
||||
describe('and datasource differs', () => {
|
||||
it('then it should return update datasource', () => {
|
||||
const { initalState, serializedUrlState } = setup();
|
||||
const { initialState, serializedUrlState } = setup();
|
||||
const expectedState = {
|
||||
...initalState,
|
||||
...initialState,
|
||||
left: {
|
||||
...initalState.left,
|
||||
...initialState.left,
|
||||
update: {
|
||||
...initalState.left.update,
|
||||
...initialState.left.update,
|
||||
datasource: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const stateWithDifferentDataSource = {
|
||||
...initalState,
|
||||
...initialState,
|
||||
left: {
|
||||
...initalState.left,
|
||||
...initialState.left,
|
||||
urlState: {
|
||||
...initalState.left.urlState,
|
||||
...initialState.left.urlState,
|
||||
datasource: 'different datasource',
|
||||
},
|
||||
},
|
||||
@ -449,23 +448,23 @@ describe('Explore reducer', () => {
|
||||
|
||||
describe('and range differs', () => {
|
||||
it('then it should return update range', () => {
|
||||
const { initalState, serializedUrlState } = setup();
|
||||
const { initialState, serializedUrlState } = setup();
|
||||
const expectedState = {
|
||||
...initalState,
|
||||
...initialState,
|
||||
left: {
|
||||
...initalState.left,
|
||||
...initialState.left,
|
||||
update: {
|
||||
...initalState.left.update,
|
||||
...initialState.left.update,
|
||||
range: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const stateWithDifferentDataSource = {
|
||||
...initalState,
|
||||
...initialState,
|
||||
left: {
|
||||
...initalState.left,
|
||||
...initialState.left,
|
||||
urlState: {
|
||||
...initalState.left.urlState,
|
||||
...initialState.left.urlState,
|
||||
range: {
|
||||
from: 'now',
|
||||
to: 'now-6h',
|
||||
@ -490,23 +489,23 @@ describe('Explore reducer', () => {
|
||||
|
||||
describe('and queries differs', () => {
|
||||
it('then it should return update queries', () => {
|
||||
const { initalState, serializedUrlState } = setup();
|
||||
const { initialState, serializedUrlState } = setup();
|
||||
const expectedState = {
|
||||
...initalState,
|
||||
...initialState,
|
||||
left: {
|
||||
...initalState.left,
|
||||
...initialState.left,
|
||||
update: {
|
||||
...initalState.left.update,
|
||||
...initialState.left.update,
|
||||
queries: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const stateWithDifferentDataSource = {
|
||||
...initalState,
|
||||
...initialState,
|
||||
left: {
|
||||
...initalState.left,
|
||||
...initialState.left,
|
||||
urlState: {
|
||||
...initalState.left.urlState,
|
||||
...initialState.left.urlState,
|
||||
queries: [{ expr: '{__filename__="some.log"}' }],
|
||||
},
|
||||
},
|
||||
@ -528,25 +527,25 @@ describe('Explore reducer', () => {
|
||||
|
||||
describe('and ui differs', () => {
|
||||
it('then it should return update ui', () => {
|
||||
const { initalState, serializedUrlState } = setup();
|
||||
const { initialState, serializedUrlState } = setup();
|
||||
const expectedState = {
|
||||
...initalState,
|
||||
...initialState,
|
||||
left: {
|
||||
...initalState.left,
|
||||
...initialState.left,
|
||||
update: {
|
||||
...initalState.left.update,
|
||||
...initialState.left.update,
|
||||
ui: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const stateWithDifferentDataSource = {
|
||||
...initalState,
|
||||
...initialState,
|
||||
left: {
|
||||
...initalState.left,
|
||||
...initialState.left,
|
||||
urlState: {
|
||||
...initalState.left.urlState,
|
||||
...initialState.left.urlState,
|
||||
ui: {
|
||||
...initalState.left.urlState.ui,
|
||||
...initialState.left.urlState.ui,
|
||||
showingGraph: true,
|
||||
},
|
||||
},
|
||||
@ -569,11 +568,11 @@ describe('Explore reducer', () => {
|
||||
|
||||
describe('and nothing differs', () => {
|
||||
it('then it should return update ui', () => {
|
||||
const { initalState, serializedUrlState } = setup();
|
||||
const expectedState = { ...initalState };
|
||||
const { initialState, serializedUrlState } = setup();
|
||||
const expectedState = { ...initialState };
|
||||
|
||||
reducerTester()
|
||||
.givenReducer(exploreReducer, initalState)
|
||||
.givenReducer(exploreReducer, initialState)
|
||||
.whenActionIsDispatched(
|
||||
updateLocation({
|
||||
query: {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,8 @@ import React from 'react';
|
||||
import { FolderSettingsPage, Props } from './FolderSettingsPage';
|
||||
import { shallow } from 'enzyme';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
import { setFolderTitle } from './state/reducers';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
@ -18,7 +20,7 @@ const setup = (propOverrides?: object) => {
|
||||
permissions: [],
|
||||
},
|
||||
getFolderByUid: jest.fn(),
|
||||
setFolderTitle: jest.fn(),
|
||||
setFolderTitle: mockToolkitActionCreator(setFolderTitle),
|
||||
saveFolder: jest.fn(),
|
||||
deleteFolder: jest.fn(),
|
||||
};
|
||||
|
@ -6,9 +6,10 @@ import { Input } from '@grafana/ui';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { StoreState, FolderState, CoreEvents } from 'app/types';
|
||||
import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions';
|
||||
import { CoreEvents, FolderState, StoreState } from 'app/types';
|
||||
import { deleteFolder, getFolderByUid, saveFolder } from './state/actions';
|
||||
import { getLoadingNav } from './state/navModel';
|
||||
import { setFolderTitle } from './state/reducers';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
|
@ -1,60 +1,13 @@
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { StoreState } from 'app/types';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { FolderDTO, FolderState } from 'app/types';
|
||||
import {
|
||||
DashboardAcl,
|
||||
DashboardAclDTO,
|
||||
PermissionLevel,
|
||||
DashboardAclUpdateDTO,
|
||||
NewDashboardAclItem,
|
||||
} from 'app/types/acl';
|
||||
|
||||
import { updateNavIndex, updateLocation } from 'app/core/actions';
|
||||
import { buildNavModel } from './navModel';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadFolder = 'LOAD_FOLDER',
|
||||
SetFolderTitle = 'SET_FOLDER_TITLE',
|
||||
SaveFolder = 'SAVE_FOLDER',
|
||||
LoadFolderPermissions = 'LOAD_FOLDER_PERMISSONS',
|
||||
}
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { FolderState, ThunkResult } from 'app/types';
|
||||
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl';
|
||||
|
||||
export interface LoadFolderAction {
|
||||
type: ActionTypes.LoadFolder;
|
||||
payload: FolderDTO;
|
||||
}
|
||||
|
||||
export interface SetFolderTitleAction {
|
||||
type: ActionTypes.SetFolderTitle;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface LoadFolderPermissionsAction {
|
||||
type: ActionTypes.LoadFolderPermissions;
|
||||
payload: DashboardAcl[];
|
||||
}
|
||||
|
||||
export type Action = LoadFolderAction | SetFolderTitleAction | LoadFolderPermissionsAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
|
||||
|
||||
export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({
|
||||
type: ActionTypes.LoadFolder,
|
||||
payload: folder,
|
||||
});
|
||||
|
||||
export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({
|
||||
type: ActionTypes.SetFolderTitle,
|
||||
payload: newTitle,
|
||||
});
|
||||
|
||||
export const loadFolderPermissions = (items: DashboardAclDTO[]): LoadFolderPermissionsAction => ({
|
||||
type: ActionTypes.LoadFolderPermissions,
|
||||
payload: items,
|
||||
});
|
||||
import { updateLocation, updateNavIndex } from 'app/core/actions';
|
||||
import { buildNavModel } from './navModel';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { loadFolder, loadFolderPermissions } from './reducers';
|
||||
|
||||
export function getFolderByUid(uid: string): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { FolderDTO, OrgRole, PermissionLevel, FolderState } from 'app/types';
|
||||
import { inititalState, folderReducer } from './reducers';
|
||||
import { FolderDTO, FolderState, OrgRole, PermissionLevel } from 'app/types';
|
||||
import { folderReducer, initialState, loadFolder, loadFolderPermissions, setFolderTitle } from './reducers';
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
|
||||
function getTestFolder(): FolderDTO {
|
||||
return {
|
||||
@ -14,84 +14,130 @@ function getTestFolder(): FolderDTO {
|
||||
}
|
||||
|
||||
describe('folder reducer', () => {
|
||||
describe('loadFolder', () => {
|
||||
describe('when loadFolder is dispatched', () => {
|
||||
it('should load folder and set hasChanged to false', () => {
|
||||
const folder = getTestFolder();
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadFolder,
|
||||
payload: folder,
|
||||
};
|
||||
|
||||
const state = folderReducer(inititalState, action);
|
||||
|
||||
expect(state.hasChanged).toEqual(false);
|
||||
expect(state.title).toEqual('test folder');
|
||||
reducerTester<FolderState>()
|
||||
.givenReducer(folderReducer, { ...initialState, hasChanged: true })
|
||||
.whenActionIsDispatched(loadFolder(getTestFolder()))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
hasChanged: false,
|
||||
...getTestFolder(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('detFolderTitle', () => {
|
||||
it('should set title', () => {
|
||||
const action: Action = {
|
||||
type: ActionTypes.SetFolderTitle,
|
||||
payload: 'new title',
|
||||
};
|
||||
describe('when setFolderTitle is dispatched', () => {
|
||||
describe('and title has length', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<FolderState>()
|
||||
.givenReducer(folderReducer, { ...initialState })
|
||||
.whenActionIsDispatched(setFolderTitle('ready'))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
hasChanged: true,
|
||||
title: 'ready',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const state = folderReducer(inititalState, action);
|
||||
|
||||
expect(state.hasChanged).toEqual(true);
|
||||
expect(state.title).toEqual('new title');
|
||||
describe('and title has no length', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<FolderState>()
|
||||
.givenReducer(folderReducer, { ...initialState })
|
||||
.whenActionIsDispatched(setFolderTitle(''))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
hasChanged: false,
|
||||
title: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadFolderPermissions', () => {
|
||||
let state: FolderState;
|
||||
|
||||
beforeEach(() => {
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadFolderPermissions,
|
||||
payload: [
|
||||
{ id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View },
|
||||
{ id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
|
||||
{
|
||||
id: 4,
|
||||
dashboardId: 10,
|
||||
permission: PermissionLevel.View,
|
||||
teamId: 1,
|
||||
team: 'MyTestTeam',
|
||||
inherited: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
dashboardId: 1,
|
||||
permission: PermissionLevel.View,
|
||||
userId: 1,
|
||||
userLogin: 'MyTestUser',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
dashboardId: 1,
|
||||
permission: PermissionLevel.Edit,
|
||||
teamId: 2,
|
||||
team: 'MyTestTeam2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
state = folderReducer(inititalState, action);
|
||||
});
|
||||
|
||||
it('should add permissions to state', async () => {
|
||||
expect(state.permissions.length).toBe(5);
|
||||
});
|
||||
|
||||
it('should be sorted by sort rank and alphabetically', async () => {
|
||||
expect(state.permissions[0].name).toBe('MyTestTeam');
|
||||
expect(state.permissions[0].dashboardId).toBe(10);
|
||||
expect(state.permissions[1].name).toBe('Editor');
|
||||
expect(state.permissions[2].name).toBe('Viewer');
|
||||
expect(state.permissions[3].name).toBe('MyTestTeam2');
|
||||
expect(state.permissions[4].name).toBe('MyTestUser');
|
||||
describe('when loadFolderPermissions is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<FolderState>()
|
||||
.givenReducer(folderReducer, { ...initialState })
|
||||
.whenActionIsDispatched(
|
||||
loadFolderPermissions([
|
||||
{ id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View },
|
||||
{ id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
|
||||
{
|
||||
id: 4,
|
||||
dashboardId: 10,
|
||||
permission: PermissionLevel.View,
|
||||
teamId: 1,
|
||||
team: 'MyTestTeam',
|
||||
inherited: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
dashboardId: 1,
|
||||
permission: PermissionLevel.View,
|
||||
userId: 1,
|
||||
userLogin: 'MyTestUser',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
dashboardId: 1,
|
||||
permission: PermissionLevel.Edit,
|
||||
teamId: 2,
|
||||
team: 'MyTestTeam2',
|
||||
},
|
||||
])
|
||||
)
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
permissions: [
|
||||
{
|
||||
dashboardId: 10,
|
||||
id: 4,
|
||||
inherited: true,
|
||||
name: 'MyTestTeam',
|
||||
permission: 1,
|
||||
sortRank: 120,
|
||||
team: 'MyTestTeam',
|
||||
teamId: 1,
|
||||
},
|
||||
{
|
||||
dashboardId: 1,
|
||||
icon: 'fa fa-fw fa-street-view',
|
||||
id: 3,
|
||||
name: 'Editor',
|
||||
permission: 2,
|
||||
role: OrgRole.Editor,
|
||||
sortRank: 31,
|
||||
},
|
||||
{
|
||||
dashboardId: 1,
|
||||
icon: 'fa fa-fw fa-street-view',
|
||||
id: 2,
|
||||
name: 'Viewer',
|
||||
permission: 1,
|
||||
role: OrgRole.Viewer,
|
||||
sortRank: 30,
|
||||
},
|
||||
{
|
||||
dashboardId: 1,
|
||||
id: 6,
|
||||
name: 'MyTestTeam2',
|
||||
permission: 2,
|
||||
sortRank: 20,
|
||||
team: 'MyTestTeam2',
|
||||
teamId: 2,
|
||||
},
|
||||
{
|
||||
dashboardId: 1,
|
||||
id: 5,
|
||||
name: 'MyTestUser',
|
||||
permission: 1,
|
||||
sortRank: 10,
|
||||
userId: 1,
|
||||
userLogin: 'MyTestUser',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { FolderState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { DashboardAclDTO, FolderDTO, FolderState } from 'app/types';
|
||||
import { processAclItems } from 'app/core/utils/acl';
|
||||
|
||||
export const inititalState: FolderState = {
|
||||
export const initialState: FolderState = {
|
||||
id: 0,
|
||||
uid: 'loading',
|
||||
title: 'loading',
|
||||
@ -13,28 +14,36 @@ export const inititalState: FolderState = {
|
||||
permissions: [],
|
||||
};
|
||||
|
||||
export const folderReducer = (state = inititalState, action: Action): FolderState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadFolder:
|
||||
const folderSlice = createSlice({
|
||||
name: 'folder',
|
||||
initialState,
|
||||
reducers: {
|
||||
loadFolder: (state, action: PayloadAction<FolderDTO>): FolderState => {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
hasChanged: false,
|
||||
};
|
||||
case ActionTypes.SetFolderTitle:
|
||||
},
|
||||
setFolderTitle: (state, action: PayloadAction<string>): FolderState => {
|
||||
return {
|
||||
...state,
|
||||
title: action.payload,
|
||||
hasChanged: action.payload.trim().length > 0,
|
||||
};
|
||||
case ActionTypes.LoadFolderPermissions:
|
||||
},
|
||||
loadFolderPermissions: (state, action: PayloadAction<DashboardAclDTO[]>): FolderState => {
|
||||
return {
|
||||
...state,
|
||||
permissions: processAclItems(action.payload),
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { loadFolderPermissions, loadFolder, setFolderTitle } = folderSlice.actions;
|
||||
|
||||
export const folderReducer = folderSlice.reducer;
|
||||
|
||||
export default {
|
||||
folder: folderReducer,
|
||||
|
@ -1,8 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { NavModel } from '@grafana/data';
|
||||
|
||||
import { OrgDetailsPage, Props } from './OrgDetailsPage';
|
||||
import { Organization } from '../../types';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
import { setOrganizationName } from './state/reducers';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
@ -16,7 +19,7 @@ const setup = (propOverrides?: object) => {
|
||||
},
|
||||
} as NavModel,
|
||||
loadOrganization: jest.fn(),
|
||||
setOrganizationName: jest.fn(),
|
||||
setOrganizationName: mockToolkitActionCreator(setOrganizationName),
|
||||
updateOrganization: jest.fn(),
|
||||
};
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { NavModel } from '@grafana/data';
|
||||
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import OrgProfile from './OrgProfile';
|
||||
import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences';
|
||||
import { loadOrganization, setOrganizationName, updateOrganization } from './state/actions';
|
||||
import { loadOrganization, updateOrganization } from './state/actions';
|
||||
import { Organization, StoreState } from 'app/types';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { setOrganizationName } from './state/reducers';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
|
@ -1,32 +1,6 @@
|
||||
import { Organization, ThunkResult } from 'app/types';
|
||||
import { ThunkResult } from 'app/types';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadOrganization = 'LOAD_ORGANIZATION',
|
||||
SetOrganizationName = 'SET_ORGANIZATION_NAME',
|
||||
}
|
||||
|
||||
interface LoadOrganizationAction {
|
||||
type: ActionTypes.LoadOrganization;
|
||||
payload: Organization;
|
||||
}
|
||||
|
||||
interface SetOrganizationNameAction {
|
||||
type: ActionTypes.SetOrganizationName;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
const organizationLoaded = (organization: Organization) => ({
|
||||
type: ActionTypes.LoadOrganization,
|
||||
payload: organization,
|
||||
});
|
||||
|
||||
export const setOrganizationName = (orgName: string) => ({
|
||||
type: ActionTypes.SetOrganizationName,
|
||||
payload: orgName,
|
||||
});
|
||||
|
||||
export type Action = LoadOrganizationAction | SetOrganizationNameAction;
|
||||
import { organizationLoaded } from './reducers';
|
||||
|
||||
export function loadOrganization(): ThunkResult<any> {
|
||||
return async dispatch => {
|
||||
|
27
public/app/features/org/state/reducers.test.ts
Normal file
27
public/app/features/org/state/reducers.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { OrganizationState } from '../../../types';
|
||||
import { initialState, organizationLoaded, organizationReducer, setOrganizationName } from './reducers';
|
||||
|
||||
describe('organizationReducer', () => {
|
||||
describe('when organizationLoaded is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<OrganizationState>()
|
||||
.givenReducer(organizationReducer, { ...initialState })
|
||||
.whenActionIsDispatched(organizationLoaded({ id: 1, name: 'An org' }))
|
||||
.thenStateShouldEqual({
|
||||
organization: { id: 1, name: 'An org' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setOrganizationName is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<OrganizationState>()
|
||||
.givenReducer(organizationReducer, { ...initialState, organization: { id: 1, name: 'An org' } })
|
||||
.whenActionIsDispatched(setOrganizationName('New Name'))
|
||||
.thenStateShouldEqual({
|
||||
organization: { id: 1, name: 'New Name' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,21 +1,27 @@
|
||||
import { Organization, OrganizationState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState: OrganizationState = {
|
||||
import { Organization, OrganizationState } from 'app/types';
|
||||
|
||||
export const initialState: OrganizationState = {
|
||||
organization: {} as Organization,
|
||||
};
|
||||
|
||||
const organizationReducer = (state = initialState, action: Action): OrganizationState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadOrganization:
|
||||
const organizationSlice = createSlice({
|
||||
name: 'organization',
|
||||
initialState,
|
||||
reducers: {
|
||||
organizationLoaded: (state, action: PayloadAction<Organization>): OrganizationState => {
|
||||
return { ...state, organization: action.payload };
|
||||
|
||||
case ActionTypes.SetOrganizationName:
|
||||
},
|
||||
setOrganizationName: (state, action: PayloadAction<string>): OrganizationState => {
|
||||
return { ...state, organization: { ...state.organization, name: action.payload } };
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
export const { setOrganizationName, organizationLoaded } = organizationSlice.actions;
|
||||
|
||||
export const organizationReducer = organizationSlice.reducer;
|
||||
|
||||
export default {
|
||||
organization: organizationReducer,
|
||||
|
@ -2,8 +2,9 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { PluginListPage, Props } from './PluginListPage';
|
||||
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { PluginMeta } from '@grafana/data';
|
||||
import { NavModel, PluginMeta } from '@grafana/data';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
import { setPluginsLayoutMode, setPluginsSearchQuery } from './state/reducers';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
@ -17,8 +18,8 @@ const setup = (propOverrides?: object) => {
|
||||
} as NavModel,
|
||||
plugins: [] as PluginMeta[],
|
||||
searchQuery: '',
|
||||
setPluginsSearchQuery: jest.fn(),
|
||||
setPluginsLayoutMode: jest.fn(),
|
||||
setPluginsSearchQuery: mockToolkitActionCreator(setPluginsSearchQuery),
|
||||
setPluginsLayoutMode: mockToolkitActionCreator(setPluginsLayoutMode),
|
||||
layoutMode: LayoutModes.Grid,
|
||||
loadPlugins: jest.fn(),
|
||||
hasFetched: false,
|
||||
|
@ -4,13 +4,13 @@ import { connect } from 'react-redux';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
|
||||
import PluginList from './PluginList';
|
||||
import { loadPlugins, setPluginsLayoutMode, setPluginsSearchQuery } from './state/actions';
|
||||
import { loadPlugins } from './state/actions';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { getLayoutMode, getPlugins, getPluginsSearchQuery } from './state/selectors';
|
||||
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { PluginMeta } from '@grafana/data';
|
||||
import { NavModel, PluginMeta } from '@grafana/data';
|
||||
import { StoreState } from 'app/types';
|
||||
import { setPluginsLayoutMode, setPluginsSearchQuery } from './state/reducers';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
|
@ -1,74 +1,7 @@
|
||||
import { StoreState } from 'app/types';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||
import { PluginDashboard } from '../../../types/plugins';
|
||||
import { PluginMeta } from '@grafana/data';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadPlugins = 'LOAD_PLUGINS',
|
||||
LoadPluginDashboards = 'LOAD_PLUGIN_DASHBOARDS',
|
||||
LoadedPluginDashboards = 'LOADED_PLUGIN_DASHBOARDS',
|
||||
SetPluginsSearchQuery = 'SET_PLUGIN_SEARCH_QUERY',
|
||||
SetLayoutMode = 'SET_LAYOUT_MODE',
|
||||
}
|
||||
|
||||
export interface LoadPluginsAction {
|
||||
type: ActionTypes.LoadPlugins;
|
||||
payload: PluginMeta[];
|
||||
}
|
||||
|
||||
export interface LoadPluginDashboardsAction {
|
||||
type: ActionTypes.LoadPluginDashboards;
|
||||
}
|
||||
|
||||
export interface LoadedPluginDashboardsAction {
|
||||
type: ActionTypes.LoadedPluginDashboards;
|
||||
payload: PluginDashboard[];
|
||||
}
|
||||
|
||||
export interface SetPluginsSearchQueryAction {
|
||||
type: ActionTypes.SetPluginsSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface SetLayoutModeAction {
|
||||
type: ActionTypes.SetLayoutMode;
|
||||
payload: LayoutMode;
|
||||
}
|
||||
|
||||
export const setPluginsLayoutMode = (mode: LayoutMode): SetLayoutModeAction => ({
|
||||
type: ActionTypes.SetLayoutMode,
|
||||
payload: mode,
|
||||
});
|
||||
|
||||
export const setPluginsSearchQuery = (query: string): SetPluginsSearchQueryAction => ({
|
||||
type: ActionTypes.SetPluginsSearchQuery,
|
||||
payload: query,
|
||||
});
|
||||
|
||||
const pluginsLoaded = (plugins: PluginMeta[]): LoadPluginsAction => ({
|
||||
type: ActionTypes.LoadPlugins,
|
||||
payload: plugins,
|
||||
});
|
||||
|
||||
const pluginDashboardsLoad = (): LoadPluginDashboardsAction => ({
|
||||
type: ActionTypes.LoadPluginDashboards,
|
||||
});
|
||||
|
||||
const pluginDashboardsLoaded = (dashboards: PluginDashboard[]): LoadedPluginDashboardsAction => ({
|
||||
type: ActionTypes.LoadedPluginDashboards,
|
||||
payload: dashboards,
|
||||
});
|
||||
|
||||
export type Action =
|
||||
| LoadPluginsAction
|
||||
| LoadPluginDashboardsAction
|
||||
| LoadedPluginDashboardsAction
|
||||
| SetPluginsSearchQueryAction
|
||||
| SetLayoutModeAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||
import { ThunkResult } from 'app/types';
|
||||
import { pluginDashboardsLoad, pluginDashboardsLoaded, pluginsLoaded } from './reducers';
|
||||
|
||||
export function loadPlugins(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
|
151
public/app/features/plugins/state/reducers.test.ts
Normal file
151
public/app/features/plugins/state/reducers.test.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { PluginsState } from '../../../types';
|
||||
import {
|
||||
initialState,
|
||||
pluginDashboardsLoad,
|
||||
pluginDashboardsLoaded,
|
||||
pluginsLoaded,
|
||||
pluginsReducer,
|
||||
setPluginsLayoutMode,
|
||||
setPluginsSearchQuery,
|
||||
} from './reducers';
|
||||
import { PluginMetaInfo, PluginType } from '@grafana/data';
|
||||
import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||
|
||||
describe('pluginsReducer', () => {
|
||||
describe('when pluginsLoaded is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<PluginsState>()
|
||||
.givenReducer(pluginsReducer, { ...initialState })
|
||||
.whenActionIsDispatched(
|
||||
pluginsLoaded([
|
||||
{
|
||||
id: 'some-id',
|
||||
baseUrl: 'some-url',
|
||||
module: 'some module',
|
||||
name: 'Some Plugin',
|
||||
type: PluginType.app,
|
||||
info: {} as PluginMetaInfo,
|
||||
},
|
||||
])
|
||||
)
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
hasFetched: true,
|
||||
plugins: [
|
||||
{
|
||||
baseUrl: 'some-url',
|
||||
id: 'some-id',
|
||||
info: {} as PluginMetaInfo,
|
||||
module: 'some module',
|
||||
name: 'Some Plugin',
|
||||
type: PluginType.app,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setPluginsSearchQuery is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<PluginsState>()
|
||||
.givenReducer(pluginsReducer, { ...initialState })
|
||||
.whenActionIsDispatched(setPluginsSearchQuery('A query'))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
searchQuery: 'A query',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setPluginsLayoutMode is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<PluginsState>()
|
||||
.givenReducer(pluginsReducer, { ...initialState })
|
||||
.whenActionIsDispatched(setPluginsLayoutMode(LayoutModes.List))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
layoutMode: LayoutModes.List,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when pluginDashboardsLoad is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<PluginsState>()
|
||||
.givenReducer(pluginsReducer, {
|
||||
...initialState,
|
||||
dashboards: [
|
||||
{
|
||||
dashboardId: 1,
|
||||
title: 'Some Dash',
|
||||
description: 'Some Desc',
|
||||
folderId: 2,
|
||||
imported: false,
|
||||
importedRevision: 1,
|
||||
importedUri: 'some-uri',
|
||||
importedUrl: 'some-url',
|
||||
path: 'some/path',
|
||||
pluginId: 'some-plugin-id',
|
||||
removed: false,
|
||||
revision: 22,
|
||||
slug: 'someSlug',
|
||||
},
|
||||
],
|
||||
})
|
||||
.whenActionIsDispatched(pluginDashboardsLoad())
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
dashboards: [],
|
||||
isLoadingPluginDashboards: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when pluginDashboardsLoad is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<PluginsState>()
|
||||
.givenReducer(pluginsReducer, { ...initialState, isLoadingPluginDashboards: true })
|
||||
.whenActionIsDispatched(
|
||||
pluginDashboardsLoaded([
|
||||
{
|
||||
dashboardId: 1,
|
||||
title: 'Some Dash',
|
||||
description: 'Some Desc',
|
||||
folderId: 2,
|
||||
imported: false,
|
||||
importedRevision: 1,
|
||||
importedUri: 'some-uri',
|
||||
importedUrl: 'some-url',
|
||||
path: 'some/path',
|
||||
pluginId: 'some-plugin-id',
|
||||
removed: false,
|
||||
revision: 22,
|
||||
slug: 'someSlug',
|
||||
},
|
||||
])
|
||||
)
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
dashboards: [
|
||||
{
|
||||
dashboardId: 1,
|
||||
title: 'Some Dash',
|
||||
description: 'Some Desc',
|
||||
folderId: 2,
|
||||
imported: false,
|
||||
importedRevision: 1,
|
||||
importedUri: 'some-uri',
|
||||
importedUrl: 'some-url',
|
||||
path: 'some/path',
|
||||
pluginId: 'some-plugin-id',
|
||||
removed: false,
|
||||
revision: 22,
|
||||
slug: 'someSlug',
|
||||
},
|
||||
],
|
||||
isLoadingPluginDashboards: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,37 +1,49 @@
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { PluginsState } from 'app/types';
|
||||
import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||
import { PluginDashboard } from '../../../types/plugins';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { PluginMeta } from '@grafana/data';
|
||||
import { PluginsState } from 'app/types';
|
||||
import { LayoutMode, LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||
import { PluginDashboard } from '../../../types/plugins';
|
||||
|
||||
export const initialState: PluginsState = {
|
||||
plugins: [] as PluginMeta[],
|
||||
plugins: [],
|
||||
searchQuery: '',
|
||||
layoutMode: LayoutModes.Grid,
|
||||
hasFetched: false,
|
||||
dashboards: [] as PluginDashboard[],
|
||||
dashboards: [],
|
||||
isLoadingPluginDashboards: false,
|
||||
};
|
||||
|
||||
export const pluginsReducer = (state = initialState, action: Action): PluginsState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadPlugins:
|
||||
const pluginsSlice = createSlice({
|
||||
name: 'plugins',
|
||||
initialState,
|
||||
reducers: {
|
||||
pluginsLoaded: (state, action: PayloadAction<PluginMeta[]>): PluginsState => {
|
||||
return { ...state, hasFetched: true, plugins: action.payload };
|
||||
|
||||
case ActionTypes.SetPluginsSearchQuery:
|
||||
},
|
||||
setPluginsSearchQuery: (state, action: PayloadAction<string>): PluginsState => {
|
||||
return { ...state, searchQuery: action.payload };
|
||||
|
||||
case ActionTypes.SetLayoutMode:
|
||||
},
|
||||
setPluginsLayoutMode: (state, action: PayloadAction<LayoutMode>): PluginsState => {
|
||||
return { ...state, layoutMode: action.payload };
|
||||
|
||||
case ActionTypes.LoadPluginDashboards:
|
||||
},
|
||||
pluginDashboardsLoad: (state, action: PayloadAction<undefined>): PluginsState => {
|
||||
return { ...state, dashboards: [], isLoadingPluginDashboards: true };
|
||||
|
||||
case ActionTypes.LoadedPluginDashboards:
|
||||
},
|
||||
pluginDashboardsLoaded: (state, action: PayloadAction<PluginDashboard[]>): PluginsState => {
|
||||
return { ...state, dashboards: action.payload, isLoadingPluginDashboards: false };
|
||||
}
|
||||
return state;
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
pluginsLoaded,
|
||||
pluginDashboardsLoad,
|
||||
pluginDashboardsLoaded,
|
||||
setPluginsLayoutMode,
|
||||
setPluginsSearchQuery,
|
||||
} = pluginsSlice.actions;
|
||||
|
||||
export const pluginsReducer = pluginsSlice.reducer;
|
||||
|
||||
export default {
|
||||
plugins: pluginsReducer,
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { CreateTeam, Props } from './CreateTeam';
|
||||
import { mockActionCreator } from 'app/core/redux';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const props: Props = {
|
||||
updateLocation: mockActionCreator(updateLocation),
|
||||
updateLocation: mockToolkitActionCreator(updateLocation),
|
||||
navModel: {} as any,
|
||||
};
|
||||
const wrapper = shallow(<CreateTeam {...props} />);
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Props, TeamList } from './TeamList';
|
||||
import { Team, OrgRole } from '../../types';
|
||||
import { OrgRole, Team } from '../../types';
|
||||
import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
import { setSearchQuery } from './state/reducers';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
@ -19,7 +21,7 @@ const setup = (propOverrides?: object) => {
|
||||
teams: [] as Team[],
|
||||
loadTeams: jest.fn(),
|
||||
deleteTeam: jest.fn(),
|
||||
setSearchQuery: jest.fn(),
|
||||
setSearchQuery: mockToolkitActionCreator(setSearchQuery),
|
||||
searchQuery: '',
|
||||
teamsCount: 0,
|
||||
hasFetched: false,
|
||||
|
@ -4,14 +4,15 @@ import Page from 'app/core/components/Page/Page';
|
||||
import { DeleteButton } from '@grafana/ui';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { Team, OrgRole, StoreState } from 'app/types';
|
||||
import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
|
||||
import { OrgRole, StoreState, Team } from 'app/types';
|
||||
import { deleteTeam, loadTeams } from './state/actions';
|
||||
import { getSearchQuery, getTeams, getTeamsCount, isPermissionTeamAdmin } from './state/selectors';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { config } from 'app/core/config';
|
||||
import { contextSrv, User } from 'app/core/services/context_srv';
|
||||
import { connectWithCleanUp } from '../../core/components/connectWithCleanUp';
|
||||
import { setSearchQuery } from './state/reducers';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { TeamMembers, Props, State } from './TeamMembers';
|
||||
import { TeamMember, OrgRole } from '../../types';
|
||||
import { Props, State, TeamMembers } from './TeamMembers';
|
||||
import { OrgRole, TeamMember } from '../../types';
|
||||
import { getMockTeamMembers } from './__mocks__/teamMocks';
|
||||
import { User } from 'app/core/services/context_srv';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
import { setSearchMemberQuery } from './state/reducers';
|
||||
|
||||
const signedInUserId = 1;
|
||||
|
||||
@ -11,7 +13,7 @@ const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
members: [] as TeamMember[],
|
||||
searchMemberQuery: '',
|
||||
setSearchMemberQuery: jest.fn(),
|
||||
setSearchMemberQuery: mockToolkitActionCreator(setSearchMemberQuery),
|
||||
addTeamMember: jest.fn(),
|
||||
syncEnabled: false,
|
||||
editorsCanAdmin: false,
|
||||
|
@ -4,13 +4,14 @@ import { SlideDown } from 'app/core/components/Animations/SlideDown';
|
||||
import { UserPicker } from 'app/core/components/Select/UserPicker';
|
||||
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
||||
import { TeamMember, User } from 'app/types';
|
||||
import { addTeamMember, setSearchMemberQuery } from './state/actions';
|
||||
import { addTeamMember } from './state/actions';
|
||||
import { getSearchMemberQuery, isSignedInUserTeamAdmin } from './state/selectors';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { WithFeatureToggle } from 'app/core/components/WithFeatureToggle';
|
||||
import { config } from 'app/core/config';
|
||||
import { contextSrv, User as SignedInUser } from 'app/core/services/context_srv';
|
||||
import TeamMemberRow from './TeamMemberRow';
|
||||
import { setSearchMemberQuery } from './state/reducers';
|
||||
|
||||
export interface Props {
|
||||
members: TeamMember[];
|
||||
|
@ -1,87 +1,9 @@
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { StoreState, Team, TeamGroup, TeamMember } from 'app/types';
|
||||
import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions';
|
||||
|
||||
import { TeamMember, ThunkResult } from 'app/types';
|
||||
import { updateNavIndex } from 'app/core/actions';
|
||||
import { buildNavModel } from './navModel';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadTeams = 'LOAD_TEAMS',
|
||||
LoadTeam = 'LOAD_TEAM',
|
||||
SetSearchQuery = 'SET_TEAM_SEARCH_QUERY',
|
||||
SetSearchMemberQuery = 'SET_TEAM_MEMBER_SEARCH_QUERY',
|
||||
LoadTeamMembers = 'TEAM_MEMBERS_LOADED',
|
||||
LoadTeamGroups = 'TEAM_GROUPS_LOADED',
|
||||
}
|
||||
|
||||
export interface LoadTeamsAction {
|
||||
type: ActionTypes.LoadTeams;
|
||||
payload: Team[];
|
||||
}
|
||||
|
||||
export interface LoadTeamAction {
|
||||
type: ActionTypes.LoadTeam;
|
||||
payload: Team;
|
||||
}
|
||||
|
||||
export interface LoadTeamMembersAction {
|
||||
type: ActionTypes.LoadTeamMembers;
|
||||
payload: TeamMember[];
|
||||
}
|
||||
|
||||
export interface LoadTeamGroupsAction {
|
||||
type: ActionTypes.LoadTeamGroups;
|
||||
payload: TeamGroup[];
|
||||
}
|
||||
|
||||
export interface SetSearchQueryAction {
|
||||
type: ActionTypes.SetSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface SetSearchMemberQueryAction {
|
||||
type: ActionTypes.SetSearchMemberQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| LoadTeamsAction
|
||||
| SetSearchQueryAction
|
||||
| LoadTeamAction
|
||||
| LoadTeamMembersAction
|
||||
| SetSearchMemberQueryAction
|
||||
| LoadTeamGroupsAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action | UpdateNavIndexAction>;
|
||||
|
||||
const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({
|
||||
type: ActionTypes.LoadTeams,
|
||||
payload: teams,
|
||||
});
|
||||
|
||||
const teamLoaded = (team: Team): LoadTeamAction => ({
|
||||
type: ActionTypes.LoadTeam,
|
||||
payload: team,
|
||||
});
|
||||
|
||||
const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => ({
|
||||
type: ActionTypes.LoadTeamMembers,
|
||||
payload: teamMembers,
|
||||
});
|
||||
|
||||
const teamGroupsLoaded = (teamGroups: TeamGroup[]): LoadTeamGroupsAction => ({
|
||||
type: ActionTypes.LoadTeamGroups,
|
||||
payload: teamGroups,
|
||||
});
|
||||
|
||||
export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({
|
||||
type: ActionTypes.SetSearchMemberQuery,
|
||||
payload: searchQuery,
|
||||
});
|
||||
|
||||
export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({
|
||||
type: ActionTypes.SetSearchQuery,
|
||||
payload: searchQuery,
|
||||
});
|
||||
import { teamGroupsLoaded, teamLoaded, teamMembersLoaded, teamsLoaded } from './reducers';
|
||||
|
||||
export function loadTeams(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
|
@ -1,72 +1,92 @@
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import { initialTeamsState, initialTeamState, teamReducer, teamsReducer } from './reducers';
|
||||
import { getMockTeam, getMockTeamMember } from '../__mocks__/teamMocks';
|
||||
import {
|
||||
initialTeamsState,
|
||||
initialTeamState,
|
||||
setSearchMemberQuery,
|
||||
setSearchQuery,
|
||||
teamGroupsLoaded,
|
||||
teamLoaded,
|
||||
teamMembersLoaded,
|
||||
teamReducer,
|
||||
teamsLoaded,
|
||||
teamsReducer,
|
||||
} from './reducers';
|
||||
import { getMockTeam, getMockTeamGroups, getMockTeamMember } from '../__mocks__/teamMocks';
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { TeamsState, TeamState } from '../../../types';
|
||||
|
||||
describe('teams reducer', () => {
|
||||
it('should set teams', () => {
|
||||
const payload = [getMockTeam()];
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadTeams,
|
||||
payload,
|
||||
};
|
||||
|
||||
const result = teamsReducer(initialTeamsState, action);
|
||||
|
||||
expect(result.teams).toEqual(payload);
|
||||
describe('when teamsLoaded is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<TeamsState>()
|
||||
.givenReducer(teamsReducer, { ...initialTeamsState })
|
||||
.whenActionIsDispatched(teamsLoaded([getMockTeam()]))
|
||||
.thenStateShouldEqual({
|
||||
...initialTeamsState,
|
||||
hasFetched: true,
|
||||
teams: [getMockTeam()],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should set search query', () => {
|
||||
const payload = 'test';
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.SetSearchQuery,
|
||||
payload,
|
||||
};
|
||||
|
||||
const result = teamsReducer(initialTeamsState, action);
|
||||
|
||||
expect(result.searchQuery).toEqual('test');
|
||||
describe('when setSearchQueryAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<TeamsState>()
|
||||
.givenReducer(teamsReducer, { ...initialTeamsState })
|
||||
.whenActionIsDispatched(setSearchQuery('test'))
|
||||
.thenStateShouldEqual({
|
||||
...initialTeamsState,
|
||||
searchQuery: 'test',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('team reducer', () => {
|
||||
it('should set team', () => {
|
||||
const payload = getMockTeam();
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadTeam,
|
||||
payload,
|
||||
};
|
||||
|
||||
const result = teamReducer(initialTeamState, action);
|
||||
|
||||
expect(result.team).toEqual(payload);
|
||||
describe('when loadTeamsAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<TeamState>()
|
||||
.givenReducer(teamReducer, { ...initialTeamState })
|
||||
.whenActionIsDispatched(teamLoaded(getMockTeam()))
|
||||
.thenStateShouldEqual({
|
||||
...initialTeamState,
|
||||
team: getMockTeam(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should set team members', () => {
|
||||
const mockTeamMember = getMockTeamMember();
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.LoadTeamMembers,
|
||||
payload: [mockTeamMember],
|
||||
};
|
||||
|
||||
const result = teamReducer(initialTeamState, action);
|
||||
|
||||
expect(result.members).toEqual([mockTeamMember]);
|
||||
describe('when loadTeamMembersAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<TeamState>()
|
||||
.givenReducer(teamReducer, { ...initialTeamState })
|
||||
.whenActionIsDispatched(teamMembersLoaded([getMockTeamMember()]))
|
||||
.thenStateShouldEqual({
|
||||
...initialTeamState,
|
||||
members: [getMockTeamMember()],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should set member search query', () => {
|
||||
const payload = 'member';
|
||||
describe('when setSearchMemberQueryAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<TeamState>()
|
||||
.givenReducer(teamReducer, { ...initialTeamState })
|
||||
.whenActionIsDispatched(setSearchMemberQuery('member'))
|
||||
.thenStateShouldEqual({
|
||||
...initialTeamState,
|
||||
searchMemberQuery: 'member',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const action: Action = {
|
||||
type: ActionTypes.SetSearchMemberQuery,
|
||||
payload,
|
||||
};
|
||||
|
||||
const result = teamReducer(initialTeamState, action);
|
||||
|
||||
expect(result.searchMemberQuery).toEqual('member');
|
||||
describe('when loadTeamGroupsAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<TeamState>()
|
||||
.givenReducer(teamReducer, { ...initialTeamState })
|
||||
.whenActionIsDispatched(teamGroupsLoaded(getMockTeamGroups(1)))
|
||||
.thenStateShouldEqual({
|
||||
...initialTeamState,
|
||||
groups: getMockTeamGroups(1),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,26 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
|
||||
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '', hasFetched: false };
|
||||
|
||||
const teamsSlice = createSlice({
|
||||
name: 'teams',
|
||||
initialState: initialTeamsState,
|
||||
reducers: {
|
||||
teamsLoaded: (state, action: PayloadAction<Team[]>): TeamsState => {
|
||||
return { ...state, hasFetched: true, teams: action.payload };
|
||||
},
|
||||
setSearchQuery: (state, action: PayloadAction<string>): TeamsState => {
|
||||
return { ...state, searchQuery: action.payload };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { teamsLoaded, setSearchQuery } = teamsSlice.actions;
|
||||
|
||||
export const teamsReducer = teamsSlice.reducer;
|
||||
|
||||
export const initialTeamState: TeamState = {
|
||||
team: {} as Team,
|
||||
members: [] as TeamMember[],
|
||||
@ -9,34 +28,28 @@ export const initialTeamState: TeamState = {
|
||||
searchMemberQuery: '',
|
||||
};
|
||||
|
||||
export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadTeams:
|
||||
return { ...state, hasFetched: true, teams: action.payload };
|
||||
|
||||
case ActionTypes.SetSearchQuery:
|
||||
return { ...state, searchQuery: action.payload };
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const teamReducer = (state = initialTeamState, action: Action): TeamState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadTeam:
|
||||
const teamSlice = createSlice({
|
||||
name: 'team',
|
||||
initialState: initialTeamState,
|
||||
reducers: {
|
||||
teamLoaded: (state, action: PayloadAction<Team>): TeamState => {
|
||||
return { ...state, team: action.payload };
|
||||
|
||||
case ActionTypes.LoadTeamMembers:
|
||||
},
|
||||
teamMembersLoaded: (state, action: PayloadAction<TeamMember[]>): TeamState => {
|
||||
return { ...state, members: action.payload };
|
||||
|
||||
case ActionTypes.SetSearchMemberQuery:
|
||||
},
|
||||
setSearchMemberQuery: (state, action: PayloadAction<string>): TeamState => {
|
||||
return { ...state, searchMemberQuery: action.payload };
|
||||
|
||||
case ActionTypes.LoadTeamGroups:
|
||||
},
|
||||
teamGroupsLoaded: (state, action: PayloadAction<TeamGroup[]>): TeamState => {
|
||||
return { ...state, groups: action.payload };
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
export const { teamLoaded, teamGroupsLoaded, teamMembersLoaded, setSearchMemberQuery } = teamSlice.actions;
|
||||
|
||||
export const teamReducer = teamSlice.reducer;
|
||||
|
||||
export default {
|
||||
teams: teamsReducer,
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { UsersActionBar, Props } from './UsersActionBar';
|
||||
import { Props, UsersActionBar } from './UsersActionBar';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
import { setUsersSearchQuery } from './state/reducers';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
searchQuery: '',
|
||||
setUsersSearchQuery: jest.fn(),
|
||||
setUsersSearchQuery: mockToolkitActionCreator(setUsersSearchQuery),
|
||||
onShowInvites: jest.fn(),
|
||||
pendingInvitesCount: 0,
|
||||
canInvite: false,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { setUsersSearchQuery } from './state/actions';
|
||||
import { setUsersSearchQuery } from './state/reducers';
|
||||
import { getInviteesCount, getUsersSearchQuery } from './state/selectors';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { UsersListPage, Props } from './UsersListPage';
|
||||
import { Props, UsersListPage } from './UsersListPage';
|
||||
import { Invitee, OrgUser } from 'app/types';
|
||||
import { getMockUser } from './__mocks__/userMocks';
|
||||
import appEvents from '../../core/app_events';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
import { setUsersSearchQuery } from './state/reducers';
|
||||
|
||||
jest.mock('../../core/app_events', () => ({
|
||||
emit: jest.fn(),
|
||||
@ -28,7 +30,7 @@ const setup = (propOverrides?: object) => {
|
||||
loadUsers: jest.fn(),
|
||||
updateUser: jest.fn(),
|
||||
removeUser: jest.fn(),
|
||||
setUsersSearchQuery: jest.fn(),
|
||||
setUsersSearchQuery: mockToolkitActionCreator(setUsersSearchQuery),
|
||||
hasFetched: false,
|
||||
};
|
||||
|
||||
|
@ -1,17 +1,18 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { renderMarkdown } from '@grafana/data';
|
||||
import { NavModel, renderMarkdown } from '@grafana/data';
|
||||
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import UsersActionBar from './UsersActionBar';
|
||||
import UsersTable from './UsersTable';
|
||||
import InviteesTable from './InviteesTable';
|
||||
import { Invitee, OrgUser, CoreEvents } from 'app/types';
|
||||
import { CoreEvents, Invitee, OrgUser } from 'app/types';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { loadUsers, loadInvitees, setUsersSearchQuery, updateUser, removeUser } from './state/actions';
|
||||
import { loadInvitees, loadUsers, removeUser, updateUser } from './state/actions';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { getInvitees, getUsers, getUsersSearchQuery } from './state/selectors';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { setUsersSearchQuery } from './state/reducers';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
|
@ -1,47 +1,7 @@
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { StoreState } from '../../../types';
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { Invitee, OrgUser } from 'app/types';
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadUsers = 'LOAD_USERS',
|
||||
LoadInvitees = 'LOAD_INVITEES',
|
||||
SetUsersSearchQuery = 'SET_USERS_SEARCH_QUERY',
|
||||
}
|
||||
|
||||
export interface LoadUsersAction {
|
||||
type: ActionTypes.LoadUsers;
|
||||
payload: OrgUser[];
|
||||
}
|
||||
|
||||
export interface LoadInviteesAction {
|
||||
type: ActionTypes.LoadInvitees;
|
||||
payload: Invitee[];
|
||||
}
|
||||
|
||||
export interface SetUsersSearchQueryAction {
|
||||
type: ActionTypes.SetUsersSearchQuery;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
const usersLoaded = (users: OrgUser[]): LoadUsersAction => ({
|
||||
type: ActionTypes.LoadUsers,
|
||||
payload: users,
|
||||
});
|
||||
|
||||
const inviteesLoaded = (invitees: Invitee[]): LoadInviteesAction => ({
|
||||
type: ActionTypes.LoadInvitees,
|
||||
payload: invitees,
|
||||
});
|
||||
|
||||
export const setUsersSearchQuery = (query: string): SetUsersSearchQueryAction => ({
|
||||
type: ActionTypes.SetUsersSearchQuery,
|
||||
payload: query,
|
||||
});
|
||||
|
||||
export type Action = LoadUsersAction | SetUsersSearchQueryAction | LoadInviteesAction;
|
||||
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||
import { OrgUser } from 'app/types';
|
||||
import { inviteesLoaded, usersLoaded } from './reducers';
|
||||
|
||||
export function loadUsers(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
|
44
public/app/features/users/state/reducers.test.ts
Normal file
44
public/app/features/users/state/reducers.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { UsersState } from '../../../types';
|
||||
import { initialState, inviteesLoaded, setUsersSearchQuery, usersLoaded, usersReducer } from './reducers';
|
||||
import { getMockInvitees, getMockUsers } from '../__mocks__/userMocks';
|
||||
|
||||
describe('usersReducer', () => {
|
||||
describe('when usersLoaded is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<UsersState>()
|
||||
.givenReducer(usersReducer, { ...initialState })
|
||||
.whenActionIsDispatched(usersLoaded(getMockUsers(1)))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
users: getMockUsers(1),
|
||||
hasFetched: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when inviteesLoaded is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<UsersState>()
|
||||
.givenReducer(usersReducer, { ...initialState })
|
||||
.whenActionIsDispatched(inviteesLoaded(getMockInvitees(1)))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
invitees: getMockInvitees(1),
|
||||
hasFetched: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setUsersSearchQuery is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<UsersState>()
|
||||
.givenReducer(usersReducer, { ...initialState })
|
||||
.whenActionIsDispatched(setUsersSearchQuery('a query'))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
searchQuery: 'a query',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { Invitee, OrgUser, UsersState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export const initialState: UsersState = {
|
||||
@ -13,20 +14,25 @@ export const initialState: UsersState = {
|
||||
hasFetched: false,
|
||||
};
|
||||
|
||||
export const usersReducer = (state = initialState, action: Action): UsersState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadUsers:
|
||||
const usersSlice = createSlice({
|
||||
name: 'users',
|
||||
initialState,
|
||||
reducers: {
|
||||
usersLoaded: (state, action: PayloadAction<OrgUser[]>): UsersState => {
|
||||
return { ...state, hasFetched: true, users: action.payload };
|
||||
|
||||
case ActionTypes.LoadInvitees:
|
||||
},
|
||||
inviteesLoaded: (state, action: PayloadAction<Invitee[]>): UsersState => {
|
||||
return { ...state, hasFetched: true, invitees: action.payload };
|
||||
|
||||
case ActionTypes.SetUsersSearchQuery:
|
||||
},
|
||||
setUsersSearchQuery: (state, action: PayloadAction<string>): UsersState => {
|
||||
return { ...state, searchQuery: action.payload };
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
export const { inviteesLoaded, setUsersSearchQuery, usersLoaded } = usersSlice.actions;
|
||||
|
||||
export const usersReducer = usersSlice.reducer;
|
||||
|
||||
export default {
|
||||
users: usersReducer,
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { applyMiddleware, compose, createStore } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { configureStore as reduxConfigureStore } from '@reduxjs/toolkit';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { setStore } from './store';
|
||||
import { StoreState } from 'app/types/store';
|
||||
import { toggleLogActionsMiddleware } from 'app/core/middlewares/application';
|
||||
import { addReducer, createRootReducer } from '../core/reducers/root';
|
||||
import { ActionOf } from 'app/core/redux';
|
||||
import { buildInitialState } from '../core/reducers/navModel';
|
||||
|
||||
export function addRootReducer(reducers: any) {
|
||||
// this is ok now because we add reducers before configureStore is called
|
||||
@ -16,23 +16,23 @@ export function addRootReducer(reducers: any) {
|
||||
}
|
||||
|
||||
export function configureStore() {
|
||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
const logger = createLogger({
|
||||
predicate: (getState: () => StoreState) => {
|
||||
return getState().application.logActions;
|
||||
},
|
||||
});
|
||||
const storeEnhancers =
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? applyMiddleware(toggleLogActionsMiddleware, thunk, logger)
|
||||
: applyMiddleware(thunk);
|
||||
|
||||
const store = createStore<StoreState, ActionOf<any>, any, any>(
|
||||
createRootReducer(),
|
||||
{},
|
||||
composeEnhancers(storeEnhancers)
|
||||
);
|
||||
const middleware = process.env.NODE_ENV !== 'production' ? [toggleLogActionsMiddleware, thunk, logger] : [thunk];
|
||||
|
||||
const store = reduxConfigureStore({
|
||||
reducer: createRootReducer(),
|
||||
middleware,
|
||||
devTools: process.env.NODE_ENV !== 'production',
|
||||
preloadedState: {
|
||||
navIndex: buildInitialState(),
|
||||
},
|
||||
});
|
||||
|
||||
setStore(store);
|
||||
return store;
|
||||
}
|
||||
|
@ -16,9 +16,12 @@ export interface AlertRuleDTO {
|
||||
export interface AlertRule {
|
||||
id: number;
|
||||
dashboardId: number;
|
||||
dashboardUid?: string;
|
||||
dashboardSlug?: string;
|
||||
panelId: number;
|
||||
name: string;
|
||||
state: string;
|
||||
newStateDate?: string;
|
||||
stateText: string;
|
||||
stateIcon: string;
|
||||
stateClass: string;
|
||||
@ -26,6 +29,7 @@ export interface AlertRule {
|
||||
url: string;
|
||||
info?: string;
|
||||
executionError?: string;
|
||||
evalDate?: string;
|
||||
evalData?: { noData?: boolean; evalMatches?: any };
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ThunkAction, ThunkDispatch as GenericThunkDispatch } from 'redux-thunk';
|
||||
import { ActionOf } from 'app/core/redux';
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { NavIndex } from '@grafana/data';
|
||||
|
||||
import { LocationState } from './location';
|
||||
import { AlertRulesState } from './alerting';
|
||||
@ -12,7 +13,6 @@ import { UsersState, UserState } from './user';
|
||||
import { OrganizationState } from './organization';
|
||||
import { AppNotificationsState } from './appNotifications';
|
||||
import { PluginsState } from './plugins';
|
||||
import { NavIndex } from '@grafana/data';
|
||||
import { ApplicationState } from './application';
|
||||
import { LdapState, LdapUserState } from './ldap';
|
||||
import { PanelEditorState } from '../features/dashboard/panel_editor/state/reducers';
|
||||
@ -43,6 +43,6 @@ export interface StoreState {
|
||||
/*
|
||||
* Utility type to get strongly types thunks
|
||||
*/
|
||||
export type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ActionOf<any>>;
|
||||
export type ThunkResult<R> = ThunkAction<R, StoreState, undefined, PayloadAction<any>>;
|
||||
|
||||
export type ThunkDispatch = GenericThunkDispatch<StoreState, undefined, any>;
|
||||
|
12
public/test/core/redux/mocks.ts
Normal file
12
public/test/core/redux/mocks.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ActionCreatorWithoutPayload, PayloadActionCreator } from '@reduxjs/toolkit';
|
||||
|
||||
export const mockToolkitActionCreator = (creator: PayloadActionCreator<any>) => {
|
||||
return Object.assign(jest.fn(), creator);
|
||||
};
|
||||
|
||||
export type ToolkitActionCreatorWithoutPayloadMockType = typeof mockToolkitActionCreatorWithoutPayload &
|
||||
ActionCreatorWithoutPayload<any>;
|
||||
|
||||
export const mockToolkitActionCreatorWithoutPayload = (creator: ActionCreatorWithoutPayload<any>) => {
|
||||
return Object.assign(jest.fn(), creator);
|
||||
};
|
@ -1,4 +1,6 @@
|
||||
import { reducerFactory, actionCreatorFactory } from 'app/core/redux';
|
||||
import { AnyAction } from 'redux';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { reducerTester } from './reducerTester';
|
||||
|
||||
interface DummyState {
|
||||
@ -9,29 +11,27 @@ const initialState: DummyState = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const dummyAction = actionCreatorFactory<string>('dummyAction').create();
|
||||
const dummyAction = createAction<string>('dummyAction');
|
||||
|
||||
const mutatingReducer = reducerFactory(initialState)
|
||||
.addMapper({
|
||||
filter: dummyAction,
|
||||
mapper: (state, action) => {
|
||||
state.data.push(action.payload);
|
||||
return state;
|
||||
},
|
||||
})
|
||||
.create();
|
||||
const mutatingReducer = (state: DummyState = initialState, action: AnyAction): DummyState => {
|
||||
if (dummyAction.match(action)) {
|
||||
state.data.push(action.payload);
|
||||
return state;
|
||||
}
|
||||
|
||||
const okReducer = reducerFactory(initialState)
|
||||
.addMapper({
|
||||
filter: dummyAction,
|
||||
mapper: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
data: state.data.concat(action.payload),
|
||||
};
|
||||
},
|
||||
})
|
||||
.create();
|
||||
return state;
|
||||
};
|
||||
|
||||
const okReducer = (state: DummyState = initialState, action: AnyAction): DummyState => {
|
||||
if (dummyAction.match(action)) {
|
||||
return {
|
||||
...state,
|
||||
data: state.data.concat(action.payload),
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
describe('reducerTester', () => {
|
||||
describe('when reducer mutates state', () => {
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { Reducer } from 'redux';
|
||||
|
||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export interface Given<State> {
|
||||
givenReducer: (reducer: Reducer<State, ActionOf<any>>, state: State, disableDeepFreeze?: boolean) => When<State>;
|
||||
givenReducer: (reducer: Reducer<State, PayloadAction<any>>, state: State, disableDeepFreeze?: boolean) => When<State>;
|
||||
}
|
||||
|
||||
export interface When<State> {
|
||||
whenActionIsDispatched: (action: ActionOf<any>) => Then<State>;
|
||||
whenActionIsDispatched: (action: PayloadAction<any>) => Then<State>;
|
||||
}
|
||||
|
||||
export interface Then<State> {
|
||||
@ -50,12 +49,12 @@ export const deepFreeze = <T>(obj: T): T => {
|
||||
interface ReducerTester<State> extends Given<State>, When<State>, Then<State> {}
|
||||
|
||||
export const reducerTester = <State>(): Given<State> => {
|
||||
let reducerUnderTest: Reducer<State, ActionOf<any>>;
|
||||
let reducerUnderTest: Reducer<State, PayloadAction<any>>;
|
||||
let resultingState: State;
|
||||
let initialState: State;
|
||||
|
||||
const givenReducer = (
|
||||
reducer: Reducer<State, ActionOf<any>>,
|
||||
reducer: Reducer<State, PayloadAction<any>>,
|
||||
state: State,
|
||||
disableDeepFreeze = false
|
||||
): When<State> => {
|
||||
@ -68,7 +67,7 @@ export const reducerTester = <State>(): Given<State> => {
|
||||
return instance;
|
||||
};
|
||||
|
||||
const whenActionIsDispatched = (action: ActionOf<any>): Then<State> => {
|
||||
const whenActionIsDispatched = (action: PayloadAction<any>): Then<State> => {
|
||||
resultingState = reducerUnderTest(resultingState || initialState, action);
|
||||
|
||||
return instance;
|
||||
|
@ -1,7 +1,7 @@
|
||||
// @ts-ignore
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
@ -10,13 +10,13 @@ export interface ThunkGiven {
|
||||
}
|
||||
|
||||
export interface ThunkWhen {
|
||||
whenThunkIsDispatched: (...args: any) => Promise<Array<ActionOf<any>>>;
|
||||
whenThunkIsDispatched: (...args: any) => Promise<Array<PayloadAction<any>>>;
|
||||
}
|
||||
|
||||
export const thunkTester = (initialState: any, debug?: boolean): ThunkGiven => {
|
||||
const store = mockStore(initialState);
|
||||
let thunkUnderTest: any = null;
|
||||
let dispatchedActions: Array<ActionOf<any>> = [];
|
||||
let dispatchedActions: Array<PayloadAction<any>> = [];
|
||||
|
||||
const givenThunk = (thunkFunction: any): ThunkWhen => {
|
||||
thunkUnderTest = thunkFunction;
|
||||
@ -24,7 +24,7 @@ export const thunkTester = (initialState: any, debug?: boolean): ThunkGiven => {
|
||||
return instance;
|
||||
};
|
||||
|
||||
const whenThunkIsDispatched = async (...args: any): Promise<Array<ActionOf<any>>> => {
|
||||
const whenThunkIsDispatched = async (...args: any): Promise<Array<PayloadAction<any>>> => {
|
||||
await store.dispatch(thunkUnderTest(...args));
|
||||
|
||||
dispatchedActions = store.getActions();
|
||||
|
36
yarn.lock
36
yarn.lock
@ -2914,6 +2914,18 @@
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
warning "^3.0.0"
|
||||
|
||||
"@reduxjs/toolkit@1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.2.1.tgz#2a68b608ff377a5409272c0a0ea08d87047007c1"
|
||||
integrity sha512-mVvcjAyPS91wbAG2JoLIVP0JiWoLYHCA5WYevjHBjxhTlBPa3lYbUS9/IqanGPIt9TmSYON42l3BTPfWsfr2wg==
|
||||
dependencies:
|
||||
immer "^4.0.1"
|
||||
redux "^4.0.0"
|
||||
redux-devtools-extension "^2.13.8"
|
||||
redux-immutable-state-invariant "^2.1.0"
|
||||
redux-thunk "^2.3.0"
|
||||
reselect "^4.0.0"
|
||||
|
||||
"@rtsao/plugin-proposal-class-properties@7.0.1-patch.1":
|
||||
version "7.0.1-patch.1"
|
||||
resolved "https://registry.yarnpkg.com/@rtsao/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.0.1-patch.1.tgz#ac0f758a25a85b5be0e70a25f6e5b58103c58391"
|
||||
@ -11579,6 +11591,11 @@ immer@1.10.0:
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
|
||||
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
|
||||
|
||||
immer@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-4.0.2.tgz#9ff0fcdf88e06f92618a5978ceecb5884e633559"
|
||||
integrity sha512-Q/tm+yKqnKy4RIBmmtISBlhXuSDrB69e9EKTYiIenIKQkXBQir43w+kN/eGiax3wt1J0O1b2fYcNqLSbEcXA7w==
|
||||
|
||||
immutable@3.8.2, immutable@^3.8.2:
|
||||
version "3.8.2"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
|
||||
@ -11786,7 +11803,7 @@ interpret@^1.0.0, interpret@^1.1.0, interpret@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
|
||||
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
|
||||
|
||||
invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4:
|
||||
invariant@^2.1.0, invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
|
||||
@ -18318,6 +18335,19 @@ reduce-flatten@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327"
|
||||
integrity sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=
|
||||
|
||||
redux-devtools-extension@^2.13.8:
|
||||
version "2.13.8"
|
||||
resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1"
|
||||
integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==
|
||||
|
||||
redux-immutable-state-invariant@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz#308fd3cc7415a0e7f11f51ec997b6379c7055ce1"
|
||||
integrity sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==
|
||||
dependencies:
|
||||
invariant "^2.1.0"
|
||||
json-stringify-safe "^5.0.1"
|
||||
|
||||
redux-logger@3.0.6:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf"
|
||||
@ -18332,7 +18362,7 @@ redux-mock-store@1.5.3:
|
||||
dependencies:
|
||||
lodash.isplainobject "^4.0.6"
|
||||
|
||||
redux-thunk@2.3.0:
|
||||
redux-thunk@2.3.0, redux-thunk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
|
||||
@ -18654,7 +18684,7 @@ requires-port@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
reselect@*, reselect@4.0.0:
|
||||
reselect@*, reselect@4.0.0, reselect@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
|
||||
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
|
||||
|
Loading…
Reference in New Issue
Block a user