mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'react-mobx'
This commit is contained in:
@@ -24,5 +24,6 @@ module.exports = {
|
|||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"./public/test/jest-shim.ts",
|
"./public/test/jest-shim.ts",
|
||||||
"./public/test/jest-setup.ts"
|
"./public/test/jest-setup.ts"
|
||||||
]
|
],
|
||||||
|
"snapshotSerializers": ["enzyme-to-json/serializer"],
|
||||||
};
|
};
|
||||||
|
|||||||
20
package.json
20
package.json
@@ -25,6 +25,7 @@
|
|||||||
"css-loader": "^0.28.7",
|
"css-loader": "^0.28.7",
|
||||||
"enzyme": "^3.1.0",
|
"enzyme": "^3.1.0",
|
||||||
"enzyme-adapter-react-16": "^1.0.1",
|
"enzyme-adapter-react-16": "^1.0.1",
|
||||||
|
"enzyme-to-json": "^3.3.0",
|
||||||
"es6-promise": "^3.0.2",
|
"es6-promise": "^3.0.2",
|
||||||
"es6-shim": "^0.35.3",
|
"es6-shim": "^0.35.3",
|
||||||
"expect.js": "~0.2.0",
|
"expect.js": "~0.2.0",
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
"html-loader": "^0.5.1",
|
"html-loader": "^0.5.1",
|
||||||
"html-webpack-plugin": "^2.30.1",
|
"html-webpack-plugin": "^2.30.1",
|
||||||
"husky": "^0.14.3",
|
"husky": "^0.14.3",
|
||||||
"jest": "^21.2.1",
|
"jest": "^22.0.4",
|
||||||
"jshint-stylish": "~2.2.1",
|
"jshint-stylish": "~2.2.1",
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"karma": "1.7.0",
|
"karma": "1.7.0",
|
||||||
@@ -83,12 +84,12 @@
|
|||||||
"sinon": "1.17.6",
|
"sinon": "1.17.6",
|
||||||
"systemjs": "0.20.19",
|
"systemjs": "0.20.19",
|
||||||
"systemjs-plugin-css": "^0.1.36",
|
"systemjs-plugin-css": "^0.1.36",
|
||||||
"ts-jest": "^21.1.3",
|
"ts-jest": "^22.0.0",
|
||||||
"ts-loader": "^2.3.7",
|
"ts-loader": "^3.2.0",
|
||||||
"tslint": "^5.7.0",
|
"tslint": "^5.8.0",
|
||||||
"tslint-loader": "^3.5.3",
|
"tslint-loader": "^3.5.3",
|
||||||
"typescript": "^2.5.2",
|
"typescript": "^2.6.2",
|
||||||
"webpack": "^3.6.0",
|
"webpack": "^3.10.0",
|
||||||
"webpack-bundle-analyzer": "^2.9.0",
|
"webpack-bundle-analyzer": "^2.9.0",
|
||||||
"webpack-cleanup-plugin": "^0.5.1",
|
"webpack-cleanup-plugin": "^0.5.1",
|
||||||
"webpack-merge": "^4.1.0",
|
"webpack-merge": "^4.1.0",
|
||||||
@@ -138,12 +139,15 @@
|
|||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"jquery": "^3.2.1",
|
"jquery": "^3.2.1",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
|
"mobx": "^3.4.1",
|
||||||
|
"mobx-react": "^4.3.5",
|
||||||
|
"mobx-state-tree": "^1.3.1",
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
"mousetrap": "^1.6.0",
|
"mousetrap": "^1.6.0",
|
||||||
"perfect-scrollbar": "^1.2.0",
|
"perfect-scrollbar": "^1.2.0",
|
||||||
"prop-types": "^15.6.0",
|
"prop-types": "^15.6.0",
|
||||||
"react": "^16.1.1",
|
"react": "^16.2.0",
|
||||||
"react-dom": "^16.1.1",
|
"react-dom": "^16.2.0",
|
||||||
"react-grid-layout": "^0.16.1",
|
"react-grid-layout": "^0.16.1",
|
||||||
"react-sizeme": "^2.3.6",
|
"react-sizeme": "^2.3.6",
|
||||||
"remarkable": "^1.7.1",
|
"remarkable": "^1.7.1",
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var response models.AlertStateType = models.AlertStatePending
|
var response models.AlertStateType = models.AlertStatePending
|
||||||
pausedState := "un paused"
|
pausedState := "un-paused"
|
||||||
if cmd.Paused {
|
if cmd.Paused {
|
||||||
response = models.AlertStatePaused
|
response = models.AlertStatePaused
|
||||||
pausedState = "paused"
|
pausedState = "paused"
|
||||||
@@ -287,7 +287,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
|
|||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"alertId": alertId,
|
"alertId": alertId,
|
||||||
"state": response,
|
"state": response,
|
||||||
"message": "alert " + pausedState,
|
"message": "Alert " + pausedState,
|
||||||
}
|
}
|
||||||
|
|
||||||
return Json(200, result)
|
return Json(200, result)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
|
|||||||
params = append(params, query.PanelId)
|
params = append(params, query.PanelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(query.State) > 0 && query.State[0] != "ALL" {
|
if len(query.State) > 0 && query.State[0] != "all" {
|
||||||
sql.WriteString(` AND (`)
|
sql.WriteString(` AND (`)
|
||||||
for i, v := range query.State {
|
for i, v := range query.State {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func init() {
|
|||||||
bus.AddHandler("sql", GetAdminStats)
|
bus.AddHandler("sql", GetAdminStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
var activeUserTimeLimit time.Duration = time.Hour * 24 * 14
|
var activeUserTimeLimit time.Duration = time.Hour * 24 * 30
|
||||||
|
|
||||||
func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
|
func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
|
||||||
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
|
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ _.move = function(array, fromIndex, toIndex) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
import { coreModule, registerAngularDirectives } from './core/core';
|
import { coreModule, registerAngularDirectives } from './core/core';
|
||||||
|
import { setupAngularRoutes } from './routes/routes';
|
||||||
|
|
||||||
export class GrafanaApp {
|
export class GrafanaApp {
|
||||||
registerFunctions: any;
|
registerFunctions: any;
|
||||||
@@ -113,6 +114,7 @@ export class GrafanaApp {
|
|||||||
this.useModule(coreModule);
|
this.useModule(coreModule);
|
||||||
|
|
||||||
// register react angular wrappers
|
// register react angular wrappers
|
||||||
|
coreModule.config(setupAngularRoutes);
|
||||||
registerAngularDirectives();
|
registerAngularDirectives();
|
||||||
|
|
||||||
var preBootRequires = [System.import('app/features/all')];
|
var preBootRequires = [System.import('app/features/all')];
|
||||||
@@ -121,6 +123,7 @@ export class GrafanaApp {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
// disable tool tip animation
|
// disable tool tip animation
|
||||||
$.fn.tooltip.defaults.animation = false;
|
$.fn.tooltip.defaults.animation = false;
|
||||||
|
|
||||||
// bootstrap the app
|
// bootstrap the app
|
||||||
angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
|
angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
|
||||||
_.each(this.preBootModules, module => {
|
_.each(this.preBootModules, module => {
|
||||||
|
|||||||
68
public/app/containers/AlertRuleList/AlertRuleList.jest.tsx
Normal file
68
public/app/containers/AlertRuleList/AlertRuleList.jest.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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: '',
|
||||||
|
dashboardUri: 'db/mygool',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
store = RootStore.create(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
backendSrv: backendSrv,
|
||||||
|
navTree: createNavTree('alerting', 'alert-list'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
page = mount(<AlertRuleList {...store} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call api to get rules', () => {
|
||||||
|
expect(backendSrv.get.mock.calls[0][0]).toEqual('/api/alerts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render 1 rule', () => {
|
||||||
|
page.update();
|
||||||
|
let ruleNode = page.find('.card-item-wrapper');
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
145
public/app/containers/AlertRuleList/AlertRuleList.tsx
Normal file
145
public/app/containers/AlertRuleList/AlertRuleList.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { inject, observer } from 'mobx-react';
|
||||||
|
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||||
|
import { IAlertRule } from 'app/stores/AlertListStore/AlertListStore';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import IContainerProps from 'app/containers/IContainerProps';
|
||||||
|
|
||||||
|
@inject('view', 'nav', 'alertList')
|
||||||
|
@observer
|
||||||
|
export class AlertRuleList extends React.Component<IContainerProps, any> {
|
||||||
|
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: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { nav, alertList } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader model={nav as any} />
|
||||||
|
<div className="page-container page-body">
|
||||||
|
<div className="page-action-bar">
|
||||||
|
<div className="gf-form">
|
||||||
|
<label className="gf-form-label">Filter by state</label>
|
||||||
|
|
||||||
|
<div className="gf-form-select-wrapper width-13">
|
||||||
|
<select className="gf-form-input" onChange={this.onStateFilterChanged} value={alertList.stateFilter}>
|
||||||
|
{this.stateFilters.map(AlertStateFilterOption)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="page-action-bar__spacer" />
|
||||||
|
|
||||||
|
<a className="btn btn-secondary" onClick={this.onOpenHowTo}>
|
||||||
|
<i className="fa fa-info-circle" /> How to add an alert
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="card-section card-list-layout-list">
|
||||||
|
<ol className="card-list">{alertList.rules.map(rule => <AlertRuleItem rule={rule} key={rule.id} />)}</ol>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertStateFilterOption({ text, value }) {
|
||||||
|
return (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{text}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlertRuleItemProps {
|
||||||
|
rule: IAlertRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> {
|
||||||
|
toggleState = () => {
|
||||||
|
this.props.rule.togglePaused();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { rule } = this.props;
|
||||||
|
|
||||||
|
let stateClass = classNames({
|
||||||
|
fa: true,
|
||||||
|
'fa-play': rule.isPaused,
|
||||||
|
'fa-pause': !rule.isPaused,
|
||||||
|
});
|
||||||
|
|
||||||
|
let ruleUrl = `dashboard/${rule.dashboardUri}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="card-item-wrapper">
|
||||||
|
<div className="card-item card-item--alert">
|
||||||
|
<div className="card-item-header">
|
||||||
|
<div className="card-item-type">
|
||||||
|
<a
|
||||||
|
className="card-item-cog"
|
||||||
|
title="Pausing an alert rule prevents it from executing"
|
||||||
|
onClick={this.toggleState}
|
||||||
|
>
|
||||||
|
<i className={stateClass} />
|
||||||
|
</a>
|
||||||
|
<a className="card-item-cog" href={ruleUrl} title="Edit alert rule">
|
||||||
|
<i className="icon-gf icon-gf-settings" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-item-body">
|
||||||
|
<div className="card-item-details">
|
||||||
|
<div className="card-item-name">
|
||||||
|
<a href={ruleUrl}>{rule.name}</a>
|
||||||
|
</div>
|
||||||
|
<div className="card-item-sub-name">
|
||||||
|
<span className={`alert-list-item-state ${rule.stateClass}`}>
|
||||||
|
<i className={rule.stateIcon} /> {rule.stateText}
|
||||||
|
</span>
|
||||||
|
<span> for {rule.stateAge}</span>
|
||||||
|
</div>
|
||||||
|
{rule.info && <div className="small muted">{rule.info}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`AlertRuleList should render 1 rule 1`] = `
|
||||||
|
<li
|
||||||
|
className="card-item-wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item card-item--alert"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item-header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item-type"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="card-item-cog"
|
||||||
|
onClick={[Function]}
|
||||||
|
title="Pausing an alert rule prevents it from executing"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-pause"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="card-item-cog"
|
||||||
|
href="dashboard/db/mygool?panelId=3&fullscreen&edit&tab=alert"
|
||||||
|
title="Edit alert rule"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="icon-gf icon-gf-settings"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="card-item-body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item-details"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item-name"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="dashboard/db/mygool?panelId=3&fullscreen&edit&tab=alert"
|
||||||
|
>
|
||||||
|
Panel Title alert
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="card-item-sub-name"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="alert-list-item-state alert-state-ok"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="icon-gf icon-gf-online"
|
||||||
|
/>
|
||||||
|
|
||||||
|
OK
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
for
|
||||||
|
5 minutes
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
15
public/app/containers/IContainerProps.ts
Normal file
15
public/app/containers/IContainerProps.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { SearchStore } from './../stores/SearchStore/SearchStore';
|
||||||
|
import { ServerStatsStore } from './../stores/ServerStatsStore/ServerStatsStore';
|
||||||
|
import { NavStore } from './../stores/NavStore/NavStore';
|
||||||
|
import { AlertListStore } from './../stores/AlertListStore/AlertListStore';
|
||||||
|
import { ViewStore } from './../stores/ViewStore/ViewStore';
|
||||||
|
|
||||||
|
interface IContainerProps {
|
||||||
|
search: typeof SearchStore.Type;
|
||||||
|
serverStats: typeof ServerStatsStore.Type;
|
||||||
|
nav: typeof NavStore.Type;
|
||||||
|
alertList: typeof AlertListStore.Type;
|
||||||
|
view: typeof ViewStore.Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IContainerProps;
|
||||||
30
public/app/containers/ServerStats/ServerStats.jest.tsx
Normal file
30
public/app/containers/ServerStats/ServerStats.jest.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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(<ServerStats {...store} />);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(page.toJSON()).toMatchSnapshot();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
45
public/app/containers/ServerStats/ServerStats.tsx
Normal file
45
public/app/containers/ServerStats/ServerStats.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { inject, observer } from 'mobx-react';
|
||||||
|
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||||
|
import IContainerProps from 'app/containers/IContainerProps';
|
||||||
|
|
||||||
|
@inject('nav', 'serverStats')
|
||||||
|
@observer
|
||||||
|
export class ServerStats extends React.Component<IContainerProps, any> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const { nav, serverStats } = this.props;
|
||||||
|
|
||||||
|
nav.load('cfg', 'admin', 'server-stats');
|
||||||
|
serverStats.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { nav, serverStats } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader model={nav as any} />
|
||||||
|
<div className="page-container page-body">
|
||||||
|
<table className="filter-table form-inline">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{serverStats.stats.map(StatItem)}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatItem(stat) {
|
||||||
|
return (
|
||||||
|
<tr key={stat.name}>
|
||||||
|
<td>{stat.name}</td>
|
||||||
|
<td>{stat.value}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ServerStats Should render table with stats 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="page-header-canvas"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="page-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="page-header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="page-header__inner"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="page-header__logo"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className="page-header__info-block"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
className="page-header__title"
|
||||||
|
>
|
||||||
|
admin-Text
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<div
|
||||||
|
className="gf-form-select-wrapper width-20 page-header__select-nav"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form-select-icon "
|
||||||
|
htmlFor="page-header-select-nav"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className="gf-select-nav gf-form-input"
|
||||||
|
defaultValue="/url/server-stats"
|
||||||
|
id="page-header-select-nav"
|
||||||
|
onChange={[Function]}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="/url/server-stats"
|
||||||
|
>
|
||||||
|
server-stats-Text
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
className="gf-tabs page-header__tabs"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
className="gf-tabs-item"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="gf-tabs-link active"
|
||||||
|
href="/url/server-stats"
|
||||||
|
target={undefined}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className=""
|
||||||
|
/>
|
||||||
|
server-stats-Text
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="page-container page-body"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
className="filter-table form-inline"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Total dashboards
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
10
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Total users
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Active users (seen last 30 days)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Total orgs
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Total playlists
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Total snapshots
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Total dashboard tags
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Total starred dashboards
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Total alerts
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
import { react2AngularDirective } from "app/core/utils/react2angular";
|
||||||
import { PasswordStrength } from './components/PasswordStrength';
|
import { PasswordStrength } from "./components/PasswordStrength";
|
||||||
import PageHeader from './components/PageHeader/PageHeader';
|
import PageHeader from "./components/PageHeader/PageHeader";
|
||||||
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from "./components/EmptyListCTA/EmptyListCTA";
|
||||||
import LoginBackground from './components/Login/LoginBackground';
|
import LoginBackground from "./components/Login/LoginBackground";
|
||||||
|
import { SearchResult } from "./components/search/SearchResult";
|
||||||
|
|
||||||
export function registerAngularDirectives() {
|
export function registerAngularDirectives() {
|
||||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
react2AngularDirective("passwordStrength", PasswordStrength, ["password"]);
|
||||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
react2AngularDirective("pageHeader", PageHeader, ["model", "noTabs"]);
|
||||||
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
react2AngularDirective("emptyListCta", EmptyListCTA, ["model"]);
|
||||||
react2AngularDirective('loginBackground', LoginBackground, []);
|
react2AngularDirective("loginBackground", LoginBackground, []);
|
||||||
|
react2AngularDirective("searchResult", SearchResult, []);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { NavModel, NavModelItem } from "../../nav_model_srv";
|
import { NavModel, NavModelItem } from '../../nav_model_srv';
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames';
|
||||||
import appEvents from "app/core/app_events";
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
export interface IProps {
|
export interface IProps {
|
||||||
model: NavModel;
|
model: NavModel;
|
||||||
@@ -13,8 +13,8 @@ function TabItem(tab: NavModelItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let tabClasses = classNames({
|
let tabClasses = classNames({
|
||||||
"gf-tabs-link": true,
|
'gf-tabs-link': true,
|
||||||
active: tab.active
|
active: tab.active,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -49,13 +49,7 @@ function Navigation({ main }: { main: NavModelItem }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectNav({
|
function SelectNav({ main, customCss }: { main: NavModelItem; customCss: string }) {
|
||||||
main,
|
|
||||||
customCss
|
|
||||||
}: {
|
|
||||||
main: NavModelItem;
|
|
||||||
customCss: string;
|
|
||||||
}) {
|
|
||||||
const defaultSelectedItem = main.children.find(navItem => {
|
const defaultSelectedItem = main.children.find(navItem => {
|
||||||
return navItem.active === true;
|
return navItem.active === true;
|
||||||
});
|
});
|
||||||
@@ -63,15 +57,12 @@ function SelectNav({
|
|||||||
const gotoUrl = evt => {
|
const gotoUrl = evt => {
|
||||||
var element = evt.target;
|
var element = evt.target;
|
||||||
var url = element.options[element.selectedIndex].value;
|
var url = element.options[element.selectedIndex].value;
|
||||||
appEvents.emit("location-change", { href: url });
|
appEvents.emit('location-change', { href: url });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`gf-form-select-wrapper width-20 ${customCss}`}>
|
<div className={`gf-form-select-wrapper width-20 ${customCss}`}>
|
||||||
<label
|
<label className={`gf-form-select-icon ${defaultSelectedItem.icon}`} htmlFor="page-header-select-nav" />
|
||||||
className={`gf-form-select-icon ${defaultSelectedItem.icon}`}
|
|
||||||
htmlFor="page-header-select-nav"
|
|
||||||
/>
|
|
||||||
{/* Label to make it clickable */}
|
{/* Label to make it clickable */}
|
||||||
<select
|
<select
|
||||||
className="gf-select-nav gf-form-input"
|
className="gf-select-nav gf-form-input"
|
||||||
@@ -86,9 +77,7 @@ function SelectNav({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Tabs({ main, customCss }: { main: NavModelItem; customCss: string }) {
|
function Tabs({ main, customCss }: { main: NavModelItem; customCss: string }) {
|
||||||
return (
|
return <ul className={`gf-tabs ${customCss}`}>{main.children.map(TabItem)}</ul>;
|
||||||
<ul className={`gf-tabs ${customCss}`}>{main.children.map(TabItem)}</ul>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PageHeader extends React.Component<IProps, any> {
|
export default class PageHeader extends React.Component<IProps, any> {
|
||||||
@@ -125,13 +114,9 @@ export default class PageHeader extends React.Component<IProps, any> {
|
|||||||
{main.text && <h1 className="page-header__title">{main.text}</h1>}
|
{main.text && <h1 className="page-header__title">{main.text}</h1>}
|
||||||
{main.breadcrumbs &&
|
{main.breadcrumbs &&
|
||||||
main.breadcrumbs.length > 0 && (
|
main.breadcrumbs.length > 0 && (
|
||||||
<h1 className="page-header__title">
|
<h1 className="page-header__title">{this.renderBreadcrumb(main.breadcrumbs)}</h1>
|
||||||
{this.renderBreadcrumb(main.breadcrumbs)}
|
|
||||||
</h1>
|
|
||||||
)}
|
)}
|
||||||
{main.subTitle && (
|
{main.subTitle && <div className="page-header__sub-title">{main.subTitle}</div>}
|
||||||
<div className="page-header__sub-title">{main.subTitle}</div>
|
|
||||||
)}
|
|
||||||
{main.subType && (
|
{main.subType && (
|
||||||
<div className="page-header__stamps">
|
<div className="page-header__stamps">
|
||||||
<i className={main.subType.icon} />
|
<i className={main.subType.icon} />
|
||||||
|
|||||||
@@ -6,20 +6,22 @@ import coreModule from 'app/core/core_module';
|
|||||||
import { profiler } from 'app/core/profiler';
|
import { profiler } from 'app/core/profiler';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import Drop from 'tether-drop';
|
import Drop from 'tether-drop';
|
||||||
|
import { createStore } from 'app/stores/store';
|
||||||
|
|
||||||
export class GrafanaCtrl {
|
export class GrafanaCtrl {
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, globalEventSrv) {
|
constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, bridgeSrv, backendSrv) {
|
||||||
|
createStore(backendSrv);
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = function() {
|
||||||
$scope.contextSrv = contextSrv;
|
$scope.contextSrv = contextSrv;
|
||||||
|
$scope.appSubUrl = config.appSubUrl;
|
||||||
$rootScope.appSubUrl = config.appSubUrl;
|
|
||||||
$scope._ = _;
|
$scope._ = _;
|
||||||
|
|
||||||
profiler.init(config, $rootScope);
|
profiler.init(config, $rootScope);
|
||||||
alertSrv.init();
|
alertSrv.init();
|
||||||
utilSrv.init();
|
utilSrv.init();
|
||||||
globalEventSrv.init();
|
bridgeSrv.init();
|
||||||
|
|
||||||
$scope.dashAlerts = alertSrv;
|
$scope.dashAlerts = alertSrv;
|
||||||
};
|
};
|
||||||
@@ -46,71 +48,12 @@ export class GrafanaCtrl {
|
|||||||
appEvents.emit(name, payload);
|
appEvents.emit(name, payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
$rootScope.colors = [
|
|
||||||
'#7EB26D',
|
|
||||||
'#EAB839',
|
|
||||||
'#6ED0E0',
|
|
||||||
'#EF843C',
|
|
||||||
'#E24D42',
|
|
||||||
'#1F78C1',
|
|
||||||
'#BA43A9',
|
|
||||||
'#705DA0',
|
|
||||||
'#508642',
|
|
||||||
'#CCA300',
|
|
||||||
'#447EBC',
|
|
||||||
'#C15C17',
|
|
||||||
'#890F02',
|
|
||||||
'#0A437C',
|
|
||||||
'#6D1F62',
|
|
||||||
'#584477',
|
|
||||||
'#B7DBAB',
|
|
||||||
'#F4D598',
|
|
||||||
'#70DBED',
|
|
||||||
'#F9BA8F',
|
|
||||||
'#F29191',
|
|
||||||
'#82B5D8',
|
|
||||||
'#E5A8E2',
|
|
||||||
'#AEA2E0',
|
|
||||||
'#629E51',
|
|
||||||
'#E5AC0E',
|
|
||||||
'#64B0C8',
|
|
||||||
'#E0752D',
|
|
||||||
'#BF1B00',
|
|
||||||
'#0A50A1',
|
|
||||||
'#962D82',
|
|
||||||
'#614D93',
|
|
||||||
'#9AC48A',
|
|
||||||
'#F2C96D',
|
|
||||||
'#65C5DB',
|
|
||||||
'#F9934E',
|
|
||||||
'#EA6460',
|
|
||||||
'#5195CE',
|
|
||||||
'#D683CE',
|
|
||||||
'#806EB7',
|
|
||||||
'#3F6833',
|
|
||||||
'#967302',
|
|
||||||
'#2F575E',
|
|
||||||
'#99440A',
|
|
||||||
'#58140C',
|
|
||||||
'#052B51',
|
|
||||||
'#511749',
|
|
||||||
'#3F2B5B',
|
|
||||||
'#E0F9D7',
|
|
||||||
'#FCEACA',
|
|
||||||
'#CFFAFF',
|
|
||||||
'#F9E2D2',
|
|
||||||
'#FCE2DE',
|
|
||||||
'#BADFF4',
|
|
||||||
'#F9D9F9',
|
|
||||||
'#DEDAF7',
|
|
||||||
];
|
|
||||||
|
|
||||||
$scope.init();
|
$scope.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope) {
|
export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope, $location) {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
controller: GrafanaCtrl,
|
controller: GrafanaCtrl,
|
||||||
|
|||||||
86
public/app/core/components/search/SearchResult.tsx
Normal file
86
public/app/core/components/search/SearchResult.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
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<SearchResultProps, any> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
search: store.search
|
||||||
|
};
|
||||||
|
|
||||||
|
store.search.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.state.search.sections.map(section => {
|
||||||
|
return <SearchResultSection section={section} key={section.id} />;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SectionProps {
|
||||||
|
section: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class SearchResultSection extends React.Component<SectionProps, any> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem(item) {
|
||||||
|
return (
|
||||||
|
<a className="search-item" href={item.url} key={item.id}>
|
||||||
|
<span className="search-item__icon">
|
||||||
|
<i className="fa fa-th-large" />
|
||||||
|
</span>
|
||||||
|
<span className="search-item__body">
|
||||||
|
<div className="search-item__body-title">{item.title}</div>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSection = () => {
|
||||||
|
this.props.section.toggle();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let collapseClassNames = classNames({
|
||||||
|
fa: true,
|
||||||
|
"fa-plus": !this.props.section.expanded,
|
||||||
|
"fa-minus": this.props.section.expanded,
|
||||||
|
"search-section__header__toggle": true
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="search-section" key={this.props.section.id}>
|
||||||
|
<div className="search-section__header">
|
||||||
|
<i
|
||||||
|
className={classNames(
|
||||||
|
"search-section__header__icon",
|
||||||
|
this.props.section.icon
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="search-section__header__text">
|
||||||
|
{this.props.section.title}
|
||||||
|
</span>
|
||||||
|
<i className={collapseClassNames} onClick={this.toggleSection} />
|
||||||
|
</div>
|
||||||
|
{this.props.section.expanded && (
|
||||||
|
<div className="search-section__items">
|
||||||
|
{this.props.section.items.map(this.renderItem)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,8 +16,10 @@ export class SideMenuCtrl {
|
|||||||
constructor(private $scope, private $rootScope, private $location, private contextSrv, private $timeout) {
|
constructor(private $scope, private $rootScope, private $location, private contextSrv, private $timeout) {
|
||||||
this.isSignedIn = contextSrv.isSignedIn;
|
this.isSignedIn = contextSrv.isSignedIn;
|
||||||
this.user = contextSrv.user;
|
this.user = contextSrv.user;
|
||||||
this.mainLinks = _.filter(config.bootData.navTree, item => !item.hideFromMenu);
|
|
||||||
this.bottomNav = _.filter(config.bootData.navTree, item => item.hideFromMenu);
|
let navTree = _.cloneDeep(config.bootData.navTree);
|
||||||
|
this.mainLinks = _.filter(navTree, item => !item.hideFromMenu);
|
||||||
|
this.bottomNav = _.filter(navTree, item => item.hideFromMenu);
|
||||||
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
|
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
|
||||||
|
|
||||||
if (contextSrv.user.orgCount > 1) {
|
if (contextSrv.user.orgCount > 1) {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import { queryPartEditorDirective } from './components/query_part/query_part_edi
|
|||||||
import { formDropdownDirective } from './components/form_dropdown/form_dropdown';
|
import { formDropdownDirective } from './components/form_dropdown/form_dropdown';
|
||||||
import 'app/core/controllers/all';
|
import 'app/core/controllers/all';
|
||||||
import 'app/core/services/all';
|
import 'app/core/services/all';
|
||||||
import 'app/core/routes/routes';
|
|
||||||
import './filters/filters';
|
import './filters/filters';
|
||||||
import coreModule from './core_module';
|
import coreModule from './core_module';
|
||||||
import appEvents from './app_events';
|
import appEvents from './app_events';
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
export class BundleLoader {
|
|
||||||
lazy: any;
|
|
||||||
|
|
||||||
constructor(bundleName) {
|
|
||||||
var defer = null;
|
|
||||||
|
|
||||||
this.lazy = [
|
|
||||||
'$q',
|
|
||||||
'$route',
|
|
||||||
'$rootScope',
|
|
||||||
($q, $route, $rootScope) => {
|
|
||||||
if (defer) {
|
|
||||||
return defer.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
defer = $q.defer();
|
|
||||||
|
|
||||||
System.import(bundleName).then(() => {
|
|
||||||
defer.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
return defer.promise;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,6 @@ define([
|
|||||||
'./segment_srv',
|
'./segment_srv',
|
||||||
'./backend_srv',
|
'./backend_srv',
|
||||||
'./dynamic_directive_srv',
|
'./dynamic_directive_srv',
|
||||||
'./global_event_srv'
|
'./bridge_srv'
|
||||||
],
|
],
|
||||||
function () {});
|
function () {});
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
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';
|
||||||
|
import { store } from 'app/stores/store';
|
||||||
|
import { reaction } from 'mobx';
|
||||||
|
|
||||||
// This service is for registering global events.
|
// Services that handles angular -> mobx store sync & other react <-> angular sync
|
||||||
// Good for communication react > angular and vice verse
|
export class BridgeSrv {
|
||||||
export class GlobalEventSrv {
|
|
||||||
private appSubUrl;
|
private appSubUrl;
|
||||||
private fullPageReloadRoutes;
|
private fullPageReloadRoutes;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $location, private $timeout, private $window) {
|
constructor(private $location, private $timeout, private $window, private $rootScope) {
|
||||||
this.appSubUrl = config.appSubUrl;
|
this.appSubUrl = config.appSubUrl;
|
||||||
this.fullPageReloadRoutes = ['/logout'];
|
this.fullPageReloadRoutes = ['/logout'];
|
||||||
}
|
}
|
||||||
@@ -25,6 +26,31 @@ export class GlobalEventSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
this.$rootScope.$on('$routeUpdate', (evt, data) => {
|
||||||
|
let angularUrl = this.$location.url();
|
||||||
|
if (store.view.currentUrl !== angularUrl) {
|
||||||
|
store.view.updatePathAndQuery(this.$location.path(), this.$location.search());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$rootScope.$on('$routeChangeSuccess', (evt, data) => {
|
||||||
|
let angularUrl = this.$location.url();
|
||||||
|
if (store.view.currentUrl !== angularUrl) {
|
||||||
|
store.view.updatePathAndQuery(this.$location.path(), this.$location.search());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reaction(
|
||||||
|
() => store.view.currentUrl,
|
||||||
|
currentUrl => {
|
||||||
|
let angularUrl = this.$location.url();
|
||||||
|
if (angularUrl !== currentUrl) {
|
||||||
|
this.$location.url(currentUrl);
|
||||||
|
console.log('store updating angular $location.url', currentUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
appEvents.on('location-change', payload => {
|
appEvents.on('location-change', payload => {
|
||||||
const urlWithoutBase = this.stripBaseFromUrl(payload.href);
|
const urlWithoutBase = this.stripBaseFromUrl(payload.href);
|
||||||
if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) {
|
if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) {
|
||||||
@@ -40,4 +66,4 @@ export class GlobalEventSrv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coreModule.service('globalEventSrv', GlobalEventSrv);
|
coreModule.service('bridgeSrv', BridgeSrv);
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { GlobalEventSrv } from 'app/core/services/global_event_srv';
|
import { BridgeSrv } from 'app/core/services/bridge_srv';
|
||||||
import { beforeEach } from 'test/lib/common';
|
|
||||||
|
|
||||||
jest.mock('app/core/config', () => {
|
jest.mock('app/core/config', () => {
|
||||||
return {
|
return {
|
||||||
@@ -7,11 +6,11 @@ jest.mock('app/core/config', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GlobalEventSrv', () => {
|
describe('BridgeSrv', () => {
|
||||||
let searchSrv;
|
let searchSrv;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
searchSrv = new GlobalEventSrv(null, null, null);
|
searchSrv = new BridgeSrv(null, null, null, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('With /subUrl as appSubUrl', () => {
|
describe('With /subUrl as appSubUrl', () => {
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import EventEmitter from 'eventemitter3';
|
import EventEmitter from 'eventemitter3';
|
||||||
|
|
||||||
export class Emitter {
|
export class Emitter {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { QueryPartDef, QueryPart } from 'app/core/components/query_part/query_part';
|
import { QueryPartDef, QueryPart } from 'app/core/components/query_part/query_part';
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
import { coreModule, appEvents } from 'app/core/core';
|
|
||||||
import alertDef from './alert_def';
|
|
||||||
|
|
||||||
export class AlertListCtrl {
|
|
||||||
alerts: any;
|
|
||||||
stateFilters = [
|
|
||||||
{ text: 'All', value: null },
|
|
||||||
{ text: 'OK', value: 'ok' },
|
|
||||||
{ text: 'Not OK', value: 'not_ok' },
|
|
||||||
{ text: 'Alerting', value: 'alerting' },
|
|
||||||
{ text: 'No Data', value: 'no_data' },
|
|
||||||
{ text: 'Paused', value: 'paused' },
|
|
||||||
];
|
|
||||||
filters = {
|
|
||||||
state: 'ALL',
|
|
||||||
};
|
|
||||||
navModel: any;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(private backendSrv, private $location, navModelSrv) {
|
|
||||||
this.navModel = navModelSrv.getNav('alerting', 'alert-list', 0);
|
|
||||||
|
|
||||||
var params = $location.search();
|
|
||||||
this.filters.state = params.state || null;
|
|
||||||
this.loadAlerts();
|
|
||||||
}
|
|
||||||
|
|
||||||
filtersChanged() {
|
|
||||||
this.$location.search(this.filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadAlerts() {
|
|
||||||
this.backendSrv.get('/api/alerts', this.filters).then(result => {
|
|
||||||
this.alerts = _.map(result, alert => {
|
|
||||||
alert.stateModel = alertDef.getStateDisplayModel(alert.state);
|
|
||||||
alert.newStateDateAgo = moment(alert.newStateDate)
|
|
||||||
.fromNow()
|
|
||||||
.replace(' ago', '');
|
|
||||||
if (alert.evalData && alert.evalData.no_data) {
|
|
||||||
alert.no_data = true;
|
|
||||||
}
|
|
||||||
return alert;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pauseAlertRule(alertId: any) {
|
|
||||||
var alert = _.find(this.alerts, { id: alertId });
|
|
||||||
|
|
||||||
var payload = {
|
|
||||||
paused: alert.state !== 'paused',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.backendSrv.post(`/api/alerts/${alert.id}/pause`, payload).then(result => {
|
|
||||||
alert.state = result.state;
|
|
||||||
alert.stateModel = alertDef.getStateDisplayModel(result.state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
openHowTo() {
|
|
||||||
appEvents.emit('show-modal', {
|
|
||||||
src: 'public/app/features/alerting/partials/alert_howto.html',
|
|
||||||
modalClass: 'confirm-modal',
|
|
||||||
model: {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.controller('AlertListCtrl', AlertListCtrl);
|
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
import './alert_list_ctrl';
|
|
||||||
import './notifications_list_ctrl';
|
import './notifications_list_ctrl';
|
||||||
import './notification_edit_ctrl';
|
import './notification_edit_ctrl';
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
<page-header model="ctrl.navModel"></page-header>
|
|
||||||
|
|
||||||
<div class="page-container page-body">
|
|
||||||
|
|
||||||
<div class="page-action-bar">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label">Filter by state</label>
|
|
||||||
<div class="gf-form-select-wrapper width-13">
|
|
||||||
<select class="gf-form-input" ng-model="ctrl.filters.state" ng-options="f.value as f.text for f in ctrl.stateFilters" ng-change="ctrl.filtersChanged()">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-action-bar__spacer">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a class="btn btn-secondary" ng-click="ctrl.openHowTo()">
|
|
||||||
<i class="fa fa-info-circle"></i>
|
|
||||||
How to add an alert
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="card-section card-list-layout-list">
|
|
||||||
|
|
||||||
<ol class="card-list" >
|
|
||||||
<li class="card-item-wrapper" ng-repeat="alert in ctrl.alerts">
|
|
||||||
<div class="card-item card-item--alert">
|
|
||||||
<div class="card-item-header">
|
|
||||||
<div class="card-item-type">
|
|
||||||
<a class="card-item-cog" bs-tooltip="'Pausing an alert rule prevents it from executing'" ng-click="ctrl.pauseAlertRule(alert.id)">
|
|
||||||
<i ng-show="alert.state !== 'paused'" class="fa fa-pause"></i>
|
|
||||||
<i ng-show="alert.state === 'paused'" class="fa fa-play"></i>
|
|
||||||
</a>
|
|
||||||
<a class="card-item-cog" href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert" bs-tooltip="'Edit alert rule'">
|
|
||||||
<i class="icon-gf icon-gf-settings"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-item-body">
|
|
||||||
<div class="card-item-details">
|
|
||||||
<div class="card-item-name">
|
|
||||||
<a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert">
|
|
||||||
{{alert.name}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="card-item-sub-name">
|
|
||||||
<span class="alert-list-item-state {{alert.stateModel.stateClass}}">
|
|
||||||
<i class="{{alert.stateModel.iconClass}}"></i>
|
|
||||||
{{alert.stateModel.text}} <span class="small muted" ng-show="alert.no_data">(due to no data)</span>
|
|
||||||
</span> for {{alert.newStateDateAgo}}
|
|
||||||
</div>
|
|
||||||
<div class="small muted" ng-show="alert.executionError !== ''">
|
|
||||||
Error: "{{alert.executionError}}"
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
@@ -8,5 +8,8 @@ define([
|
|||||||
'./playlist/all',
|
'./playlist/all',
|
||||||
'./snapshot/all',
|
'./snapshot/all',
|
||||||
'./panel/all',
|
'./panel/all',
|
||||||
|
'./org/all',
|
||||||
|
'./admin/admin',
|
||||||
|
'./alerting/all',
|
||||||
'./styleguide/styleguide',
|
'./styleguide/styleguide',
|
||||||
], function () {});
|
], function () {});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames';
|
||||||
import { PanelModel } from "../panel_model";
|
import { PanelModel } from '../panel_model';
|
||||||
import { PanelContainer } from "./PanelContainer";
|
import { PanelContainer } from './PanelContainer';
|
||||||
import templateSrv from "app/features/templating/template_srv";
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
import appEvents from "app/core/app_events";
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
export interface DashboardRowProps {
|
export interface DashboardRowProps {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@@ -18,7 +18,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
collapsed: this.props.panel.collapsed
|
collapsed: this.props.panel.collapsed,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.panelContainer = this.props.getPanelContainer();
|
this.panelContainer = this.props.getPanelContainer();
|
||||||
@@ -38,22 +38,22 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openSettings() {
|
openSettings() {
|
||||||
appEvents.emit("show-modal", {
|
appEvents.emit('show-modal', {
|
||||||
templateHtml: `<row-options row="model.row" on-updated="model.onUpdated()" dismiss="dismiss()"></row-options>`,
|
templateHtml: `<row-options row="model.row" on-updated="model.onUpdated()" dismiss="dismiss()"></row-options>`,
|
||||||
modalClass: "modal--narrow",
|
modalClass: 'modal--narrow',
|
||||||
model: {
|
model: {
|
||||||
row: this.props.panel,
|
row: this.props.panel,
|
||||||
onUpdated: this.forceUpdate.bind(this)
|
onUpdated: this.forceUpdate.bind(this),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
delete() {
|
||||||
appEvents.emit("confirm-modal", {
|
appEvents.emit('confirm-modal', {
|
||||||
title: "Delete Row",
|
title: 'Delete Row',
|
||||||
text: "Are you sure you want to remove this row and all its panels?",
|
text: 'Are you sure you want to remove this row and all its panels?',
|
||||||
altActionText: "Delete row only",
|
altActionText: 'Delete row only',
|
||||||
icon: "fa-trash",
|
icon: 'fa-trash',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
const panelContainer = this.props.getPanelContainer();
|
const panelContainer = this.props.getPanelContainer();
|
||||||
const dashboard = panelContainer.getDashboard();
|
const dashboard = panelContainer.getDashboard();
|
||||||
@@ -63,37 +63,30 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
const panelContainer = this.props.getPanelContainer();
|
const panelContainer = this.props.getPanelContainer();
|
||||||
const dashboard = panelContainer.getDashboard();
|
const dashboard = panelContainer.getDashboard();
|
||||||
dashboard.removeRow(this.props.panel, false);
|
dashboard.removeRow(this.props.panel, false);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
"dashboard-row": true,
|
'dashboard-row': true,
|
||||||
"dashboard-row--collapsed": this.state.collapsed
|
'dashboard-row--collapsed': this.state.collapsed,
|
||||||
});
|
});
|
||||||
const chevronClass = classNames({
|
const chevronClass = classNames({
|
||||||
fa: true,
|
fa: true,
|
||||||
"fa-chevron-down": !this.state.collapsed,
|
'fa-chevron-down': !this.state.collapsed,
|
||||||
"fa-chevron-right": this.state.collapsed
|
'fa-chevron-right': this.state.collapsed,
|
||||||
});
|
});
|
||||||
|
|
||||||
let title = templateSrv.replaceWithText(
|
let title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
|
||||||
this.props.panel.title,
|
const hiddenPanels = this.props.panel.panels ? this.props.panel.panels.length : 0;
|
||||||
this.props.panel.scopedVars
|
|
||||||
);
|
|
||||||
const hiddenPanels = this.props.panel.panels
|
|
||||||
? this.props.panel.panels.length
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<a className="dashboard-row__title pointer" onClick={this.toggle}>
|
<a className="dashboard-row__title pointer" onClick={this.toggle}>
|
||||||
<i className={chevronClass} />
|
<i className={chevronClass} />
|
||||||
{title}
|
{title}
|
||||||
<span className="dashboard-row__panel_count">
|
<span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span>
|
||||||
({hiddenPanels} hidden panels)
|
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
<div className="dashboard-row__actions">
|
<div className="dashboard-row__actions">
|
||||||
<a className="pointer" onClick={this.openSettings}>
|
<a className="pointer" onClick={this.openSettings}>
|
||||||
|
|||||||
@@ -14,14 +14,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane style-guide-icon-list">
|
|
||||||
<div class="row">
|
|
||||||
<div ng-repeat="icon in ctrl.icons" class="col-md-2 col-sm-3 col-xs-4">
|
|
||||||
<i class="icon-gf icon-gf-{{icon}}" bs-tooltip="'icon-gf icon-gf-{{icon}}'"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="page-heading">Forms</h3>
|
<h3 class="page-heading">Forms</h3>
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
|
|||||||
@@ -1,51 +1,17 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
class StyleGuideCtrl {
|
class StyleGuideCtrl {
|
||||||
colors: any = [];
|
|
||||||
theme: string;
|
theme: string;
|
||||||
buttonNames = ['primary', 'secondary', 'inverse', 'success', 'warning', 'danger'];
|
buttonNames = ['primary', 'secondary', 'inverse', 'success', 'warning', 'danger'];
|
||||||
buttonSizes = ['btn-small', '', 'btn-large'];
|
buttonSizes = ['btn-small', '', 'btn-large'];
|
||||||
buttonVariants = ['-'];
|
buttonVariants = ['-'];
|
||||||
icons: any = [];
|
|
||||||
page: any;
|
|
||||||
pages = ['colors', 'buttons', 'icons', 'plugins'];
|
|
||||||
navModel: any;
|
navModel: any;
|
||||||
|
|
||||||
/** @ngInject **/
|
/** @ngInject **/
|
||||||
constructor(private $http, private $routeParams, private backendSrv, navModelSrv) {
|
constructor(private $routeParams, private backendSrv, navModelSrv) {
|
||||||
this.navModel = navModelSrv.getNav('cfg', 'admin', 'styleguide', 1);
|
this.navModel = navModelSrv.getNav('cfg', 'admin', 'styleguide', 1);
|
||||||
this.theme = config.bootData.user.lightTheme ? 'light' : 'dark';
|
this.theme = config.bootData.user.lightTheme ? 'light' : 'dark';
|
||||||
this.page = {};
|
|
||||||
|
|
||||||
if ($routeParams.page) {
|
|
||||||
this.page[$routeParams.page] = 1;
|
|
||||||
} else {
|
|
||||||
this.page.colors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.page.colors) {
|
|
||||||
this.loadColors();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.page.icons) {
|
|
||||||
this.loadIcons();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadColors() {
|
|
||||||
this.$http.get('public/build/styleguide.json').then(res => {
|
|
||||||
this.colors = _.map(res.data[this.theme], (value, key) => {
|
|
||||||
return { name: key, value: value };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadIcons() {
|
|
||||||
this.$http.get('public/sass/icons.json').then(res => {
|
|
||||||
this.icons = res.data;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switchTheme() {
|
switchTheme() {
|
||||||
|
|||||||
33
public/app/routes/ReactContainer.tsx
Normal file
33
public/app/routes/ReactContainer.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
import { store } from 'app/stores/store';
|
||||||
|
import { Provider } from 'mobx-react';
|
||||||
|
|
||||||
|
function WrapInProvider(store, Component, props) {
|
||||||
|
return (
|
||||||
|
<Provider {...store}>
|
||||||
|
<Component {...props} />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
export function reactContainer($route, $location) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: '',
|
||||||
|
link(scope, elem) {
|
||||||
|
let component = $route.current.locals.component;
|
||||||
|
let props = {};
|
||||||
|
|
||||||
|
ReactDOM.render(WrapInProvider(store, component, props), elem[0]);
|
||||||
|
|
||||||
|
scope.$on('$destroy', function() {
|
||||||
|
ReactDOM.unmountComponentAtNode(elem[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.directive('reactContainer', reactContainer);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import coreModule from '../core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
export class LoadDashboardCtrl {
|
export class LoadDashboardCtrl {
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
@@ -1,43 +1,12 @@
|
|||||||
import './dashboard_loaders';
|
import './dashboard_loaders';
|
||||||
import coreModule from 'app/core/core_module';
|
import './ReactContainer';
|
||||||
|
import { ServerStats } from 'app/containers/ServerStats/ServerStats';
|
||||||
|
import { AlertRuleList } from 'app/containers/AlertRuleList/AlertRuleList';
|
||||||
|
|
||||||
/** @ngInject **/
|
/** @ngInject **/
|
||||||
function setupAngularRoutes($routeProvider, $locationProvider) {
|
export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||||
$locationProvider.html5Mode(true);
|
$locationProvider.html5Mode(true);
|
||||||
|
|
||||||
var loadOrgBundle = {
|
|
||||||
lazy: [
|
|
||||||
'$q',
|
|
||||||
'$route',
|
|
||||||
'$rootScope',
|
|
||||||
($q, $route, $rootScope) => {
|
|
||||||
return System.import('app/features/org/all');
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
var loadAdminBundle = {
|
|
||||||
lazy: [
|
|
||||||
'$q',
|
|
||||||
'$route',
|
|
||||||
'$rootScope',
|
|
||||||
($q, $route, $rootScope) => {
|
|
||||||
return System.import('app/features/admin/admin');
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
var loadAlertingBundle = {
|
|
||||||
lazy: [
|
|
||||||
'$q',
|
|
||||||
'$route',
|
|
||||||
'$rootScope',
|
|
||||||
($q, $route, $rootScope) => {
|
|
||||||
return System.import('app/features/alerting/all');
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
$routeProvider
|
$routeProvider
|
||||||
.when('/', {
|
.when('/', {
|
||||||
templateUrl: 'public/app/partials/dashboard.html',
|
templateUrl: 'public/app/partials/dashboard.html',
|
||||||
@@ -111,110 +80,92 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
.when('/org', {
|
.when('/org', {
|
||||||
templateUrl: 'public/app/features/org/partials/orgDetails.html',
|
templateUrl: 'public/app/features/org/partials/orgDetails.html',
|
||||||
controller: 'OrgDetailsCtrl',
|
controller: 'OrgDetailsCtrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
.when('/org/new', {
|
.when('/org/new', {
|
||||||
templateUrl: 'public/app/features/org/partials/newOrg.html',
|
templateUrl: 'public/app/features/org/partials/newOrg.html',
|
||||||
controller: 'NewOrgCtrl',
|
controller: 'NewOrgCtrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
.when('/org/users', {
|
.when('/org/users', {
|
||||||
templateUrl: 'public/app/features/org/partials/orgUsers.html',
|
templateUrl: 'public/app/features/org/partials/orgUsers.html',
|
||||||
controller: 'OrgUsersCtrl',
|
controller: 'OrgUsersCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
.when('/org/users/invite', {
|
.when('/org/users/invite', {
|
||||||
templateUrl: 'public/app/features/org/partials/invite.html',
|
templateUrl: 'public/app/features/org/partials/invite.html',
|
||||||
controller: 'UserInviteCtrl',
|
controller: 'UserInviteCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
.when('/org/apikeys', {
|
.when('/org/apikeys', {
|
||||||
templateUrl: 'public/app/features/org/partials/orgApiKeys.html',
|
templateUrl: 'public/app/features/org/partials/orgApiKeys.html',
|
||||||
controller: 'OrgApiKeysCtrl',
|
controller: 'OrgApiKeysCtrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
.when('/org/teams', {
|
.when('/org/teams', {
|
||||||
templateUrl: 'public/app/features/org/partials/teams.html',
|
templateUrl: 'public/app/features/org/partials/teams.html',
|
||||||
controller: 'TeamsCtrl',
|
controller: 'TeamsCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
.when('/org/teams/new', {
|
.when('/org/teams/new', {
|
||||||
templateUrl: 'public/app/features/org/partials/create_team.html',
|
templateUrl: 'public/app/features/org/partials/create_team.html',
|
||||||
controller: 'CreateTeamCtrl',
|
controller: 'CreateTeamCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
.when('/org/teams/edit/:id', {
|
.when('/org/teams/edit/:id', {
|
||||||
templateUrl: 'public/app/features/org/partials/team_details.html',
|
templateUrl: 'public/app/features/org/partials/team_details.html',
|
||||||
controller: 'TeamDetailsCtrl',
|
controller: 'TeamDetailsCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
.when('/profile', {
|
.when('/profile', {
|
||||||
templateUrl: 'public/app/features/org/partials/profile.html',
|
templateUrl: 'public/app/features/org/partials/profile.html',
|
||||||
controller: 'ProfileCtrl',
|
controller: 'ProfileCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
.when('/profile/password', {
|
.when('/profile/password', {
|
||||||
templateUrl: 'public/app/features/org/partials/change_password.html',
|
templateUrl: 'public/app/features/org/partials/change_password.html',
|
||||||
controller: 'ChangePasswordCtrl',
|
controller: 'ChangePasswordCtrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
.when('/profile/select-org', {
|
.when('/profile/select-org', {
|
||||||
templateUrl: 'public/app/features/org/partials/select_org.html',
|
templateUrl: 'public/app/features/org/partials/select_org.html',
|
||||||
controller: 'SelectOrgCtrl',
|
controller: 'SelectOrgCtrl',
|
||||||
resolve: loadOrgBundle,
|
|
||||||
})
|
})
|
||||||
// ADMIN
|
// ADMIN
|
||||||
.when('/admin', {
|
.when('/admin', {
|
||||||
templateUrl: 'public/app/features/admin/partials/admin_home.html',
|
templateUrl: 'public/app/features/admin/partials/admin_home.html',
|
||||||
controller: 'AdminHomeCtrl',
|
controller: 'AdminHomeCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadAdminBundle,
|
|
||||||
})
|
})
|
||||||
.when('/admin/settings', {
|
.when('/admin/settings', {
|
||||||
templateUrl: 'public/app/features/admin/partials/settings.html',
|
templateUrl: 'public/app/features/admin/partials/settings.html',
|
||||||
controller: 'AdminSettingsCtrl',
|
controller: 'AdminSettingsCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadAdminBundle,
|
|
||||||
})
|
})
|
||||||
.when('/admin/users', {
|
.when('/admin/users', {
|
||||||
templateUrl: 'public/app/features/admin/partials/users.html',
|
templateUrl: 'public/app/features/admin/partials/users.html',
|
||||||
controller: 'AdminListUsersCtrl',
|
controller: 'AdminListUsersCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadAdminBundle,
|
|
||||||
})
|
})
|
||||||
.when('/admin/users/create', {
|
.when('/admin/users/create', {
|
||||||
templateUrl: 'public/app/features/admin/partials/new_user.html',
|
templateUrl: 'public/app/features/admin/partials/new_user.html',
|
||||||
controller: 'AdminEditUserCtrl',
|
controller: 'AdminEditUserCtrl',
|
||||||
resolve: loadAdminBundle,
|
|
||||||
})
|
})
|
||||||
.when('/admin/users/edit/:id', {
|
.when('/admin/users/edit/:id', {
|
||||||
templateUrl: 'public/app/features/admin/partials/edit_user.html',
|
templateUrl: 'public/app/features/admin/partials/edit_user.html',
|
||||||
controller: 'AdminEditUserCtrl',
|
controller: 'AdminEditUserCtrl',
|
||||||
resolve: loadAdminBundle,
|
|
||||||
})
|
})
|
||||||
.when('/admin/orgs', {
|
.when('/admin/orgs', {
|
||||||
templateUrl: 'public/app/features/admin/partials/orgs.html',
|
templateUrl: 'public/app/features/admin/partials/orgs.html',
|
||||||
controller: 'AdminListOrgsCtrl',
|
controller: 'AdminListOrgsCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadAdminBundle,
|
|
||||||
})
|
})
|
||||||
.when('/admin/orgs/edit/:id', {
|
.when('/admin/orgs/edit/:id', {
|
||||||
templateUrl: 'public/app/features/admin/partials/edit_org.html',
|
templateUrl: 'public/app/features/admin/partials/edit_org.html',
|
||||||
controller: 'AdminEditOrgCtrl',
|
controller: 'AdminEditOrgCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadAdminBundle,
|
|
||||||
})
|
})
|
||||||
.when('/admin/stats', {
|
.when('/admin/stats', {
|
||||||
templateUrl: 'public/app/features/admin/partials/stats.html',
|
template: '<react-container />',
|
||||||
controller: 'AdminStatsCtrl',
|
resolve: {
|
||||||
controllerAs: 'ctrl',
|
component: () => ServerStats,
|
||||||
resolve: loadAdminBundle,
|
},
|
||||||
})
|
})
|
||||||
// LOGIN / SIGNUP
|
// LOGIN / SIGNUP
|
||||||
.when('/login', {
|
.when('/login', {
|
||||||
@@ -274,30 +225,31 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
templateUrl: 'public/app/features/alerting/partials/alert_list.html',
|
templateUrl: 'public/app/features/alerting/partials/alert_list.html',
|
||||||
controller: 'AlertListCtrl',
|
controller: 'AlertListCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadAlertingBundle,
|
})
|
||||||
|
.when('/alerting/list', {
|
||||||
|
template: '<react-container />',
|
||||||
|
reloadOnSearch: false,
|
||||||
|
resolve: {
|
||||||
|
component: () => AlertRuleList,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.when('/alerting/notifications', {
|
.when('/alerting/notifications', {
|
||||||
templateUrl: 'public/app/features/alerting/partials/notifications_list.html',
|
templateUrl: 'public/app/features/alerting/partials/notifications_list.html',
|
||||||
controller: 'AlertNotificationsListCtrl',
|
controller: 'AlertNotificationsListCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadAlertingBundle,
|
|
||||||
})
|
})
|
||||||
.when('/alerting/notification/new', {
|
.when('/alerting/notification/new', {
|
||||||
templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
|
templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
|
||||||
controller: 'AlertNotificationEditCtrl',
|
controller: 'AlertNotificationEditCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadAlertingBundle,
|
|
||||||
})
|
})
|
||||||
.when('/alerting/notification/:id/edit', {
|
.when('/alerting/notification/:id/edit', {
|
||||||
templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
|
templateUrl: 'public/app/features/alerting/partials/notification_edit.html',
|
||||||
controller: 'AlertNotificationEditCtrl',
|
controller: 'AlertNotificationEditCtrl',
|
||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
resolve: loadAlertingBundle,
|
|
||||||
})
|
})
|
||||||
.otherwise({
|
.otherwise({
|
||||||
templateUrl: 'public/app/partials/error.html',
|
templateUrl: 'public/app/partials/error.html',
|
||||||
controller: 'ErrorCtrl',
|
controller: 'ErrorCtrl',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
coreModule.config(setupAngularRoutes);
|
|
||||||
34
public/app/stores/AlertListStore/AlertListStore.ts
Normal file
34
public/app/stores/AlertListStore/AlertListStore.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { types, getEnv, flow } from 'mobx-state-tree';
|
||||||
|
import { AlertRule } from './AlertRule';
|
||||||
|
import { setStateFields } from './helpers';
|
||||||
|
|
||||||
|
type IAlertRuleType = typeof AlertRule.Type;
|
||||||
|
export interface IAlertRule extends IAlertRuleType {}
|
||||||
|
|
||||||
|
export const AlertListStore = types
|
||||||
|
.model('AlertListStore', {
|
||||||
|
rules: types.array(AlertRule),
|
||||||
|
stateFilter: types.optional(types.string, 'all'),
|
||||||
|
})
|
||||||
|
.actions(self => ({
|
||||||
|
loadRules: flow(function* load(filters) {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
self.stateFilter = filters.state; // store state filter used in api query
|
||||||
|
const apiRules = yield backendSrv.get('/api/alerts', filters);
|
||||||
|
self.rules.clear();
|
||||||
|
|
||||||
|
for (let rule of apiRules) {
|
||||||
|
setStateFields(rule, rule.state);
|
||||||
|
|
||||||
|
if (rule.executionError) {
|
||||||
|
rule.info = 'Execution Error: ' + rule.executionError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.evalData && rule.evalData.noData) {
|
||||||
|
rule.info = 'Query returned no data';
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rules.push(AlertRule.create(rule));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
34
public/app/stores/AlertListStore/AlertRule.ts
Normal file
34
public/app/stores/AlertListStore/AlertRule.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { types, getEnv, flow } from 'mobx-state-tree';
|
||||||
|
import { setStateFields } from './helpers';
|
||||||
|
|
||||||
|
export const AlertRule = types
|
||||||
|
.model('AlertRule', {
|
||||||
|
id: types.identifier(types.number),
|
||||||
|
dashboardId: types.number,
|
||||||
|
panelId: types.number,
|
||||||
|
name: types.string,
|
||||||
|
state: types.string,
|
||||||
|
stateText: types.string,
|
||||||
|
stateIcon: types.string,
|
||||||
|
stateClass: types.string,
|
||||||
|
stateAge: types.string,
|
||||||
|
info: types.optional(types.string, ''),
|
||||||
|
dashboardUri: types.string,
|
||||||
|
})
|
||||||
|
.views(self => ({
|
||||||
|
get isPaused() {
|
||||||
|
return self.state === 'paused';
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.actions(self => ({
|
||||||
|
/**
|
||||||
|
* will toggle alert rule paused state
|
||||||
|
*/
|
||||||
|
togglePaused: flow(function* togglePaused() {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
const payload = { paused: self.isPaused };
|
||||||
|
const res = yield backendSrv.post(`/api/alerts/${self.id}/pause`, payload);
|
||||||
|
setStateFields(self, res.state);
|
||||||
|
self.info = '';
|
||||||
|
}),
|
||||||
|
}));
|
||||||
13
public/app/stores/AlertListStore/helpers.ts
Normal file
13
public/app/stores/AlertListStore/helpers.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import alertDef from 'app/features/alerting/alert_def';
|
||||||
|
|
||||||
|
export function setStateFields(rule, state) {
|
||||||
|
const stateModel = alertDef.getStateDisplayModel(state);
|
||||||
|
rule.state = state;
|
||||||
|
rule.stateText = stateModel.text;
|
||||||
|
rule.stateIcon = stateModel.iconClass;
|
||||||
|
rule.stateClass = stateModel.stateClass;
|
||||||
|
rule.stateAge = moment(rule.newStateDate)
|
||||||
|
.fromNow()
|
||||||
|
.replace(' ago', '');
|
||||||
|
}
|
||||||
12
public/app/stores/NavStore/NavItem.ts
Normal file
12
public/app/stores/NavStore/NavItem.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { types } from 'mobx-state-tree';
|
||||||
|
|
||||||
|
export const NavItem = types.model('NavItem', {
|
||||||
|
id: types.identifier(types.string),
|
||||||
|
text: types.string,
|
||||||
|
url: types.optional(types.string, ''),
|
||||||
|
subTitle: types.optional(types.string, ''),
|
||||||
|
icon: types.optional(types.string, ''),
|
||||||
|
img: types.optional(types.string, ''),
|
||||||
|
active: types.optional(types.boolean, false),
|
||||||
|
children: types.optional(types.array(types.late(() => NavItem)), []),
|
||||||
|
});
|
||||||
41
public/app/stores/NavStore/NavStore.ts
Normal file
41
public/app/stores/NavStore/NavStore.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { types, getEnv } from 'mobx-state-tree';
|
||||||
|
import { NavItem } from './NavItem';
|
||||||
|
|
||||||
|
export const NavStore = types
|
||||||
|
.model('NavStore', {
|
||||||
|
main: types.maybe(NavItem),
|
||||||
|
node: types.maybe(NavItem),
|
||||||
|
})
|
||||||
|
.actions(self => ({
|
||||||
|
load(...args) {
|
||||||
|
let children = getEnv(self).navTree;
|
||||||
|
let main, node;
|
||||||
|
let parents = [];
|
||||||
|
|
||||||
|
for (let id of args) {
|
||||||
|
node = children.find(el => el.id === id);
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
throw new Error(`NavItem with id ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
children = node.children;
|
||||||
|
parents.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
main = parents[parents.length - 2];
|
||||||
|
|
||||||
|
if (main.children) {
|
||||||
|
for (let item of main.children) {
|
||||||
|
item.active = false;
|
||||||
|
|
||||||
|
if (item.url === node.url) {
|
||||||
|
item.active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.main = NavItem.create(main);
|
||||||
|
self.node = NavItem.create(node);
|
||||||
|
},
|
||||||
|
}));
|
||||||
26
public/app/stores/RootStore/RootStore.ts
Normal file
26
public/app/stores/RootStore/RootStore.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { types } from 'mobx-state-tree';
|
||||||
|
import { SearchStore } from './../SearchStore/SearchStore';
|
||||||
|
import { ServerStatsStore } from './../ServerStatsStore/ServerStatsStore';
|
||||||
|
import { NavStore } from './../NavStore/NavStore';
|
||||||
|
import { AlertListStore } from './../AlertListStore/AlertListStore';
|
||||||
|
import { ViewStore } from './../ViewStore/ViewStore';
|
||||||
|
|
||||||
|
export const RootStore = types.model({
|
||||||
|
search: types.optional(SearchStore, {
|
||||||
|
sections: [],
|
||||||
|
}),
|
||||||
|
serverStats: types.optional(ServerStatsStore, {
|
||||||
|
stats: [],
|
||||||
|
}),
|
||||||
|
nav: types.optional(NavStore, {}),
|
||||||
|
alertList: types.optional(AlertListStore, {
|
||||||
|
rules: [],
|
||||||
|
}),
|
||||||
|
view: types.optional(ViewStore, {
|
||||||
|
path: '',
|
||||||
|
query: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type IRootStoreType = typeof RootStore.Type;
|
||||||
|
export interface IRootStore extends IRootStoreType {}
|
||||||
10
public/app/stores/SearchStore/ResultItem.ts
Normal file
10
public/app/stores/SearchStore/ResultItem.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { types } from 'mobx-state-tree';
|
||||||
|
|
||||||
|
export const ResultItem = types.model('ResultItem', {
|
||||||
|
id: types.identifier(types.number),
|
||||||
|
folderId: types.optional(types.number, 0),
|
||||||
|
title: types.string,
|
||||||
|
url: types.string,
|
||||||
|
icon: types.string,
|
||||||
|
folderTitle: types.optional(types.string, ''),
|
||||||
|
});
|
||||||
27
public/app/stores/SearchStore/SearchResultSection.ts
Normal file
27
public/app/stores/SearchStore/SearchResultSection.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { types } from 'mobx-state-tree';
|
||||||
|
import { ResultItem } from './ResultItem';
|
||||||
|
|
||||||
|
export const SearchResultSection = types
|
||||||
|
.model('SearchResultSection', {
|
||||||
|
id: types.identifier(),
|
||||||
|
title: types.string,
|
||||||
|
icon: types.string,
|
||||||
|
expanded: types.boolean,
|
||||||
|
items: types.array(ResultItem),
|
||||||
|
})
|
||||||
|
.actions(self => ({
|
||||||
|
toggle() {
|
||||||
|
self.expanded = !self.expanded;
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
self.items.push(
|
||||||
|
ResultItem.create({
|
||||||
|
id: i,
|
||||||
|
title: 'Dashboard ' + self.items.length,
|
||||||
|
icon: 'gicon gicon-dashboard',
|
||||||
|
url: 'asd',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
22
public/app/stores/SearchStore/SearchStore.ts
Normal file
22
public/app/stores/SearchStore/SearchStore.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { types } from 'mobx-state-tree';
|
||||||
|
import { SearchResultSection } from './SearchResultSection';
|
||||||
|
|
||||||
|
export const SearchStore = types
|
||||||
|
.model('SearchStore', {
|
||||||
|
sections: types.array(SearchResultSection),
|
||||||
|
})
|
||||||
|
.actions(self => ({
|
||||||
|
query() {
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
self.sections.push(
|
||||||
|
SearchResultSection.create({
|
||||||
|
id: 'starred' + i,
|
||||||
|
title: 'starred',
|
||||||
|
icon: 'fa fa-fw fa-star-o',
|
||||||
|
expanded: false,
|
||||||
|
items: [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
6
public/app/stores/ServerStatsStore/ServerStat.ts
Normal file
6
public/app/stores/ServerStatsStore/ServerStat.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { types } from 'mobx-state-tree';
|
||||||
|
|
||||||
|
export const ServerStat = types.model('ServerStat', {
|
||||||
|
name: types.string,
|
||||||
|
value: types.optional(types.number, 0),
|
||||||
|
});
|
||||||
24
public/app/stores/ServerStatsStore/ServerStatsStore.ts
Normal file
24
public/app/stores/ServerStatsStore/ServerStatsStore.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { types, getEnv, flow } from 'mobx-state-tree';
|
||||||
|
import { ServerStat } from './ServerStat';
|
||||||
|
|
||||||
|
export const ServerStatsStore = types
|
||||||
|
.model('ServerStatsStore', {
|
||||||
|
stats: types.array(ServerStat),
|
||||||
|
error: types.optional(types.string, ''),
|
||||||
|
})
|
||||||
|
.actions(self => ({
|
||||||
|
load: flow(function* load() {
|
||||||
|
const backendSrv = getEnv(self).backendSrv;
|
||||||
|
const res = yield backendSrv.get('/api/admin/stats');
|
||||||
|
self.stats.clear();
|
||||||
|
self.stats.push(ServerStat.create({ name: 'Total dashboards', value: res.dashboards }));
|
||||||
|
self.stats.push(ServerStat.create({ name: 'Total users', value: res.users }));
|
||||||
|
self.stats.push(ServerStat.create({ name: 'Active users (seen last 30 days)', value: res.activeUsers }));
|
||||||
|
self.stats.push(ServerStat.create({ name: 'Total orgs', value: res.orgs }));
|
||||||
|
self.stats.push(ServerStat.create({ name: 'Total playlists', value: res.playlists }));
|
||||||
|
self.stats.push(ServerStat.create({ name: 'Total snapshots', value: res.snapshots }));
|
||||||
|
self.stats.push(ServerStat.create({ name: 'Total dashboard tags', value: res.tags }));
|
||||||
|
self.stats.push(ServerStat.create({ name: 'Total starred dashboards', value: res.stars }));
|
||||||
|
self.stats.push(ServerStat.create({ name: 'Total alerts', value: res.alerts }));
|
||||||
|
}),
|
||||||
|
}));
|
||||||
46
public/app/stores/ViewStore/ViewStore.ts
Normal file
46
public/app/stores/ViewStore/ViewStore.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { types } from 'mobx-state-tree';
|
||||||
|
|
||||||
|
const QueryValueType = types.union(types.string, types.boolean, types.number);
|
||||||
|
const urlParameterize = queryObj => {
|
||||||
|
const keys = Object.keys(queryObj);
|
||||||
|
const newQuery = keys.reduce((acc: string, key: string, idx: number) => {
|
||||||
|
const preChar = idx === 0 ? '?' : '&';
|
||||||
|
return acc + preChar + key + '=' + queryObj[key];
|
||||||
|
}, '');
|
||||||
|
|
||||||
|
return newQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ViewStore = types
|
||||||
|
.model({
|
||||||
|
path: types.string,
|
||||||
|
query: types.map(QueryValueType),
|
||||||
|
})
|
||||||
|
.views(self => ({
|
||||||
|
get currentUrl() {
|
||||||
|
let path = self.path;
|
||||||
|
|
||||||
|
if (self.query.size) {
|
||||||
|
path += urlParameterize(self.query.toJS());
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.actions(self => {
|
||||||
|
function updateQuery(query: any) {
|
||||||
|
self.query.clear();
|
||||||
|
for (let key of Object.keys(query)) {
|
||||||
|
self.query.set(key, query[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePathAndQuery(path: string, query: any) {
|
||||||
|
self.path = path;
|
||||||
|
updateQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateQuery,
|
||||||
|
updatePathAndQuery,
|
||||||
|
};
|
||||||
|
});
|
||||||
16
public/app/stores/store.ts
Normal file
16
public/app/stores/store.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { RootStore, IRootStore } from './RootStore/RootStore';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
|
export let store: IRootStore;
|
||||||
|
|
||||||
|
export function createStore(backendSrv) {
|
||||||
|
store = RootStore.create(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
backendSrv: backendSrv,
|
||||||
|
navTree: config.bootData.navTree,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
@@ -217,7 +217,7 @@ $input-border: 1px solid $input-border-color;
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
font: normal normal normal $font-size-sm/1 FontAwesome;
|
font: normal normal normal $font-size-sm/1 FontAwesome;
|
||||||
content: "\f0d7";
|
content: '\f0d7';
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,7 +270,7 @@ $input-border: 1px solid $input-border-color;
|
|||||||
|
|
||||||
select.gf-form-input {
|
select.gf-form-input {
|
||||||
text-indent: 0.01px;
|
text-indent: 0.01px;
|
||||||
text-overflow: "";
|
text-overflow: '';
|
||||||
padding-right: $input-padding-x*3;
|
padding-right: $input-padding-x*3;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
||||||
@@ -292,7 +292,7 @@ $input-border: 1px solid $input-border-color;
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
font: normal normal normal $font-size-sm/1 FontAwesome;
|
font: normal normal normal $font-size-sm/1 FontAwesome;
|
||||||
content: "\f0d7";
|
content: '\f0d7';
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
@@ -319,7 +319,7 @@ $input-border: 1px solid $input-border-color;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.natural-language-input {
|
.natural-language-input {
|
||||||
&input[type="number"] {
|
&input[type='number'] {
|
||||||
font-size: $font-size-base;
|
font-size: $font-size-base;
|
||||||
line-height: $input-line-height;
|
line-height: $input-line-height;
|
||||||
margin: -6px -5px 0 5px;
|
margin: -6px -5px 0 5px;
|
||||||
@@ -342,7 +342,7 @@ $input-border: 1px solid $input-border-color;
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: $input-color;
|
color: $input-color;
|
||||||
font: normal normal normal $font-size-sm/1 FontAwesome;
|
font: normal normal normal $font-size-sm/1 FontAwesome;
|
||||||
content: "\f0d7";
|
content: '\f0d7';
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: -14px; // half of square's length
|
right: -14px; // half of square's length
|
||||||
@@ -157,8 +157,7 @@
|
|||||||
background: linear-gradient(135deg, $btn-inverse-bg, $btn-inverse-bg-hl);
|
background: linear-gradient(135deg, $btn-inverse-bg, $btn-inverse-bg-hl);
|
||||||
|
|
||||||
// stylish arrow design using box shadow
|
// stylish arrow design using box shadow
|
||||||
box-shadow: 2px -2px 0 2px rgb(35, 31, 31),
|
box-shadow: 2px -2px 0 2px rgb(35, 31, 31), 3px -3px 0 2px rgba(255, 255, 255, 0.1);
|
||||||
3px -3px 0 2px rgba(255, 255, 255, 0.1);
|
|
||||||
|
|
||||||
// 5px - for rounded arrows and
|
// 5px - for rounded arrows and
|
||||||
// 50px - to prevent hover glitches on the border created using shadows*/
|
// 50px - to prevent hover glitches on the border created using shadows*/
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Alert List
|
// Alert List
|
||||||
|
|
||||||
.alert-list {
|
.alert-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -94,7 +93,7 @@
|
|||||||
|
|
||||||
.panel-has-alert {
|
.panel-has-alert {
|
||||||
.panel-alert-icon:before {
|
.panel-alert-icon:before {
|
||||||
content: "\e611";
|
content: '\e611';
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
left: -3px;
|
left: -3px;
|
||||||
@@ -108,12 +107,12 @@
|
|||||||
|
|
||||||
.panel-alert-icon:before {
|
.panel-alert-icon:before {
|
||||||
color: $critical;
|
color: $critical;
|
||||||
content: "\e610";
|
content: '\e610';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--alerting::after {
|
&--alerting::after {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
@@ -121,14 +120,13 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
box-shadow: 0 0 10px rgba($critical, 1);
|
box-shadow: 0 0 10px rgba($critical, 1);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: alerting-panel 1.6s cubic-bezier(1, 0.1, 0.73, 1) 0s infinite
|
animation: alerting-panel 1.6s cubic-bezier(1, 0.1, 0.73, 1) 0s infinite alternate;
|
||||||
alternate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--ok {
|
&--ok {
|
||||||
.panel-alert-icon:before {
|
.panel-alert-icon:before {
|
||||||
color: $online;
|
color: $online;
|
||||||
content: "\e611";
|
content: '\e611';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
public/test/mocks/common.ts
Normal file
15
public/test/mocks/common.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const backendSrv = {
|
||||||
|
get: jest.fn(),
|
||||||
|
post: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createNavTree(...args) {
|
||||||
|
let root = [];
|
||||||
|
let node = root;
|
||||||
|
for (let arg of args) {
|
||||||
|
let child = { id: arg, url: `/url/${arg}`, text: `${arg}-Text`, children: [] };
|
||||||
|
node.push(child);
|
||||||
|
node = child.children;
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"noEmitOnError": false,
|
"noEmitOnError": false,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"experimentalDecorators": false,
|
"experimentalDecorators": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noImplicitThis": false,
|
"noImplicitThis": false,
|
||||||
"noImplicitUseStrict":false,
|
"noImplicitUseStrict":false,
|
||||||
|
|||||||
Reference in New Issue
Block a user