mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13619 from grafana/13591/loading-indicators-config-pages
Adding loading spinner to org pages
This commit is contained in:
commit
795b0ca90f
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,
|
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', () => {
|
|||||||
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';
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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> {
|
|||||||
</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) {
|
|||||||
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`] = `
|
|||||||
</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`] = `
|
|||||||
</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';
|
|||||||
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', () => {
|
|||||||
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', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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) => {
|
|||||||
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', () => {
|
|||||||
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';
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
<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> {
|
|||||||
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) {
|
|||||||
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`] = `
|
|||||||
<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 = {
|
|||||||
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) => {
|
|||||||
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';
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
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) {
|
|||||||
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,33 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
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>
|
<div>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
model={Object {}}
|
model={Object {}}
|
||||||
|
@ -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) => {
|
|||||||
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', () => {
|
|||||||
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';
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
|||||||
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`] = `
|
|||||||
<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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 = {
|
|||||||
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', () => {
|
|||||||
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', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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) => {
|
|||||||
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', () => {
|
|||||||
|
|
||||||
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';
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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> {
|
|||||||
{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) {
|
|||||||
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// 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`] = `
|
|||||||
</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>
|
||||||
|
`;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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 = {
|
|||||||
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 {
|
|||||||
export interface ApiKeysState {
|
export interface ApiKeysState {
|
||||||
keys: ApiKey[];
|
keys: ApiKey[];
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
|
hasFetched: boolean;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
|||||||
plugins: Plugin[];
|
plugins: Plugin[];
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
layoutMode: string;
|
layoutMode: string;
|
||||||
|
hasFetched: boolean;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
|||||||
externalUserMngLinkUrl: string;
|
externalUserMngLinkUrl: string;
|
||||||
externalUserMngLinkName: string;
|
externalUserMngLinkName: string;
|
||||||
externalUserMngInfo: 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/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';
|
||||||
|
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