mirror of
https://github.com/grafana/grafana.git
synced 2025-01-08 15:13:30 -06:00
Added Loading state on org pages
This commit is contained in:
parent
974eddee8f
commit
02e7d713a1
17
public/app/core/components/PageLoader/PageLoader.tsx
Normal file
17
public/app/core/components/PageLoader/PageLoader.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React, { SFC } from 'react';
|
||||
|
||||
interface Props {
|
||||
pageName: string;
|
||||
}
|
||||
|
||||
const PageLoader: SFC<Props> = ({ pageName }) => {
|
||||
const loadingText = `Loading ${pageName}...`;
|
||||
return (
|
||||
<div className="page-loader-wrapper">
|
||||
<i className="page-loader-wrapper__spinner fa fa-spinner fa-spin" />
|
||||
<div className="page-loader-wrapper__text">{loadingText}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageLoader;
|
@ -9,6 +9,7 @@ const setup = (propOverrides?: object) => {
|
||||
navModel: {} as NavModel,
|
||||
apiKeys: [] as ApiKey[],
|
||||
searchQuery: '',
|
||||
hasFetched: false,
|
||||
loadApiKeys: jest.fn(),
|
||||
deleteApiKey: jest.fn(),
|
||||
setSearchQuery: jest.fn(),
|
||||
@ -35,6 +36,7 @@ describe('Render', () => {
|
||||
it('should render API keys table', () => {
|
||||
const { wrapper } = setup({
|
||||
apiKeys: getMultipleMockKeys(5),
|
||||
hasFetched: true,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
@ -8,6 +8,7 @@ import { getApiKeys } from './state/selectors';
|
||||
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import ApiKeysAddedModal from './ApiKeysAddedModal';
|
||||
import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@ -16,6 +17,7 @@ export interface Props {
|
||||
navModel: NavModel;
|
||||
apiKeys: ApiKey[];
|
||||
searchQuery: string;
|
||||
hasFetched: boolean;
|
||||
loadApiKeys: typeof loadApiKeys;
|
||||
deleteApiKey: typeof deleteApiKey;
|
||||
setSearchQuery: typeof setSearchQuery;
|
||||
@ -99,9 +101,45 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
});
|
||||
};
|
||||
|
||||
renderTable() {
|
||||
const { apiKeys } = this.props;
|
||||
|
||||
return [
|
||||
<h3 key="header" className="page-heading">
|
||||
Existing Keys
|
||||
</h3>,
|
||||
<table key="table" className="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
{apiKeys.length > 0 && (
|
||||
<tbody>
|
||||
{apiKeys.map(key => {
|
||||
return (
|
||||
<tr key={key.id}>
|
||||
<td>{key.name}</td>
|
||||
<td>{key.role}</td>
|
||||
<td>
|
||||
<a onClick={() => this.onDeleteApiKey(key)} className="btn btn-danger btn-mini">
|
||||
<i className="fa fa-remove" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
)}
|
||||
</table>,
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { newApiKey, isAdding } = this.state;
|
||||
const { navModel, apiKeys, searchQuery } = this.props;
|
||||
const { hasFetched, navModel, searchQuery } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -170,34 +208,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
</form>
|
||||
</div>
|
||||
</SlideDown>
|
||||
|
||||
<h3 className="page-heading">Existing Keys</h3>
|
||||
<table className="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th style={{ width: '34px' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
{apiKeys.length > 0 ? (
|
||||
<tbody>
|
||||
{apiKeys.map(key => {
|
||||
return (
|
||||
<tr key={key.id}>
|
||||
<td>{key.name}</td>
|
||||
<td>{key.role}</td>
|
||||
<td>
|
||||
<a onClick={() => this.onDeleteApiKey(key)} className="btn btn-danger btn-mini">
|
||||
<i className="fa fa-remove" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
) : null}
|
||||
</table>
|
||||
{hasFetched ? this.renderTable() : <PageLoader pageName="Api keys" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -209,6 +220,7 @@ function mapStateToProps(state) {
|
||||
navModel: getNavModel(state.navIndex, 'apikeys'),
|
||||
apiKeys: getApiKeys(state.apiKeys),
|
||||
searchQuery: state.apiKeys.searchQuery,
|
||||
hasFetched: state.apiKeys.hasFetched,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -138,11 +138,13 @@ exports[`Render should render API keys table 1`] = `
|
||||
</Component>
|
||||
<h3
|
||||
className="page-heading"
|
||||
key="header"
|
||||
>
|
||||
Existing Keys
|
||||
</h3>
|
||||
<table
|
||||
className="filter-table"
|
||||
key="table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -404,32 +406,9 @@ exports[`Render should render component 1`] = `
|
||||
</form>
|
||||
</div>
|
||||
</Component>
|
||||
<h3
|
||||
className="page-heading"
|
||||
>
|
||||
Existing Keys
|
||||
</h3>
|
||||
<table
|
||||
className="filter-table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Role
|
||||
</th>
|
||||
<th
|
||||
style={
|
||||
Object {
|
||||
"width": "34px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<PageLoader
|
||||
pageName="Api keys"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -4,12 +4,13 @@ import { Action, ActionTypes } from './actions';
|
||||
export const initialApiKeysState: ApiKeysState = {
|
||||
keys: [],
|
||||
searchQuery: '',
|
||||
hasFetched: false,
|
||||
};
|
||||
|
||||
export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadApiKeys:
|
||||
return { ...state, keys: action.payload };
|
||||
return { ...state, hasFetched: true, keys: action.payload };
|
||||
case ActionTypes.SetApiKeysSearchQuery:
|
||||
return { ...state, searchQuery: action.payload };
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ describe('API Keys selectors', () => {
|
||||
const mockKeys = getMultipleMockKeys(5);
|
||||
|
||||
it('should return all keys if no search query', () => {
|
||||
const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '' };
|
||||
const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '', hasFetched: false };
|
||||
|
||||
const keys = getApiKeys(mockState);
|
||||
|
||||
@ -15,7 +15,7 @@ describe('API Keys selectors', () => {
|
||||
});
|
||||
|
||||
it('should filter keys if search query exists', () => {
|
||||
const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '5' };
|
||||
const mockState: ApiKeysState = { keys: mockKeys, searchQuery: '5', hasFetched: false };
|
||||
|
||||
const keys = getApiKeys(mockState);
|
||||
|
||||
|
@ -15,6 +15,7 @@ const setup = (propOverrides?: object) => {
|
||||
searchQuery: '',
|
||||
setDataSourcesSearchQuery: jest.fn(),
|
||||
setDataSourcesLayoutMode: jest.fn(),
|
||||
hasFetched: false,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
@ -33,6 +34,7 @@ describe('Render', () => {
|
||||
const wrapper = setup({
|
||||
dataSources: getMockDataSources(5),
|
||||
dataSourcesCount: 5,
|
||||
hasFetched: true,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import PageHeader from '../../core/components/PageHeader/PageHeader';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import OrgActionBar from '../../core/components/OrgActionBar/OrgActionBar';
|
||||
import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA';
|
||||
import DataSourcesList from './DataSourcesList';
|
||||
@ -22,6 +23,7 @@ export interface Props {
|
||||
dataSourcesCount: number;
|
||||
layoutMode: LayoutMode;
|
||||
searchQuery: string;
|
||||
hasFetched: boolean;
|
||||
loadDataSources: typeof loadDataSources;
|
||||
setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode;
|
||||
setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery;
|
||||
@ -56,6 +58,7 @@ export class DataSourcesListPage extends PureComponent<Props> {
|
||||
searchQuery,
|
||||
setDataSourcesSearchQuery,
|
||||
setDataSourcesLayoutMode,
|
||||
hasFetched,
|
||||
} = this.props;
|
||||
|
||||
const linkButton = {
|
||||
@ -67,10 +70,10 @@ export class DataSourcesListPage extends PureComponent<Props> {
|
||||
<div>
|
||||
<PageHeader model={navModel} />
|
||||
<div className="page-container page-body">
|
||||
{dataSourcesCount === 0 ? (
|
||||
<EmptyListCTA model={emptyListModel} />
|
||||
) : (
|
||||
[
|
||||
{!hasFetched && <PageLoader pageName="Data sources" />}
|
||||
{hasFetched && dataSourcesCount === 0 && <EmptyListCTA model={emptyListModel} />}
|
||||
{hasFetched &&
|
||||
dataSourcesCount > 0 && [
|
||||
<OrgActionBar
|
||||
layoutMode={layoutMode}
|
||||
searchQuery={searchQuery}
|
||||
@ -80,8 +83,7 @@ export class DataSourcesListPage extends PureComponent<Props> {
|
||||
key="action-bar"
|
||||
/>,
|
||||
<DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
|
||||
]
|
||||
)}
|
||||
]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -95,6 +97,7 @@ function mapStateToProps(state) {
|
||||
layoutMode: getDataSourcesLayoutMode(state.dataSources),
|
||||
dataSourcesCount: getDataSourcesCount(state.dataSources),
|
||||
searchQuery: getDataSourcesSearchQuery(state.dataSources),
|
||||
hasFetched: state.dataSources.hasFetched,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -155,19 +155,8 @@ exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<EmptyListCTA
|
||||
model={
|
||||
Object {
|
||||
"buttonIcon": "gicon gicon-add-datasources",
|
||||
"buttonLink": "datasources/new",
|
||||
"buttonTitle": "Add data source",
|
||||
"proTip": "You can also define data sources through configuration files.",
|
||||
"proTipLink": "http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list",
|
||||
"proTipLinkTitle": "Learn more",
|
||||
"proTipTarget": "_blank",
|
||||
"title": "There are no data sources defined yet",
|
||||
}
|
||||
}
|
||||
<PageLoader
|
||||
pageName="Data sources"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,12 +9,13 @@ const initialState: DataSourcesState = {
|
||||
dataSourcesCount: 0,
|
||||
dataSourceTypes: [] as Plugin[],
|
||||
dataSourceTypeSearchQuery: '',
|
||||
hasFetched: false,
|
||||
};
|
||||
|
||||
export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadDataSources:
|
||||
return { ...state, dataSources: action.payload, dataSourcesCount: action.payload.length };
|
||||
return { ...state, hasFetched: true, dataSources: action.payload, dataSourcesCount: action.payload.length };
|
||||
|
||||
case ActionTypes.SetDataSourcesSearchQuery:
|
||||
return { ...state, searchQuery: action.payload };
|
||||
|
@ -13,22 +13,25 @@ const setup = (propOverrides?: object) => {
|
||||
setPluginsLayoutMode: jest.fn(),
|
||||
layoutMode: LayoutModes.Grid,
|
||||
loadPlugins: jest.fn(),
|
||||
hasFetched: false,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<PluginListPage {...props} />);
|
||||
const instance = wrapper.instance() as PluginListPage;
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
instance,
|
||||
};
|
||||
return shallow(<PluginListPage {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const { wrapper } = setup();
|
||||
const wrapper = setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render list', () => {
|
||||
const wrapper = setup({
|
||||
hasFetched: true,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import PluginList from './PluginList';
|
||||
import { NavModel, Plugin } from 'app/types';
|
||||
import { loadPlugins, setPluginsLayoutMode, setPluginsSearchQuery } from './state/actions';
|
||||
@ -15,6 +16,7 @@ export interface Props {
|
||||
plugins: Plugin[];
|
||||
layoutMode: LayoutMode;
|
||||
searchQuery: string;
|
||||
hasFetched: boolean;
|
||||
loadPlugins: typeof loadPlugins;
|
||||
setPluginsLayoutMode: typeof setPluginsLayoutMode;
|
||||
setPluginsSearchQuery: typeof setPluginsSearchQuery;
|
||||
@ -30,12 +32,21 @@ export class PluginListPage extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navModel, plugins, layoutMode, setPluginsLayoutMode, setPluginsSearchQuery, searchQuery } = this.props;
|
||||
const {
|
||||
hasFetched,
|
||||
navModel,
|
||||
plugins,
|
||||
layoutMode,
|
||||
setPluginsLayoutMode,
|
||||
setPluginsSearchQuery,
|
||||
searchQuery,
|
||||
} = this.props;
|
||||
|
||||
const linkButton = {
|
||||
href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list',
|
||||
title: 'Find more plugins on Grafana.com',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={navModel} />
|
||||
@ -47,7 +58,11 @@ export class PluginListPage extends PureComponent<Props> {
|
||||
setSearchQuery={query => setPluginsSearchQuery(query)}
|
||||
linkButton={linkButton}
|
||||
/>
|
||||
{plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />}
|
||||
{hasFetched ? (
|
||||
plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />
|
||||
) : (
|
||||
<PageLoader pageName="Plugins" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -60,6 +75,7 @@ function mapStateToProps(state) {
|
||||
plugins: getPlugins(state.plugins),
|
||||
layoutMode: getLayoutMode(state.plugins),
|
||||
searchQuery: getPluginsSearchQuery(state.plugins),
|
||||
hasFetched: state.plugins.hasFetched,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<OrgActionBar
|
||||
layoutMode="grid"
|
||||
linkButton={
|
||||
Object {
|
||||
"href": "https://grafana.com/plugins?utm_source=grafana_plugin_list",
|
||||
"title": "Find more plugins on Grafana.com",
|
||||
}
|
||||
}
|
||||
onSetLayoutMode={[Function]}
|
||||
searchQuery=""
|
||||
setSearchQuery={[Function]}
|
||||
/>
|
||||
<PageLoader
|
||||
pageName="Plugins"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render list 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
|
@ -6,12 +6,13 @@ export const initialState: PluginsState = {
|
||||
plugins: [] as Plugin[],
|
||||
searchQuery: '',
|
||||
layoutMode: LayoutModes.Grid,
|
||||
hasFetched: false,
|
||||
};
|
||||
|
||||
export const pluginsReducer = (state = initialState, action: Action): PluginsState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadPlugins:
|
||||
return { ...state, plugins: action.payload };
|
||||
return { ...state, hasFetched: true, plugins: action.payload };
|
||||
|
||||
case ActionTypes.SetPluginsSearchQuery:
|
||||
return { ...state, searchQuery: action.payload };
|
||||
|
@ -13,6 +13,7 @@ const setup = (propOverrides?: object) => {
|
||||
setSearchQuery: jest.fn(),
|
||||
searchQuery: '',
|
||||
teamsCount: 0,
|
||||
hasFetched: false,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
@ -36,6 +37,7 @@ describe('Render', () => {
|
||||
const { wrapper } = setup({
|
||||
teams: getMultipleMockTeams(5),
|
||||
teamsCount: 5,
|
||||
hasFetched: true,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
@ -4,6 +4,7 @@ import { hot } from 'react-hot-loader';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { NavModel, Team } from '../../types';
|
||||
import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
|
||||
import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
|
||||
@ -14,6 +15,7 @@ export interface Props {
|
||||
teams: Team[];
|
||||
searchQuery: string;
|
||||
teamsCount: number;
|
||||
hasFetched: boolean;
|
||||
loadTeams: typeof loadTeams;
|
||||
deleteTeam: typeof deleteTeam;
|
||||
setSearchQuery: typeof setSearchQuery;
|
||||
@ -125,13 +127,23 @@ export class TeamList extends PureComponent<Props, any> {
|
||||
);
|
||||
}
|
||||
|
||||
renderList() {
|
||||
const { teamsCount } = this.props;
|
||||
|
||||
if (teamsCount > 0) {
|
||||
return this.renderTeamList();
|
||||
} else {
|
||||
return this.renderEmptyList();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navModel, teamsCount } = this.props;
|
||||
const { hasFetched, navModel } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader model={navModel} />
|
||||
{teamsCount > 0 ? this.renderTeamList() : this.renderEmptyList()}
|
||||
{hasFetched ? this.renderList() : <PageLoader pageName="Teams" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -143,6 +155,7 @@ function mapStateToProps(state) {
|
||||
teams: getTeams(state.teams),
|
||||
searchQuery: getSearchQuery(state.teams),
|
||||
teamsCount: getTeamsCount(state.teams),
|
||||
hasFetched: state.teams.hasFetched,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -5,24 +5,9 @@ exports[`Render should render component 1`] = `
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<EmptyListCTA
|
||||
model={
|
||||
Object {
|
||||
"buttonIcon": "fa fa-plus",
|
||||
"buttonLink": "org/teams/new",
|
||||
"buttonTitle": " New team",
|
||||
"proTip": "Assign folder and dashboard permissions to teams instead of users to ease administration.",
|
||||
"proTipLink": "",
|
||||
"proTipLinkTitle": "",
|
||||
"proTipTarget": "_blank",
|
||||
"title": "You haven't created any teams yet.",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<PageLoader
|
||||
pageName="Teams"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
|
||||
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' };
|
||||
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '', hasFetched: false };
|
||||
export const initialTeamState: TeamState = {
|
||||
team: {} as Team,
|
||||
members: [] as TeamMember[],
|
||||
@ -12,7 +12,7 @@ export const initialTeamState: TeamState = {
|
||||
export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadTeams:
|
||||
return { ...state, teams: action.payload };
|
||||
return { ...state, hasFetched: true, teams: action.payload };
|
||||
|
||||
case ActionTypes.SetSearchQuery:
|
||||
return { ...state, searchQuery: action.payload };
|
||||
|
@ -7,7 +7,7 @@ describe('Teams selectors', () => {
|
||||
const mockTeams = getMultipleMockTeams(5);
|
||||
|
||||
it('should return teams if no search query', () => {
|
||||
const mockState: TeamsState = { teams: mockTeams, searchQuery: '' };
|
||||
const mockState: TeamsState = { teams: mockTeams, searchQuery: '', hasFetched: false };
|
||||
|
||||
const teams = getTeams(mockState);
|
||||
|
||||
@ -15,7 +15,7 @@ describe('Teams selectors', () => {
|
||||
});
|
||||
|
||||
it('Should filter teams if search query', () => {
|
||||
const mockState: TeamsState = { teams: mockTeams, searchQuery: '5' };
|
||||
const mockState: TeamsState = { teams: mockTeams, searchQuery: '5', hasFetched: false };
|
||||
|
||||
const teams = getTeams(mockState);
|
||||
|
||||
|
@ -22,6 +22,7 @@ const setup = (propOverrides?: object) => {
|
||||
updateUser: jest.fn(),
|
||||
removeUser: jest.fn(),
|
||||
setUsersSearchQuery: jest.fn(),
|
||||
hasFetched: false,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
@ -41,6 +42,14 @@ describe('Render', () => {
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render List page', () => {
|
||||
const { wrapper } = setup({
|
||||
hasFetched: true,
|
||||
});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Functions', () => {
|
||||
|
@ -3,8 +3,9 @@ import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import Remarkable from 'remarkable';
|
||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import UsersActionBar from './UsersActionBar';
|
||||
import UsersTable from 'app/features/users/UsersTable';
|
||||
import UsersTable from './UsersTable';
|
||||
import InviteesTable from './InviteesTable';
|
||||
import { Invitee, NavModel, OrgUser } from 'app/types';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@ -18,6 +19,7 @@ export interface Props {
|
||||
users: OrgUser[];
|
||||
searchQuery: string;
|
||||
externalUserMngInfo: string;
|
||||
hasFetched: boolean;
|
||||
loadUsers: typeof loadUsers;
|
||||
loadInvitees: typeof loadInvitees;
|
||||
setUsersSearchQuery: typeof setUsersSearchQuery;
|
||||
@ -87,8 +89,24 @@ export class UsersListPage extends PureComponent<Props, State> {
|
||||
}));
|
||||
};
|
||||
|
||||
renderTable() {
|
||||
const { invitees, users } = this.props;
|
||||
|
||||
if (this.state.showInvites) {
|
||||
return <InviteesTable invitees={invitees} onRevokeInvite={code => this.onRevokeInvite(code)} />;
|
||||
} else {
|
||||
return (
|
||||
<UsersTable
|
||||
users={users}
|
||||
onRoleChange={(role, user) => this.onRoleChange(role, user)}
|
||||
onRemoveUser={user => this.onRemoveUser(user)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { invitees, navModel, users } = this.props;
|
||||
const { navModel, hasFetched } = this.props;
|
||||
const externalUserMngInfoHtml = this.externalUserMngInfoHtml;
|
||||
|
||||
return (
|
||||
@ -99,15 +117,7 @@ export class UsersListPage extends PureComponent<Props, State> {
|
||||
{externalUserMngInfoHtml && (
|
||||
<div className="grafana-info-box" dangerouslySetInnerHTML={{ __html: externalUserMngInfoHtml }} />
|
||||
)}
|
||||
{this.state.showInvites ? (
|
||||
<InviteesTable invitees={invitees} onRevokeInvite={code => this.onRevokeInvite(code)} />
|
||||
) : (
|
||||
<UsersTable
|
||||
users={users}
|
||||
onRoleChange={(role, user) => this.onRoleChange(role, user)}
|
||||
onRemoveUser={user => this.onRemoveUser(user)}
|
||||
/>
|
||||
)}
|
||||
{hasFetched ? this.renderTable() : <PageLoader pageName="Users" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -121,6 +131,7 @@ function mapStateToProps(state) {
|
||||
searchQuery: getUsersSearchQuery(state.users),
|
||||
invitees: getInvitees(state.users),
|
||||
externalUserMngInfo: state.users.externalUserMngInfo,
|
||||
hasFetched: state.users.hasFetched,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
exports[`Render should render List page 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
@ -20,3 +20,22 @@ exports[`Render should render component 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div>
|
||||
<PageHeader
|
||||
model={Object {}}
|
||||
/>
|
||||
<div
|
||||
className="page-container page-body"
|
||||
>
|
||||
<Connect(UsersActionBar)
|
||||
onShowInvites={[Function]}
|
||||
showInvites={false}
|
||||
/>
|
||||
<PageLoader
|
||||
pageName="Users"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Invitee, OrgUser, UsersState } from 'app/types';
|
||||
import { Action, ActionTypes } from './actions';
|
||||
import config from '../../../core/config';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export const initialState: UsersState = {
|
||||
invitees: [] as Invitee[],
|
||||
@ -10,15 +10,16 @@ export const initialState: UsersState = {
|
||||
externalUserMngInfo: config.externalUserMngInfo,
|
||||
externalUserMngLinkName: config.externalUserMngLinkName,
|
||||
externalUserMngLinkUrl: config.externalUserMngLinkUrl,
|
||||
hasFetched: false,
|
||||
};
|
||||
|
||||
export const usersReducer = (state = initialState, action: Action): UsersState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LoadUsers:
|
||||
return { ...state, users: action.payload };
|
||||
return { ...state, hasFetched: true, users: action.payload };
|
||||
|
||||
case ActionTypes.LoadInvitees:
|
||||
return { ...state, invitees: action.payload };
|
||||
return { ...state, hasFetched: true, invitees: action.payload };
|
||||
|
||||
case ActionTypes.SetUsersSearchQuery:
|
||||
return { ...state, searchQuery: action.payload };
|
||||
|
@ -14,4 +14,5 @@ export interface NewApiKey {
|
||||
export interface ApiKeysState {
|
||||
keys: ApiKey[];
|
||||
searchQuery: string;
|
||||
hasFetched: boolean;
|
||||
}
|
||||
|
@ -25,4 +25,5 @@ export interface DataSourcesState {
|
||||
layoutMode: LayoutMode;
|
||||
dataSourcesCount: number;
|
||||
dataSourceTypes: Plugin[];
|
||||
hasFetched: boolean;
|
||||
}
|
||||
|
@ -44,4 +44,5 @@ export interface PluginsState {
|
||||
plugins: Plugin[];
|
||||
searchQuery: string;
|
||||
layoutMode: string;
|
||||
hasFetched: boolean;
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ export interface TeamGroup {
|
||||
export interface TeamsState {
|
||||
teams: Team[];
|
||||
searchQuery: string;
|
||||
hasFetched: boolean;
|
||||
}
|
||||
|
||||
export interface TeamState {
|
||||
|
@ -41,4 +41,5 @@ export interface UsersState {
|
||||
externalUserMngLinkUrl: string;
|
||||
externalUserMngLinkName: string;
|
||||
externalUserMngInfo: string;
|
||||
hasFetched: boolean;
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
export interface Invitee {
|
||||
code: string;
|
||||
createdOn: string;
|
||||
email: string;
|
||||
emailSent: boolean;
|
||||
emailSentOn: string;
|
||||
id: number;
|
||||
invitedByEmail: string;
|
||||
invitedByLogin: string;
|
||||
invitedByName: string;
|
||||
name: string;
|
||||
orgId: number;
|
||||
role: string;
|
||||
status: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
avatarUrl: string;
|
||||
email: string;
|
||||
lastSeenAt: string;
|
||||
lastSeenAtAge: string;
|
||||
login: string;
|
||||
orgId: number;
|
||||
role: string;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export interface UsersState {
|
||||
users: User[];
|
||||
invitees: Invitee[];
|
||||
searchQuery: string;
|
||||
canInvite: boolean;
|
||||
externalUserMngLinkUrl: string;
|
||||
externalUserMngLinkName: string;
|
||||
externalUserMngInfo: string;
|
||||
}
|
@ -95,7 +95,8 @@
|
||||
@import 'components/user-picker';
|
||||
@import 'components/description-picker';
|
||||
@import 'components/delete_button';
|
||||
@import 'components/_add_data_source.scss';
|
||||
@import 'components/add_data_source.scss';
|
||||
@import 'components/page_loader';
|
||||
|
||||
// PAGES
|
||||
@import 'pages/login';
|
||||
|
16
public/sass/components/_page_loader.scss
Normal file
16
public/sass/components/_page_loader.scss
Normal file
@ -0,0 +1,16 @@
|
||||
.page-loader-wrapper {
|
||||
padding-top: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
&__spinner {
|
||||
font-size: 32px;
|
||||
margin-bottom: $panel-margin;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user