diff --git a/docker/blocks/openldap/ldap_dev.toml b/docker/blocks/openldap/ldap_dev.toml index e79771b57de..8767ff3c64a 100644 --- a/docker/blocks/openldap/ldap_dev.toml +++ b/docker/blocks/openldap/ldap_dev.toml @@ -72,6 +72,7 @@ email = "email" [[servers.group_mappings]] group_dn = "cn=admins,ou=groups,dc=grafana,dc=org" org_role = "Admin" +grafana_admin = true # The Grafana organization database id, optional, if left out the default org (id 1) will be used # org_id = 1 diff --git a/package.json b/package.json index 9ee81d7f8ac..7afe10c0772 100644 --- a/package.json +++ b/package.json @@ -160,9 +160,13 @@ "react-grid-layout": "0.16.6", "react-highlight-words": "^0.10.0", "react-popper": "^0.7.5", + "react-redux": "^5.0.7", "react-select": "^1.1.0", "react-sizeme": "^2.3.6", "react-transition-group": "^2.2.1", + "redux": "^4.0.0", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.3.0", "remarkable": "^1.7.1", "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^5.4.3", diff --git a/public/app/containers/AlertRuleList/AlertRuleList.test.tsx b/public/app/containers/AlertRuleList/AlertRuleList.test.tsx deleted file mode 100644 index f88ff4522d4..00000000000 --- a/public/app/containers/AlertRuleList/AlertRuleList.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import moment from 'moment'; -import { AlertRuleList } from './AlertRuleList'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv, createNavTree } from 'test/mocks/common'; -import { mount } from 'enzyme'; -import toJson from 'enzyme-to-json'; - -describe('AlertRuleList', () => { - let page, store; - - beforeAll(() => { - backendSrv.get.mockReturnValue( - Promise.resolve([ - { - id: 11, - dashboardId: 58, - panelId: 3, - name: 'Panel Title alert', - state: 'ok', - newStateDate: moment() - .subtract(5, 'minutes') - .format(), - evalData: {}, - executionError: '', - url: 'd/ufkcofof/my-goal', - canEdit: true, - }, - ]) - ); - - store = RootStore.create( - {}, - { - backendSrv: backendSrv, - navTree: createNavTree('alerting', 'alert-list'), - } - ); - - page = mount(); - }); - - it('should call api to get rules', () => { - expect(backendSrv.get.mock.calls[0][0]).toEqual('/api/alerts'); - }); - - it('should render 1 rule', () => { - page.update(); - const ruleNode = page.find('.alert-rule-item'); - expect(toJson(ruleNode)).toMatchSnapshot(); - }); - - it('toggle state should change pause rule if not paused', async () => { - backendSrv.post.mockReturnValue( - Promise.resolve({ - state: 'paused', - }) - ); - - page.find('.fa-pause').simulate('click'); - - // wait for api call to resolve - await Promise.resolve(); - page.update(); - - expect(store.alertList.rules[0].state).toBe('paused'); - expect(page.find('.fa-play')).toHaveLength(1); - }); -}); diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx deleted file mode 100644 index 668136dee6f..00000000000 --- a/public/app/containers/AlertRuleList/AlertRuleList.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import classNames from 'classnames'; -import { inject, observer } from 'mobx-react'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { AlertRule } from 'app/stores/AlertListStore/AlertListStore'; -import appEvents from 'app/core/app_events'; -import ContainerProps from 'app/containers/ContainerProps'; -import Highlighter from 'react-highlight-words'; - -@inject('view', 'nav', 'alertList') -@observer -export class AlertRuleList extends React.Component { - stateFilters = [ - { text: 'All', value: 'all' }, - { text: 'OK', value: 'ok' }, - { text: 'Not OK', value: 'not_ok' }, - { text: 'Alerting', value: 'alerting' }, - { text: 'No Data', value: 'no_data' }, - { text: 'Paused', value: 'paused' }, - ]; - - constructor(props) { - super(props); - - this.props.nav.load('alerting', 'alert-list'); - this.fetchRules(); - } - - onStateFilterChanged = evt => { - this.props.view.updateQuery({ state: evt.target.value }); - this.fetchRules(); - }; - - fetchRules() { - this.props.alertList.loadRules({ - state: this.props.view.query.get('state') || 'all', - }); - } - - onOpenHowTo = () => { - appEvents.emit('show-modal', { - src: 'public/app/features/alerting/partials/alert_howto.html', - modalClass: 'confirm-modal', - model: {}, - }); - }; - - onSearchQueryChange = evt => { - this.props.alertList.setSearchQuery(evt.target.value); - }; - - render() { - const { nav, alertList } = this.props; - - return ( -
- -
-
-
- -
-
- - -
- -
-
- - - -
-
    - {alertList.filteredRules.map(rule => ( - - ))} -
-
-
-
- ); - } -} - -function AlertStateFilterOption({ text, value }) { - return ( - - ); -} - -export interface AlertRuleItemProps { - rule: AlertRule; - search: string; -} - -@observer -export class AlertRuleItem extends React.Component { - toggleState = () => { - this.props.rule.togglePaused(); - }; - - renderText(text: string) { - return ( - - ); - } - - render() { - const { rule } = this.props; - - const stateClass = classNames({ - fa: true, - 'fa-play': rule.isPaused, - 'fa-pause': !rule.isPaused, - }); - - const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; - - return ( -
  • - - - -
    -
    - -
    - {this.renderText(rule.stateText)} - for {rule.stateAge} -
    -
    - {rule.info &&
    {this.renderText(rule.info)}
    } -
    - -
    - - - - -
    -
  • - ); - } -} - -export default hot(module)(AlertRuleList); diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts index 97889278fdc..ce09b992f80 100644 --- a/public/app/containers/ContainerProps.ts +++ b/public/app/containers/ContainerProps.ts @@ -1,16 +1,10 @@ -import { SearchStore } from './../stores/SearchStore/SearchStore'; -import { ServerStatsStore } from './../stores/ServerStatsStore/ServerStatsStore'; import { NavStore } from './../stores/NavStore/NavStore'; import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; -import { AlertListStore } from './../stores/AlertListStore/AlertListStore'; import { ViewStore } from './../stores/ViewStore/ViewStore'; import { FolderStore } from './../stores/FolderStore/FolderStore'; interface ContainerProps { - search: typeof SearchStore.Type; - serverStats: typeof ServerStatsStore.Type; nav: typeof NavStore.Type; - alertList: typeof AlertListStore.Type; permissions: typeof PermissionsStore.Type; view: typeof ViewStore.Type; folder: typeof FolderStore.Type; diff --git a/public/app/containers/ServerStats/ServerStats.test.tsx b/public/app/containers/ServerStats/ServerStats.test.tsx deleted file mode 100644 index a329a47527d..00000000000 --- a/public/app/containers/ServerStats/ServerStats.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import { ServerStats } from './ServerStats'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv, createNavTree } from 'test/mocks/common'; - -describe('ServerStats', () => { - it('Should render table with stats', done => { - backendSrv.get.mockReturnValue( - Promise.resolve({ - dashboards: 10, - }) - ); - - const store = RootStore.create( - {}, - { - backendSrv: backendSrv, - navTree: createNavTree('cfg', 'admin', 'server-stats'), - } - ); - - const page = renderer.create(); - - setTimeout(() => { - expect(page.toJSON()).toMatchSnapshot(); - done(); - }); - }); -}); diff --git a/public/app/containers/ServerStats/ServerStats.tsx b/public/app/containers/ServerStats/ServerStats.tsx deleted file mode 100644 index 63e78996041..00000000000 --- a/public/app/containers/ServerStats/ServerStats.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import ContainerProps from 'app/containers/ContainerProps'; - -@inject('nav', 'serverStats') -@observer -export class ServerStats extends React.Component { - constructor(props) { - super(props); - const { nav, serverStats } = this.props; - - nav.load('cfg', 'admin', 'server-stats'); - serverStats.load(); - } - - render() { - const { nav, serverStats } = this.props; - return ( -
    - -
    - - - - - - - - {serverStats.stats.map(StatItem)} -
    NameValue
    -
    -
    - ); - } -} - -function StatItem(stat) { - return ( - - {stat.name} - {stat.value} - - ); -} - -export default hot(module)(ServerStats); diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts new file mode 100644 index 00000000000..74b61f845c0 --- /dev/null +++ b/public/app/core/actions/index.ts @@ -0,0 +1,3 @@ +import { updateLocation } from './location'; + +export { updateLocation }; diff --git a/public/app/core/actions/location.ts b/public/app/core/actions/location.ts new file mode 100644 index 00000000000..6f7ac67363e --- /dev/null +++ b/public/app/core/actions/location.ts @@ -0,0 +1,13 @@ +import { LocationUpdate } from 'app/types'; + +export type Action = UpdateLocationAction; + +export interface UpdateLocationAction { + type: 'UPDATE_LOCATION'; + payload: LocationUpdate; +} + +export const updateLocation = (location: LocationUpdate): UpdateLocationAction => ({ + type: 'UPDATE_LOCATION', + payload: location, +}); diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts new file mode 100644 index 00000000000..56d129fd263 --- /dev/null +++ b/public/app/core/actions/navModel.ts @@ -0,0 +1,13 @@ +export type Action = UpdateNavIndexAction; + +// this action is not used yet +// kind of just a placeholder, will be need for dynamic pages +// like datasource edit, teams edit page + +export interface UpdateNavIndexAction { + type: 'UPDATE_NAV_INDEX'; +} + +export const updateNavIndex = (): UpdateNavIndexAction => ({ + type: 'UPDATE_NAV_INDEX', +}); diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index b7bef2495bb..9feddde68ce 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { observer } from 'mobx-react'; -import { NavModel, NavModelItem } from '../../nav_model_srv'; +import { NavModel, NavModelItem } from 'app/types'; import classNames from 'classnames'; import appEvents from 'app/core/app_events'; import { toJS } from 'mobx'; diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 6c7b8cf3bf7..926438ffbc9 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -10,6 +10,7 @@ import { createStore } from 'app/stores/store'; import colors from 'app/core/utils/colors'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { configureStore } from 'app/stores/configureStore'; export class GrafanaCtrl { /** @ngInject */ @@ -25,6 +26,7 @@ export class GrafanaCtrl { datasourceSrv: DatasourceSrv ) { // sets singleston instances for angular services so react components can access them + configureStore(); setBackendSrv(backendSrv); createStore({ backendSrv, datasourceSrv }); diff --git a/public/app/core/components/search/SearchResult.tsx b/public/app/core/components/search/SearchResult.tsx index 3141d29ac7f..13333c168f9 100644 --- a/public/app/core/components/search/SearchResult.tsx +++ b/public/app/core/components/search/SearchResult.tsx @@ -1,22 +1,13 @@ import React from 'react'; import classNames from 'classnames'; -import { observer } from 'mobx-react'; -import { store } from 'app/stores/store'; -export interface SearchResultProps { - search: any; -} - -@observer -export class SearchResult extends React.Component { +export class SearchResult extends React.Component { constructor(props) { super(props); this.state = { - search: store.search, + search: '', }; - - store.search.query(); } render() { @@ -30,7 +21,6 @@ export interface SectionProps { section: any; } -@observer export class SearchResultSection extends React.Component { constructor(props) { super(props); diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts new file mode 100644 index 00000000000..be13528c91c --- /dev/null +++ b/public/app/core/reducers/index.ts @@ -0,0 +1,7 @@ +import { navIndexReducer as navIndex } from './navModel'; +import { locationReducer as location } from './location'; + +export default { + navIndex, + location, +}; diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts new file mode 100644 index 00000000000..4591448d082 --- /dev/null +++ b/public/app/core/reducers/location.ts @@ -0,0 +1,33 @@ +import { Action } from 'app/core/actions/location'; +import { LocationState, UrlQueryMap } from 'app/types'; +import { toUrlParams } from 'app/core/utils/url'; + +export const initialState: LocationState = { + url: '', + path: '', + query: {}, + routeParams: {}, +}; + +function renderUrl(path: string, query: UrlQueryMap): string { + if (Object.keys(query).length > 0) { + path += '?' + toUrlParams(query); + } + return path; +} + +export const locationReducer = (state = initialState, action: Action): LocationState => { + switch (action.type) { + case 'UPDATE_LOCATION': { + const { path, query, routeParams } = action.payload; + return { + url: renderUrl(path || state.path, query), + path: path || state.path, + query: query || state.query, + routeParams: routeParams || state.routeParams, + }; + } + } + + return state; +}; diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts new file mode 100644 index 00000000000..26acdb39a3d --- /dev/null +++ b/public/app/core/reducers/navModel.ts @@ -0,0 +1,29 @@ +import { Action } from 'app/core/actions/navModel'; +import { NavModelItem, NavIndex } from 'app/types'; +import config from 'app/core/config'; + +export function buildInitialState(): NavIndex { + const navIndex: NavIndex = {}; + const rootNodes = config.bootData.navTree as NavModelItem[]; + buildNavIndex(navIndex, rootNodes); + return navIndex; +} + +function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?: NavModelItem) { + for (const node of children) { + navIndex[node.id] = { + ...node, + parentItem: parentItem, + }; + + if (node.children) { + buildNavIndex(navIndex, node.children, node); + } + } +} + +export const initialState: NavIndex = buildInitialState(); + +export const navIndexReducer = (state = initialState, action: Action): NavIndex => { + return state; +}; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts new file mode 100644 index 00000000000..8b3a3edd84e --- /dev/null +++ b/public/app/core/selectors/navModel.ts @@ -0,0 +1,39 @@ +import { NavModel, NavModelItem, NavIndex } from 'app/types'; + +function getNotFoundModel(): NavModel { + const node: NavModelItem = { + id: 'not-found', + text: 'Page not found', + icon: 'fa fa-fw fa-warning', + subTitle: '404 Error', + url: 'not-found', + }; + + return { + node: node, + main: node, + }; +} + +export function getNavModel(navIndex: NavIndex, id: string): NavModel { + if (navIndex[id]) { + const node = navIndex[id]; + const main = { + ...node.parentItem, + }; + + main.children = main.children.map(item => { + return { + ...item, + active: item.url === node.url, + }; + }); + + return { + node: node, + main: main, + }; + } else { + return getNotFoundModel(); + } +} diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index bdc2976a94c..29326794ac6 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -1,8 +1,10 @@ import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import { store } from 'app/stores/store'; +import { store as reduxStore } from 'app/stores/configureStore'; import { reaction } from 'mobx'; import locationUtil from 'app/core/utils/location_util'; +import { updateLocation } from 'app/core/actions'; // Services that handles angular -> mobx store sync & other react <-> angular sync export class BridgeSrv { @@ -19,12 +21,30 @@ export class BridgeSrv { if (store.view.currentUrl !== angularUrl) { store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); } + const state = reduxStore.getState(); + if (state.location.url !== angularUrl) { + reduxStore.dispatch( + updateLocation({ + path: this.$location.path(), + query: this.$location.search(), + routeParams: this.$route.current.params, + }) + ); + } }); this.$rootScope.$on('$routeChangeSuccess', (evt, data) => { store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); + reduxStore.dispatch( + updateLocation({ + path: this.$location.path(), + query: this.$location.search(), + routeParams: this.$route.current.params, + }) + ); }); + // listen for mobx store changes and update angular reaction( () => store.view.currentUrl, currentUrl => { @@ -39,6 +59,19 @@ export class BridgeSrv { } ); + // Listen for changes in redux location -> update angular location + reduxStore.subscribe(() => { + const state = reduxStore.getState(); + const angularUrl = this.$location.url(); + const url = locationUtil.stripBaseFromUrl(state.location.url); + if (angularUrl !== url) { + this.$timeout(() => { + this.$location.url(url); + }); + console.log('store updating angular $location.url', url); + } + }); + appEvents.on('location-change', payload => { const urlWithoutBase = locationUtil.stripBaseFromUrl(payload.href); if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) { diff --git a/public/app/features/admin/admin_edit_org_ctrl.ts b/public/app/features/admin/AdminEditOrgCtrl.ts similarity index 88% rename from public/app/features/admin/admin_edit_org_ctrl.ts rename to public/app/features/admin/AdminEditOrgCtrl.ts index ec3f8548023..3117c5f0f9b 100644 --- a/public/app/features/admin/admin_edit_org_ctrl.ts +++ b/public/app/features/admin/AdminEditOrgCtrl.ts @@ -1,6 +1,5 @@ -import angular from 'angular'; -export class AdminEditOrgCtrl { +export default class AdminEditOrgCtrl { /** @ngInject */ constructor($scope, $routeParams, backendSrv, $location, navModelSrv) { $scope.init = () => { @@ -48,4 +47,3 @@ export class AdminEditOrgCtrl { } } -angular.module('grafana.controllers').controller('AdminEditOrgCtrl', AdminEditOrgCtrl); diff --git a/public/app/features/admin/admin_edit_user_ctrl.ts b/public/app/features/admin/AdminEditUserCtrl.ts similarity index 95% rename from public/app/features/admin/admin_edit_user_ctrl.ts rename to public/app/features/admin/AdminEditUserCtrl.ts index c34ccdc1cad..bf72c1746aa 100644 --- a/public/app/features/admin/admin_edit_user_ctrl.ts +++ b/public/app/features/admin/AdminEditUserCtrl.ts @@ -1,7 +1,6 @@ -import angular from 'angular'; import _ from 'lodash'; -export class AdminEditUserCtrl { +export default class AdminEditUserCtrl { /** @ngInject */ constructor($scope, $routeParams, backendSrv, $location, navModelSrv) { $scope.user = {}; @@ -117,5 +116,3 @@ export class AdminEditUserCtrl { $scope.init(); } } - -angular.module('grafana.controllers').controller('AdminEditUserCtrl', AdminEditUserCtrl); diff --git a/public/app/features/admin/admin_list_orgs_ctrl.ts b/public/app/features/admin/AdminListOrgsCtrl.ts similarity index 84% rename from public/app/features/admin/admin_list_orgs_ctrl.ts rename to public/app/features/admin/AdminListOrgsCtrl.ts index 0513752aa3e..9190f7f494e 100644 --- a/public/app/features/admin/admin_list_orgs_ctrl.ts +++ b/public/app/features/admin/AdminListOrgsCtrl.ts @@ -1,6 +1,5 @@ -import angular from 'angular'; -export class AdminListOrgsCtrl { +export default class AdminListOrgsCtrl { /** @ngInject */ constructor($scope, backendSrv, navModelSrv) { $scope.init = () => { @@ -33,4 +32,3 @@ export class AdminListOrgsCtrl { } } -angular.module('grafana.controllers').controller('AdminListOrgsCtrl', AdminListOrgsCtrl); diff --git a/public/app/features/admin/admin_list_users_ctrl.ts b/public/app/features/admin/AdminListUsersCtrl.ts similarity index 100% rename from public/app/features/admin/admin_list_users_ctrl.ts rename to public/app/features/admin/AdminListUsersCtrl.ts diff --git a/public/app/features/admin/ServerStats.test.tsx b/public/app/features/admin/ServerStats.test.tsx new file mode 100644 index 00000000000..cbcc580f612 --- /dev/null +++ b/public/app/features/admin/ServerStats.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { ServerStats } from './ServerStats'; +import { createNavModel } from 'test/mocks/common'; +import { ServerStat } from './state/apis'; + +describe('ServerStats', () => { + it('Should render table with stats', done => { + const navModel = createNavModel('Admin', 'stats'); + const stats: ServerStat[] = [{ name: 'Total dashboards', value: 10 }, { name: 'Total Users', value: 1 }]; + + const getServerStats = () => { + return Promise.resolve(stats); + }; + + const page = renderer.create(); + + setTimeout(() => { + expect(page.toJSON()).toMatchSnapshot(); + done(); + }); + }); +}); diff --git a/public/app/features/admin/ServerStats.tsx b/public/app/features/admin/ServerStats.tsx new file mode 100644 index 00000000000..40be87ed4d3 --- /dev/null +++ b/public/app/features/admin/ServerStats.tsx @@ -0,0 +1,73 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { NavModel, StoreState } from 'app/types'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { getServerStats, ServerStat } from './state/apis'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; + +interface Props { + navModel: NavModel; + getServerStats: () => Promise; +} + +interface State { + stats: ServerStat[]; +} + +export class ServerStats extends PureComponent { + constructor(props) { + super(props); + + this.state = { + stats: [], + }; + } + + async componentDidMount() { + try { + const stats = await this.props.getServerStats(); + this.setState({ stats }); + } catch (error) { + console.error(error); + } + } + + render() { + const { navModel } = this.props; + const { stats } = this.state; + + return ( +
    + +
    + + + + + + + + {stats.map(StatItem)} +
    NameValue
    +
    +
    + ); + } +} + +function StatItem(stat: ServerStat) { + return ( + + {stat.name} + {stat.value} + + ); +} + +const mapStateToProps = (state: StoreState) => ({ + navModel: getNavModel(state.navIndex, 'server-stats'), + getServerStats: getServerStats, +}); + +export default hot(module)(connect(mapStateToProps)(ServerStats)); diff --git a/public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap b/public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap similarity index 60% rename from public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap rename to public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap index eac793ca2ca..63de5bcd870 100644 --- a/public/app/containers/ServerStats/__snapshots__/ServerStats.test.tsx.snap +++ b/public/app/features/admin/__snapshots__/ServerStats.test.tsx.snap @@ -17,8 +17,9 @@ exports[`ServerStats Should render table with stats 1`] = ` - - +
    - admin-Text + Admin - +
    + subTitle +