Commit 02e7d713 by Peter Holmberg

Added Loading state on org pages

parent 974eddee
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) => { ...@@ -9,6 +9,7 @@ const setup = (propOverrides?: object) => {
navModel: {} as NavModel, navModel: {} as NavModel,
apiKeys: [] as ApiKey[], apiKeys: [] as ApiKey[],
searchQuery: '', searchQuery: '',
hasFetched: false,
loadApiKeys: jest.fn(), loadApiKeys: jest.fn(),
deleteApiKey: jest.fn(), deleteApiKey: jest.fn(),
setSearchQuery: jest.fn(), setSearchQuery: jest.fn(),
...@@ -35,6 +36,7 @@ describe('Render', () => { ...@@ -35,6 +36,7 @@ describe('Render', () => {
it('should render API keys table', () => { it('should render API keys table', () => {
const { wrapper } = setup({ const { wrapper } = setup({
apiKeys: getMultipleMockKeys(5), apiKeys: getMultipleMockKeys(5),
hasFetched: true,
}); });
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
......
...@@ -8,6 +8,7 @@ import { getApiKeys } from './state/selectors'; ...@@ -8,6 +8,7 @@ import { getApiKeys } from './state/selectors';
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
import PageHeader from 'app/core/components/PageHeader/PageHeader'; import PageHeader from 'app/core/components/PageHeader/PageHeader';
import SlideDown from 'app/core/components/Animations/SlideDown'; import SlideDown from 'app/core/components/Animations/SlideDown';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import ApiKeysAddedModal from './ApiKeysAddedModal'; import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config'; import config from 'app/core/config';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
...@@ -16,6 +17,7 @@ export interface Props { ...@@ -16,6 +17,7 @@ export interface Props {
navModel: NavModel; navModel: NavModel;
apiKeys: ApiKey[]; apiKeys: ApiKey[];
searchQuery: string; searchQuery: string;
hasFetched: boolean;
loadApiKeys: typeof loadApiKeys; loadApiKeys: typeof loadApiKeys;
deleteApiKey: typeof deleteApiKey; deleteApiKey: typeof deleteApiKey;
setSearchQuery: typeof setSearchQuery; setSearchQuery: typeof setSearchQuery;
...@@ -99,9 +101,45 @@ export class ApiKeysPage extends PureComponent<Props, any> { ...@@ -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() { render() {
const { newApiKey, isAdding } = this.state; const { newApiKey, isAdding } = this.state;
const { navModel, apiKeys, searchQuery } = this.props; const { hasFetched, navModel, searchQuery } = this.props;
return ( return (
<div> <div>
...@@ -170,34 +208,7 @@ export class ApiKeysPage extends PureComponent<Props, any> { ...@@ -170,34 +208,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
</form> </form>
</div> </div>
</SlideDown> </SlideDown>
{hasFetched ? this.renderTable() : <PageLoader pageName="Api keys" />}
<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>
</div> </div>
</div> </div>
); );
...@@ -209,6 +220,7 @@ function mapStateToProps(state) { ...@@ -209,6 +220,7 @@ function mapStateToProps(state) {
navModel: getNavModel(state.navIndex, 'apikeys'), navModel: getNavModel(state.navIndex, 'apikeys'),
apiKeys: getApiKeys(state.apiKeys), apiKeys: getApiKeys(state.apiKeys),
searchQuery: state.apiKeys.searchQuery, searchQuery: state.apiKeys.searchQuery,
hasFetched: state.apiKeys.hasFetched,
}; };
} }
......
...@@ -138,11 +138,13 @@ exports[`Render should render API keys table 1`] = ` ...@@ -138,11 +138,13 @@ exports[`Render should render API keys table 1`] = `
</Component> </Component>
<h3 <h3
className="page-heading" className="page-heading"
key="header"
> >
Existing Keys Existing Keys
</h3> </h3>
<table <table
className="filter-table" className="filter-table"
key="table"
> >
<thead> <thead>
<tr> <tr>
...@@ -404,32 +406,9 @@ exports[`Render should render component 1`] = ` ...@@ -404,32 +406,9 @@ exports[`Render should render component 1`] = `
</form> </form>
</div> </div>
</Component> </Component>
<h3 <PageLoader
className="page-heading" pageName="Api keys"
> />
Existing Keys
</h3>
<table
className="filter-table"
>
<thead>
<tr>
<th>
Name
</th>
<th>
Role
</th>
<th
style={
Object {
"width": "34px",
}
}
/>
</tr>
</thead>
</table>
</div> </div>
</div> </div>
`; `;
...@@ -4,12 +4,13 @@ import { Action, ActionTypes } from './actions'; ...@@ -4,12 +4,13 @@ import { Action, ActionTypes } from './actions';
export const initialApiKeysState: ApiKeysState = { export const initialApiKeysState: ApiKeysState = {
keys: [], keys: [],
searchQuery: '', searchQuery: '',
hasFetched: false,
}; };
export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => { export const apiKeysReducer = (state = initialApiKeysState, action: Action): ApiKeysState => {
switch (action.type) { switch (action.type) {
case ActionTypes.LoadApiKeys: case ActionTypes.LoadApiKeys:
return { ...state, keys: action.payload }; return { ...state, hasFetched: true, keys: action.payload };
case ActionTypes.SetApiKeysSearchQuery: case ActionTypes.SetApiKeysSearchQuery:
return { ...state, searchQuery: action.payload }; return { ...state, searchQuery: action.payload };
} }
......
...@@ -7,7 +7,7 @@ describe('API Keys selectors', () => { ...@@ -7,7 +7,7 @@ describe('API Keys selectors', () => {
const mockKeys = getMultipleMockKeys(5); const mockKeys = getMultipleMockKeys(5);
it('should return all keys if no search query', () => { 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); const keys = getApiKeys(mockState);
...@@ -15,7 +15,7 @@ describe('API Keys selectors', () => { ...@@ -15,7 +15,7 @@ describe('API Keys selectors', () => {
}); });
it('should filter keys if search query exists', () => { 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); const keys = getApiKeys(mockState);
......
...@@ -15,6 +15,7 @@ const setup = (propOverrides?: object) => { ...@@ -15,6 +15,7 @@ const setup = (propOverrides?: object) => {
searchQuery: '', searchQuery: '',
setDataSourcesSearchQuery: jest.fn(), setDataSourcesSearchQuery: jest.fn(),
setDataSourcesLayoutMode: jest.fn(), setDataSourcesLayoutMode: jest.fn(),
hasFetched: false,
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
...@@ -33,6 +34,7 @@ describe('Render', () => { ...@@ -33,6 +34,7 @@ describe('Render', () => {
const wrapper = setup({ const wrapper = setup({
dataSources: getMockDataSources(5), dataSources: getMockDataSources(5),
dataSourcesCount: 5, dataSourcesCount: 5,
hasFetched: true,
}); });
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
......
...@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; ...@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import PageHeader from '../../core/components/PageHeader/PageHeader'; import PageHeader from '../../core/components/PageHeader/PageHeader';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import OrgActionBar from '../../core/components/OrgActionBar/OrgActionBar'; import OrgActionBar from '../../core/components/OrgActionBar/OrgActionBar';
import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA';
import DataSourcesList from './DataSourcesList'; import DataSourcesList from './DataSourcesList';
...@@ -22,6 +23,7 @@ export interface Props { ...@@ -22,6 +23,7 @@ export interface Props {
dataSourcesCount: number; dataSourcesCount: number;
layoutMode: LayoutMode; layoutMode: LayoutMode;
searchQuery: string; searchQuery: string;
hasFetched: boolean;
loadDataSources: typeof loadDataSources; loadDataSources: typeof loadDataSources;
setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode; setDataSourcesLayoutMode: typeof setDataSourcesLayoutMode;
setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery; setDataSourcesSearchQuery: typeof setDataSourcesSearchQuery;
...@@ -56,6 +58,7 @@ export class DataSourcesListPage extends PureComponent<Props> { ...@@ -56,6 +58,7 @@ export class DataSourcesListPage extends PureComponent<Props> {
searchQuery, searchQuery,
setDataSourcesSearchQuery, setDataSourcesSearchQuery,
setDataSourcesLayoutMode, setDataSourcesLayoutMode,
hasFetched,
} = this.props; } = this.props;
const linkButton = { const linkButton = {
...@@ -67,10 +70,10 @@ export class DataSourcesListPage extends PureComponent<Props> { ...@@ -67,10 +70,10 @@ export class DataSourcesListPage extends PureComponent<Props> {
<div> <div>
<PageHeader model={navModel} /> <PageHeader model={navModel} />
<div className="page-container page-body"> <div className="page-container page-body">
{dataSourcesCount === 0 ? ( {!hasFetched && <PageLoader pageName="Data sources" />}
<EmptyListCTA model={emptyListModel} /> {hasFetched && dataSourcesCount === 0 && <EmptyListCTA model={emptyListModel} />}
) : ( {hasFetched &&
[ dataSourcesCount > 0 && [
<OrgActionBar <OrgActionBar
layoutMode={layoutMode} layoutMode={layoutMode}
searchQuery={searchQuery} searchQuery={searchQuery}
...@@ -80,8 +83,7 @@ export class DataSourcesListPage extends PureComponent<Props> { ...@@ -80,8 +83,7 @@ export class DataSourcesListPage extends PureComponent<Props> {
key="action-bar" key="action-bar"
/>, />,
<DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />, <DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
] ]}
)}
</div> </div>
</div> </div>
); );
...@@ -95,6 +97,7 @@ function mapStateToProps(state) { ...@@ -95,6 +97,7 @@ function mapStateToProps(state) {
layoutMode: getDataSourcesLayoutMode(state.dataSources), layoutMode: getDataSourcesLayoutMode(state.dataSources),
dataSourcesCount: getDataSourcesCount(state.dataSources), dataSourcesCount: getDataSourcesCount(state.dataSources),
searchQuery: getDataSourcesSearchQuery(state.dataSources), searchQuery: getDataSourcesSearchQuery(state.dataSources),
hasFetched: state.dataSources.hasFetched,
}; };
} }
......
...@@ -155,19 +155,8 @@ exports[`Render should render component 1`] = ` ...@@ -155,19 +155,8 @@ exports[`Render should render component 1`] = `
<div <div
className="page-container page-body" className="page-container page-body"
> >
<EmptyListCTA <PageLoader
model={ pageName="Data sources"
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",
}
}
/> />
</div> </div>
</div> </div>
......
...@@ -9,12 +9,13 @@ const initialState: DataSourcesState = { ...@@ -9,12 +9,13 @@ const initialState: DataSourcesState = {
dataSourcesCount: 0, dataSourcesCount: 0,
dataSourceTypes: [] as Plugin[], dataSourceTypes: [] as Plugin[],
dataSourceTypeSearchQuery: '', dataSourceTypeSearchQuery: '',
hasFetched: false,
}; };
export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => { export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => {
switch (action.type) { switch (action.type) {
case ActionTypes.LoadDataSources: 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: case ActionTypes.SetDataSourcesSearchQuery:
return { ...state, searchQuery: action.payload }; return { ...state, searchQuery: action.payload };
......
...@@ -13,22 +13,25 @@ const setup = (propOverrides?: object) => { ...@@ -13,22 +13,25 @@ const setup = (propOverrides?: object) => {
setPluginsLayoutMode: jest.fn(), setPluginsLayoutMode: jest.fn(),
layoutMode: LayoutModes.Grid, layoutMode: LayoutModes.Grid,
loadPlugins: jest.fn(), loadPlugins: jest.fn(),
hasFetched: false,
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
const wrapper = shallow(<PluginListPage {...props} />); return shallow(<PluginListPage {...props} />);
const instance = wrapper.instance() as PluginListPage;
return {
wrapper,
instance,
};
}; };
describe('Render', () => { describe('Render', () => {
it('should render component', () => { 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(); expect(wrapper).toMatchSnapshot();
}); });
......
...@@ -3,6 +3,7 @@ import { hot } from 'react-hot-loader'; ...@@ -3,6 +3,7 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PageHeader from 'app/core/components/PageHeader/PageHeader'; import PageHeader from 'app/core/components/PageHeader/PageHeader';
import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar'; import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import PluginList from './PluginList'; import PluginList from './PluginList';
import { NavModel, Plugin } from 'app/types'; import { NavModel, Plugin } from 'app/types';
import { loadPlugins, setPluginsLayoutMode, setPluginsSearchQuery } from './state/actions'; import { loadPlugins, setPluginsLayoutMode, setPluginsSearchQuery } from './state/actions';
...@@ -15,6 +16,7 @@ export interface Props { ...@@ -15,6 +16,7 @@ export interface Props {
plugins: Plugin[]; plugins: Plugin[];
layoutMode: LayoutMode; layoutMode: LayoutMode;
searchQuery: string; searchQuery: string;
hasFetched: boolean;
loadPlugins: typeof loadPlugins; loadPlugins: typeof loadPlugins;
setPluginsLayoutMode: typeof setPluginsLayoutMode; setPluginsLayoutMode: typeof setPluginsLayoutMode;
setPluginsSearchQuery: typeof setPluginsSearchQuery; setPluginsSearchQuery: typeof setPluginsSearchQuery;
...@@ -30,12 +32,21 @@ export class PluginListPage extends PureComponent<Props> { ...@@ -30,12 +32,21 @@ export class PluginListPage extends PureComponent<Props> {
} }
render() { render() {
const { navModel, plugins, layoutMode, setPluginsLayoutMode, setPluginsSearchQuery, searchQuery } = this.props; const {
hasFetched,
navModel,
plugins,
layoutMode,
setPluginsLayoutMode,
setPluginsSearchQuery,
searchQuery,
} = this.props;
const linkButton = { const linkButton = {
href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list', href: 'https://grafana.com/plugins?utm_source=grafana_plugin_list',
title: 'Find more plugins on Grafana.com', title: 'Find more plugins on Grafana.com',
}; };
return ( return (
<div> <div>
<PageHeader model={navModel} /> <PageHeader model={navModel} />
...@@ -47,7 +58,11 @@ export class PluginListPage extends PureComponent<Props> { ...@@ -47,7 +58,11 @@ export class PluginListPage extends PureComponent<Props> {
setSearchQuery={query => setPluginsSearchQuery(query)} setSearchQuery={query => setPluginsSearchQuery(query)}
linkButton={linkButton} linkButton={linkButton}
/> />
{plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />} {hasFetched ? (
plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />
) : (
<PageLoader pageName="Plugins" />
)}
</div> </div>
</div> </div>
); );
...@@ -60,6 +75,7 @@ function mapStateToProps(state) { ...@@ -60,6 +75,7 @@ function mapStateToProps(state) {
plugins: getPlugins(state.plugins), plugins: getPlugins(state.plugins),
layoutMode: getLayoutMode(state.plugins), layoutMode: getLayoutMode(state.plugins),
searchQuery: getPluginsSearchQuery(state.plugins), searchQuery: getPluginsSearchQuery(state.plugins),
hasFetched: state.plugins.hasFetched,
}; };
} }
......
...@@ -20,6 +20,33 @@ exports[`Render should render component 1`] = ` ...@@ -20,6 +20,33 @@ exports[`Render should render component 1`] = `
searchQuery="" searchQuery=""
setSearchQuery={[Function]} setSearchQuery={[Function]}
/> />
<PageLoader
pageName="Plugins"
/>
</div>
</div>
`;
exports[`Render should render list 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]}
/>
<PluginList <PluginList
layoutMode="grid" layoutMode="grid"
plugins={Array []} plugins={Array []}
......
...@@ -6,12 +6,13 @@ export const initialState: PluginsState = { ...@@ -6,12 +6,13 @@ export const initialState: PluginsState = {
plugins: [] as Plugin[], plugins: [] as Plugin[],
searchQuery: '', searchQuery: '',
layoutMode: LayoutModes.Grid, layoutMode: LayoutModes.Grid,
hasFetched: false,
}; };
export const pluginsReducer = (state = initialState, action: Action): PluginsState => { export const pluginsReducer = (state = initialState, action: Action): PluginsState => {
switch (action.type) { switch (action.type) {
case ActionTypes.LoadPlugins: case ActionTypes.LoadPlugins:
return { ...state, plugins: action.payload }; return { ...state, hasFetched: true, plugins: action.payload };
case ActionTypes.SetPluginsSearchQuery: case ActionTypes.SetPluginsSearchQuery:
return { ...state, searchQuery: action.payload }; return { ...state, searchQuery: action.payload };
......
...@@ -13,6 +13,7 @@ const setup = (propOverrides?: object) => { ...@@ -13,6 +13,7 @@ const setup = (propOverrides?: object) => {
setSearchQuery: jest.fn(), setSearchQuery: jest.fn(),
searchQuery: '', searchQuery: '',
teamsCount: 0, teamsCount: 0,
hasFetched: false,
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
...@@ -36,6 +37,7 @@ describe('Render', () => { ...@@ -36,6 +37,7 @@ describe('Render', () => {
const { wrapper } = setup({ const { wrapper } = setup({
teams: getMultipleMockTeams(5), teams: getMultipleMockTeams(5),
teamsCount: 5, teamsCount: 5,
hasFetched: true,
}); });
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
......
...@@ -4,6 +4,7 @@ import { hot } from 'react-hot-loader'; ...@@ -4,6 +4,7 @@ import { hot } from 'react-hot-loader';
import PageHeader from 'app/core/components/PageHeader/PageHeader'; import PageHeader from 'app/core/components/PageHeader/PageHeader';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { NavModel, Team } from '../../types'; import { NavModel, Team } from '../../types';
import { loadTeams, deleteTeam, setSearchQuery } from './state/actions'; import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors'; import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
...@@ -14,6 +15,7 @@ export interface Props { ...@@ -14,6 +15,7 @@ export interface Props {
teams: Team[]; teams: Team[];
searchQuery: string; searchQuery: string;
teamsCount: number; teamsCount: number;
hasFetched: boolean;
loadTeams: typeof loadTeams; loadTeams: typeof loadTeams;
deleteTeam: typeof deleteTeam; deleteTeam: typeof deleteTeam;
setSearchQuery: typeof setSearchQuery; setSearchQuery: typeof setSearchQuery;
...@@ -125,13 +127,23 @@ export class TeamList extends PureComponent<Props, any> { ...@@ -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() { render() {
const { navModel, teamsCount } = this.props; const { hasFetched, navModel } = this.props;
return ( return (
<div> <div>
<PageHeader model={navModel} /> <PageHeader model={navModel} />
{teamsCount > 0 ? this.renderTeamList() : this.renderEmptyList()} {hasFetched ? this.renderList() : <PageLoader pageName="Teams" />}
</div> </div>
); );
} }
...@@ -143,6 +155,7 @@ function mapStateToProps(state) { ...@@ -143,6 +155,7 @@ function mapStateToProps(state) {
teams: getTeams(state.teams), teams: getTeams(state.teams),
searchQuery: getSearchQuery(state.teams), searchQuery: getSearchQuery(state.teams),
teamsCount: getTeamsCount(state.teams), teamsCount: getTeamsCount(state.teams),
hasFetched: state.teams.hasFetched,
}; };
} }
......
...@@ -5,24 +5,9 @@ exports[`Render should render component 1`] = ` ...@@ -5,24 +5,9 @@ exports[`Render should render component 1`] = `
<PageHeader <PageHeader
model={Object {}} model={Object {}}
/> />
<div <PageLoader
className="page-container page-body" pageName="Teams"
> />
<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>
</div> </div>
`; `;
......
import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types'; import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from 'app/types';
import { Action, ActionTypes } from './actions'; import { Action, ActionTypes } from './actions';
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; export const initialTeamsState: TeamsState = { teams: [], searchQuery: '', hasFetched: false };
export const initialTeamState: TeamState = { export const initialTeamState: TeamState = {
team: {} as Team, team: {} as Team,
members: [] as TeamMember[], members: [] as TeamMember[],
...@@ -12,7 +12,7 @@ export const initialTeamState: TeamState = { ...@@ -12,7 +12,7 @@ export const initialTeamState: TeamState = {
export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => {
switch (action.type) { switch (action.type) {
case ActionTypes.LoadTeams: case ActionTypes.LoadTeams:
return { ...state, teams: action.payload }; return { ...state, hasFetched: true, teams: action.payload };
case ActionTypes.SetSearchQuery: case ActionTypes.SetSearchQuery:
return { ...state, searchQuery: action.payload }; return { ...state, searchQuery: action.payload };
......
...@@ -7,7 +7,7 @@ describe('Teams selectors', () => { ...@@ -7,7 +7,7 @@ describe('Teams selectors', () => {
const mockTeams = getMultipleMockTeams(5); const mockTeams = getMultipleMockTeams(5);
it('should return teams if no search query', () => { 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); const teams = getTeams(mockState);
...@@ -15,7 +15,7 @@ describe('Teams selectors', () => { ...@@ -15,7 +15,7 @@ describe('Teams selectors', () => {
}); });
it('Should filter teams if search query', () => { 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); const teams = getTeams(mockState);
......
...@@ -22,6 +22,7 @@ const setup = (propOverrides?: object) => { ...@@ -22,6 +22,7 @@ const setup = (propOverrides?: object) => {
updateUser: jest.fn(), updateUser: jest.fn(),
removeUser: jest.fn(), removeUser: jest.fn(),
setUsersSearchQuery: jest.fn(), setUsersSearchQuery: jest.fn(),
hasFetched: false,
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
...@@ -41,6 +42,14 @@ describe('Render', () => { ...@@ -41,6 +42,14 @@ describe('Render', () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it('should render List page', () => {
const { wrapper } = setup({
hasFetched: true,
});
expect(wrapper).toMatchSnapshot();
});
}); });
describe('Functions', () => { describe('Functions', () => {
......
...@@ -3,8 +3,9 @@ import { hot } from 'react-hot-loader'; ...@@ -3,8 +3,9 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Remarkable from 'remarkable'; import Remarkable from 'remarkable';
import PageHeader from 'app/core/components/PageHeader/PageHeader'; import PageHeader from 'app/core/components/PageHeader/PageHeader';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import UsersActionBar from './UsersActionBar'; import UsersActionBar from './UsersActionBar';
import UsersTable from 'app/features/users/UsersTable'; import UsersTable from './UsersTable';
import InviteesTable from './InviteesTable'; import InviteesTable from './InviteesTable';
import { Invitee, NavModel, OrgUser } from 'app/types'; import { Invitee, NavModel, OrgUser } from 'app/types';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
...@@ -18,6 +19,7 @@ export interface Props { ...@@ -18,6 +19,7 @@ export interface Props {
users: OrgUser[]; users: OrgUser[];
searchQuery: string; searchQuery: string;
externalUserMngInfo: string; externalUserMngInfo: string;
hasFetched: boolean;
loadUsers: typeof loadUsers; loadUsers: typeof loadUsers;
loadInvitees: typeof loadInvitees; loadInvitees: typeof loadInvitees;
setUsersSearchQuery: typeof setUsersSearchQuery; setUsersSearchQuery: typeof setUsersSearchQuery;
...@@ -87,8 +89,24 @@ export class UsersListPage extends PureComponent<Props, State> { ...@@ -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() { render() {
const { invitees, navModel, users } = this.props; const { navModel, hasFetched } = this.props;
const externalUserMngInfoHtml = this.externalUserMngInfoHtml; const externalUserMngInfoHtml = this.externalUserMngInfoHtml;
return ( return (
...@@ -99,15 +117,7 @@ export class UsersListPage extends PureComponent<Props, State> { ...@@ -99,15 +117,7 @@ export class UsersListPage extends PureComponent<Props, State> {
{externalUserMngInfoHtml && ( {externalUserMngInfoHtml && (
<div className="grafana-info-box" dangerouslySetInnerHTML={{ __html: externalUserMngInfoHtml }} /> <div className="grafana-info-box" dangerouslySetInnerHTML={{ __html: externalUserMngInfoHtml }} />
)} )}
{this.state.showInvites ? ( {hasFetched ? this.renderTable() : <PageLoader pageName="Users" />}
<InviteesTable invitees={invitees} onRevokeInvite={code => this.onRevokeInvite(code)} />
) : (
<UsersTable
users={users}
onRoleChange={(role, user) => this.onRoleChange(role, user)}
onRemoveUser={user => this.onRemoveUser(user)}
/>
)}
</div> </div>
</div> </div>
); );
...@@ -121,6 +131,7 @@ function mapStateToProps(state) { ...@@ -121,6 +131,7 @@ function mapStateToProps(state) {
searchQuery: getUsersSearchQuery(state.users), searchQuery: getUsersSearchQuery(state.users),
invitees: getInvitees(state.users), invitees: getInvitees(state.users),
externalUserMngInfo: state.users.externalUserMngInfo, externalUserMngInfo: state.users.externalUserMngInfo,
hasFetched: state.users.hasFetched,
}; };
} }
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = ` exports[`Render should render List page 1`] = `
<div> <div>
<PageHeader <PageHeader
model={Object {}} model={Object {}}
...@@ -20,3 +20,22 @@ exports[`Render should render component 1`] = ` ...@@ -20,3 +20,22 @@ exports[`Render should render component 1`] = `
</div> </div>
</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>
`;
import { Invitee, OrgUser, UsersState } from 'app/types'; import { Invitee, OrgUser, UsersState } from 'app/types';
import { Action, ActionTypes } from './actions'; import { Action, ActionTypes } from './actions';
import config from '../../../core/config'; import config from 'app/core/config';
export const initialState: UsersState = { export const initialState: UsersState = {
invitees: [] as Invitee[], invitees: [] as Invitee[],
...@@ -10,15 +10,16 @@ export const initialState: UsersState = { ...@@ -10,15 +10,16 @@ export const initialState: UsersState = {
externalUserMngInfo: config.externalUserMngInfo, externalUserMngInfo: config.externalUserMngInfo,
externalUserMngLinkName: config.externalUserMngLinkName, externalUserMngLinkName: config.externalUserMngLinkName,
externalUserMngLinkUrl: config.externalUserMngLinkUrl, externalUserMngLinkUrl: config.externalUserMngLinkUrl,
hasFetched: false,
}; };
export const usersReducer = (state = initialState, action: Action): UsersState => { export const usersReducer = (state = initialState, action: Action): UsersState => {
switch (action.type) { switch (action.type) {
case ActionTypes.LoadUsers: case ActionTypes.LoadUsers:
return { ...state, users: action.payload }; return { ...state, hasFetched: true, users: action.payload };
case ActionTypes.LoadInvitees: case ActionTypes.LoadInvitees:
return { ...state, invitees: action.payload }; return { ...state, hasFetched: true, invitees: action.payload };
case ActionTypes.SetUsersSearchQuery: case ActionTypes.SetUsersSearchQuery:
return { ...state, searchQuery: action.payload }; return { ...state, searchQuery: action.payload };
......
...@@ -14,4 +14,5 @@ export interface NewApiKey { ...@@ -14,4 +14,5 @@ export interface NewApiKey {
export interface ApiKeysState { export interface ApiKeysState {
keys: ApiKey[]; keys: ApiKey[];
searchQuery: string; searchQuery: string;
hasFetched: boolean;
} }
...@@ -25,4 +25,5 @@ export interface DataSourcesState { ...@@ -25,4 +25,5 @@ export interface DataSourcesState {
layoutMode: LayoutMode; layoutMode: LayoutMode;
dataSourcesCount: number; dataSourcesCount: number;
dataSourceTypes: Plugin[]; dataSourceTypes: Plugin[];
hasFetched: boolean;
} }
...@@ -44,4 +44,5 @@ export interface PluginsState { ...@@ -44,4 +44,5 @@ export interface PluginsState {
plugins: Plugin[]; plugins: Plugin[];
searchQuery: string; searchQuery: string;
layoutMode: string; layoutMode: string;
hasFetched: boolean;
} }
...@@ -23,6 +23,7 @@ export interface TeamGroup { ...@@ -23,6 +23,7 @@ export interface TeamGroup {
export interface TeamsState { export interface TeamsState {
teams: Team[]; teams: Team[];
searchQuery: string; searchQuery: string;
hasFetched: boolean;
} }
export interface TeamState { export interface TeamState {
......
...@@ -41,4 +41,5 @@ export interface UsersState { ...@@ -41,4 +41,5 @@ export interface UsersState {
externalUserMngLinkUrl: string; externalUserMngLinkUrl: string;
externalUserMngLinkName: string; externalUserMngLinkName: string;
externalUserMngInfo: string; externalUserMngInfo: string;
hasFetched: boolean;
} }
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 @@ ...@@ -95,7 +95,8 @@
@import 'components/user-picker'; @import 'components/user-picker';
@import 'components/description-picker'; @import 'components/description-picker';
@import 'components/delete_button'; @import 'components/delete_button';
@import 'components/_add_data_source.scss'; @import 'components/add_data_source.scss';
@import 'components/page_loader';
// PAGES // PAGES
@import 'pages/login'; @import 'pages/login';
......
.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;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment