diff --git a/public/app/core/components/PageLoader/PageLoader.tsx b/public/app/core/components/PageLoader/PageLoader.tsx new file mode 100644 index 00000000000..dcb67dde220 --- /dev/null +++ b/public/app/core/components/PageLoader/PageLoader.tsx @@ -0,0 +1,17 @@ +import React, { SFC } from 'react'; + +interface Props { + pageName: string; +} + +const PageLoader: SFC = ({ pageName }) => { + const loadingText = `Loading ${pageName}...`; + return ( +
+ +
{loadingText}
+
+ ); +}; + +export default PageLoader; diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx index 518180fc424..8bc6e9338fc 100644 --- a/public/app/features/api-keys/ApiKeysPage.test.tsx +++ b/public/app/features/api-keys/ApiKeysPage.test.tsx @@ -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(); diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 2f19250e835..6052b0f4fc8 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -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 { }); }; + renderTable() { + const { apiKeys } = this.props; + + return [ +

+ Existing Keys +

, + + + + + + + + {apiKeys.length > 0 && ( + + {apiKeys.map(key => { + return ( + + + + + + ); + })} + + )} +
NameRole +
{key.name}{key.role} + this.onDeleteApiKey(key)} className="btn btn-danger btn-mini"> + + +
, + ]; + } + render() { const { newApiKey, isAdding } = this.state; - const { navModel, apiKeys, searchQuery } = this.props; + const { hasFetched, navModel, searchQuery } = this.props; return (
@@ -170,34 +208,7 @@ export class ApiKeysPage extends PureComponent {
- -

Existing Keys

- - - - - - - - {apiKeys.length > 0 ? ( - - {apiKeys.map(key => { - return ( - - - - - - ); - })} - - ) : null} -
NameRole -
{key.name}{key.role} - this.onDeleteApiKey(key)} className="btn btn-danger btn-mini"> - - -
+ {hasFetched ? this.renderTable() : } ); @@ -209,6 +220,7 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, 'apikeys'), apiKeys: getApiKeys(state.apiKeys), searchQuery: state.apiKeys.searchQuery, + hasFetched: state.apiKeys.hasFetched, }; } diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index 77c7f620173..b1cac8469be 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -138,11 +138,13 @@ exports[`Render should render API keys table 1`] = `

Existing Keys

@@ -404,32 +406,9 @@ exports[`Render should render component 1`] = ` -

- Existing Keys -

-
- - - - - - -
- Name - - Role - -
+ `; diff --git a/public/app/features/api-keys/state/reducers.ts b/public/app/features/api-keys/state/reducers.ts index a21aa55dbf7..57849b20d4f 100644 --- a/public/app/features/api-keys/state/reducers.ts +++ b/public/app/features/api-keys/state/reducers.ts @@ -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 }; } diff --git a/public/app/features/api-keys/state/selectors.test.ts b/public/app/features/api-keys/state/selectors.test.ts index 7d8f3122ce6..5e9ba51462f 100644 --- a/public/app/features/api-keys/state/selectors.test.ts +++ b/public/app/features/api-keys/state/selectors.test.ts @@ -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); diff --git a/public/app/features/datasources/DataSourcesListPage.test.tsx b/public/app/features/datasources/DataSourcesListPage.test.tsx index 96f6c304b16..0ea716d62c9 100644 --- a/public/app/features/datasources/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/DataSourcesListPage.test.tsx @@ -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(); diff --git a/public/app/features/datasources/DataSourcesListPage.tsx b/public/app/features/datasources/DataSourcesListPage.tsx index a5887973a6b..6a292d63e53 100644 --- a/public/app/features/datasources/DataSourcesListPage.tsx +++ b/public/app/features/datasources/DataSourcesListPage.tsx @@ -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 { searchQuery, setDataSourcesSearchQuery, setDataSourcesLayoutMode, + hasFetched, } = this.props; const linkButton = { @@ -67,10 +70,10 @@ export class DataSourcesListPage extends PureComponent {
- {dataSourcesCount === 0 ? ( - - ) : ( - [ + {!hasFetched && } + {hasFetched && dataSourcesCount === 0 && } + {hasFetched && + dataSourcesCount > 0 && [ { key="action-bar" />, , - ] - )} + ]}
); @@ -95,6 +97,7 @@ function mapStateToProps(state) { layoutMode: getDataSourcesLayoutMode(state.dataSources), dataSourcesCount: getDataSourcesCount(state.dataSources), searchQuery: getDataSourcesSearchQuery(state.dataSources), + hasFetched: state.dataSources.hasFetched, }; } diff --git a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap index 3f9dbab72ab..c26ac50fed8 100644 --- a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap +++ b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap @@ -155,19 +155,8 @@ exports[`Render should render component 1`] = `
-
diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index acb228d3ed6..9b84799dcea 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -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 }; diff --git a/public/app/features/plugins/PluginListPage.test.tsx b/public/app/features/plugins/PluginListPage.test.tsx index b173ef51a2a..31b2f128436 100644 --- a/public/app/features/plugins/PluginListPage.test.tsx +++ b/public/app/features/plugins/PluginListPage.test.tsx @@ -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(); - const instance = wrapper.instance() as PluginListPage; - - return { - wrapper, - instance, - }; + return shallow(); }; 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(); }); diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx index d654ebd7cff..a2fcb90ce54 100644 --- a/public/app/features/plugins/PluginListPage.tsx +++ b/public/app/features/plugins/PluginListPage.tsx @@ -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 { } 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 (
@@ -47,7 +58,11 @@ export class PluginListPage extends PureComponent { setSearchQuery={query => setPluginsSearchQuery(query)} linkButton={linkButton} /> - {plugins && } + {hasFetched ? ( + plugins && + ) : ( + + )}
); @@ -60,6 +75,7 @@ function mapStateToProps(state) { plugins: getPlugins(state.plugins), layoutMode: getLayoutMode(state.plugins), searchQuery: getPluginsSearchQuery(state.plugins), + hasFetched: state.plugins.hasFetched, }; } diff --git a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap index 43d9f45883d..ad27dd5037c 100644 --- a/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap +++ b/public/app/features/plugins/__snapshots__/PluginListPage.test.tsx.snap @@ -1,6 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render component 1`] = ` +
+ +
+ + +
+
+`; + +exports[`Render should render list 1`] = `
{ 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 }; diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 6f84ca920d0..f6e1c11c9f9 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -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(); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 7b153746f9f..d8e12e338e9 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -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 { ); } + 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 (
- {teamsCount > 0 ? this.renderTeamList() : this.renderEmptyList()} + {hasFetched ? this.renderList() : }
); } @@ -143,6 +155,7 @@ function mapStateToProps(state) { teams: getTeams(state.teams), searchQuery: getSearchQuery(state.teams), teamsCount: getTeamsCount(state.teams), + hasFetched: state.teams.hasFetched, }; } diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index 7cf5951dba3..73f081d496a 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -5,24 +5,9 @@ exports[`Render should render component 1`] = ` -
- -
+
`; diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 8b76028b9cb..2e72dce0afb 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -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 }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index 5f338069bbb..3764a9355c6 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -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); diff --git a/public/app/features/users/UsersListPage.test.tsx b/public/app/features/users/UsersListPage.test.tsx index 6f59df36b55..b6cd85182ee 100644 --- a/public/app/features/users/UsersListPage.test.tsx +++ b/public/app/features/users/UsersListPage.test.tsx @@ -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', () => { diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx index 8ca65221f4c..234fc6cc1d1 100644 --- a/public/app/features/users/UsersListPage.tsx +++ b/public/app/features/users/UsersListPage.tsx @@ -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 { })); }; + renderTable() { + const { invitees, users } = this.props; + + if (this.state.showInvites) { + return this.onRevokeInvite(code)} />; + } else { + return ( + 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 { {externalUserMngInfoHtml && (
)} - {this.state.showInvites ? ( - this.onRevokeInvite(code)} /> - ) : ( - this.onRoleChange(role, user)} - onRemoveUser={user => this.onRemoveUser(user)} - /> - )} + {hasFetched ? this.renderTable() : }
); @@ -121,6 +131,7 @@ function mapStateToProps(state) { searchQuery: getUsersSearchQuery(state.users), invitees: getInvitees(state.users), externalUserMngInfo: state.users.externalUserMngInfo, + hasFetched: state.users.hasFetched, }; } diff --git a/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap index b3432491ea2..429322eac98 100644 --- a/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap +++ b/public/app/features/users/__snapshots__/UsersListPage.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Render should render component 1`] = ` +exports[`Render should render List page 1`] = `
`; + +exports[`Render should render component 1`] = ` +
+ +
+ + +
+
+`; diff --git a/public/app/features/users/state/reducers.ts b/public/app/features/users/state/reducers.ts index 9bbfdd5595f..d31682e0c45 100644 --- a/public/app/features/users/state/reducers.ts +++ b/public/app/features/users/state/reducers.ts @@ -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 }; diff --git a/public/app/types/apiKeys.ts b/public/app/types/apiKeys.ts index 6288f5165ad..6cf92011c69 100644 --- a/public/app/types/apiKeys.ts +++ b/public/app/types/apiKeys.ts @@ -14,4 +14,5 @@ export interface NewApiKey { export interface ApiKeysState { keys: ApiKey[]; searchQuery: string; + hasFetched: boolean; } diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index 4d8d755f106..0614cd2da62 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -25,4 +25,5 @@ export interface DataSourcesState { layoutMode: LayoutMode; dataSourcesCount: number; dataSourceTypes: Plugin[]; + hasFetched: boolean; } diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 92bebfef8d4..c7e9aa7a564 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -44,4 +44,5 @@ export interface PluginsState { plugins: Plugin[]; searchQuery: string; layoutMode: string; + hasFetched: boolean; } diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index b85ff3833d6..a524f0dcf87 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -23,6 +23,7 @@ export interface TeamGroup { export interface TeamsState { teams: Team[]; searchQuery: string; + hasFetched: boolean; } export interface TeamState { diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 46ecdb36035..c0b7b135ff8 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -41,4 +41,5 @@ export interface UsersState { externalUserMngLinkUrl: string; externalUserMngLinkName: string; externalUserMngInfo: string; + hasFetched: boolean; } diff --git a/public/app/types/users.ts b/public/app/types/users.ts deleted file mode 100644 index 108d6477ee0..00000000000 --- a/public/app/types/users.ts +++ /dev/null @@ -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; -} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index b9b1527e2cc..f583e481490 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -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'; diff --git a/public/sass/components/_page_loader.scss b/public/sass/components/_page_loader.scss new file mode 100644 index 00000000000..053a061600f --- /dev/null +++ b/public/sass/components/_page_loader.scss @@ -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; + } +}