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) => {
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",
}
}
<PageLoader
pageName="Api keys"
/>
</tr>
</thead>
</table>
</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,
};
}
......
......@@ -20,6 +20,33 @@ exports[`Render should render component 1`] = `
searchQuery=""
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
layoutMode="grid"
plugins={Array []}
......
......@@ -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.",
}
}
<PageLoader
pageName="Teams"
/>
</div>
</div>
`;
......
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,
};
}
......
// 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>
`;
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;
}
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';
......
.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