Commit e052e165 by Torkel Ödegaard

Merge branch 'react-panels' of github.com:grafana/grafana into react-panels

parents 4fd21070 51f8d3ca
......@@ -7,9 +7,20 @@ export interface BuildInfo {
env: string;
}
export interface PanelPlugin {
id: string;
name: string;
meta: any;
hideFromList: boolean;
module: string;
baseUrl: string;
info: any;
sort: number;
}
export class Settings {
datasources: any;
panels: any;
panels: PanelPlugin[];
appSubUrl: string;
window_title_prefix: string;
buildInfo: BuildInfo;
......
......@@ -139,7 +139,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
}
onViewModeChanged(payload) {
this.setState({ animated: payload.fullscreen });
this.setState({ animated: !payload.fullscreen });
}
updateGridPos(item, layout) {
......
......@@ -5,24 +5,27 @@ import { DashboardModel } from '../dashboard_model';
import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader';
import { DashboardRow } from './DashboardRow';
import { AddPanelPanel } from './AddPanelPanel';
import { importPluginModule } from 'app/features/plugins/plugin_loader';
import { importPluginModule, PluginExports } from 'app/features/plugins/plugin_loader';
import { PanelChrome } from './PanelChrome';
export interface DashboardPanelProps {
export interface Props {
panel: PanelModel;
dashboard: DashboardModel;
}
export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
export interface State {
pluginExports: PluginExports;
}
export class DashboardPanel extends React.Component<Props, State> {
element: any;
angularPanel: AngularComponent;
pluginInfo: any;
pluginExports: any;
specialPanels = {};
constructor(props) {
super(props);
this.state = {};
this.state = { pluginExports: null };
this.specialPanels['row'] = this.renderRow.bind(this);
this.specialPanels['add-panel'] = this.renderAddPanel.bind(this);
......@@ -32,8 +35,7 @@ export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
// load panel plugin
importPluginModule(this.pluginInfo.module).then(pluginExports => {
this.pluginExports = pluginExports;
this.forceUpdate();
this.setState({ pluginExports: pluginExports });
});
}
}
......@@ -51,8 +53,7 @@ export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
}
componentDidUpdate() {
// skip loading angular component if we have no element
// or we have already loaded it
// skip loading angular component if we have no element or we have already loaded it
if (!this.element || this.angularPanel) {
return;
}
......@@ -70,18 +71,20 @@ export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
}
render() {
const { pluginExports } = this.state;
if (this.isSpecial()) {
return this.specialPanels[this.props.panel.type]();
}
if (!this.pluginExports) {
if (!pluginExports) {
return null;
}
if (this.pluginExports.PanelComponent) {
if (pluginExports.PanelComponent) {
return (
<PanelChrome
component={this.pluginExports.PanelComponent}
component={pluginExports.PanelComponent}
panel={this.props.panel}
dashboard={this.props.dashboard}
/>
......
import React, { Component, ComponentClass } from 'react';
export interface OuterProps {
type: string;
queries: any[];
isVisible: boolean;
}
export interface PanelProps extends OuterProps {
data: any[];
}
export interface DataPanel extends ComponentClass<OuterProps> {
}
interface State {
isLoading: boolean;
data: any[];
}
export const DataPanelWrapper = (ComposedComponent: ComponentClass<PanelProps>) => {
class Wrapper extends Component<OuterProps, State> {
public static defaultProps = {
isVisible: true,
};
constructor(props: OuterProps) {
super(props);
this.state = {
isLoading: false,
data: [],
};
}
public componentDidMount() {
console.log('data panel mount');
this.issueQueries();
}
public issueQueries = async () => {
const { isVisible } = this.props;
if (!isVisible) {
return;
}
this.setState({ isLoading: true });
await new Promise(resolve => {
setTimeout(() => {
this.setState({ isLoading: false, data: [{value: 10}] });
}, 500);
});
};
public render() {
const { data, isLoading } = this.state;
console.log('data panel render');
if (!data.length) {
return (
<div className="no-data">
<p>No Data</p>
</div>
);
}
if (isLoading) {
return (
<div className="loading">
<p>Loading</p>
</div>
);
}
return <ComposedComponent {...this.props} data={data} />;
}
}
return Wrapper;
};
import React from 'react';
import React, { ComponentClass } from 'react';
import $ from 'jquery';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
import { PanelHeader } from './PanelHeader';
import { PanelEditor } from './PanelEditor';
import { DataPanel, PanelProps, DataPanelWrapper } from './DataPanel';
const TITLE_HEIGHT = 27;
const PANEL_BORDER = 2;
export interface PanelChromeProps {
export interface Props {
panel: PanelModel;
dashboard: DashboardModel;
component: any;
component: ComponentClass<PanelProps>;
}
export class PanelChrome extends React.Component<PanelChromeProps, any> {
interface State {
height: number;
}
export class PanelChrome extends React.Component<Props, State> {
panelComponent: DataPanel;
constructor(props) {
super(props);
this.props.panel.events.on('panel-size-changed', this.triggerForceUpdate.bind(this));
}
this.state = {
height: this.getPanelHeight(),
};
triggerForceUpdate() {
this.forceUpdate();
this.panelComponent = DataPanelWrapper(this.props.component);
this.props.panel.events.on('panel-size-changed', this.onPanelSizeChanged);
}
render() {
let panelContentStyle = {
onPanelSizeChanged = () => {
this.setState({
height: this.getPanelHeight(),
};
});
};
let PanelComponent = this.props.component;
componentDidMount() {
console.log('panel chrome mounted');
}
render() {
let PanelComponent = this.panelComponent;
return (
<div className="panel-height-helper">
<div className="panel-editor-container">
<div className="panel-container">
<PanelHeader panel={this.props.panel} dashboard={this.props.dashboard} />
<div className="panel-content" style={panelContentStyle}>
{<PanelComponent />}
<div className="panel-content" style={{ height: this.state.height }}>
{<PanelComponent type={'test'} queries={[]} isVisible={true} />}
</div>
</div>
{this.props.panel.isEditing && <PanelEditor panel={this.props.panel} dashboard={this.props.dashboard} />}
......@@ -59,6 +73,6 @@ export class PanelChrome extends React.Component<PanelChromeProps, any> {
height = panel.gridPos.h * GRID_CELL_HEIGHT + (panel.gridPos.h - 1) * GRID_CELL_VMARGIN;
}
return height - PANEL_BORDER + TITLE_HEIGHT;
return height - (PANEL_BORDER + TITLE_HEIGHT);
}
}
import React from 'react';
import classNames from 'classnames';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader';
import { store } from 'app/stores/store';
import { observer } from 'mobx-react';
import { QueriesTab } from './QueriesTab';
import { PanelPlugin } from 'app/core/config';
import { VizTypePicker } from './VizTypePicker';
interface PanelEditorProps {
panel: PanelModel;
dashboard: DashboardModel;
}
interface PanelEditorTab {
id: string;
text: string;
icon: string;
}
@observer
export class PanelEditor extends React.Component<PanelEditorProps, any> {
queryElement: any;
queryComp: AngularComponent;
tabs: any[];
tabs: PanelEditorTab[];
constructor(props) {
super(props);
......@@ -22,40 +32,42 @@ export class PanelEditor extends React.Component<PanelEditorProps, any> {
];
}
componentDidMount() {
if (!this.queryElement) {
return;
}
let loader = getAngularLoader();
var template = '<metrics-tab />';
let scopeProps = {
ctrl: {
panel: this.props.panel,
dashboard: this.props.dashboard,
panelCtrl: {
panel: this.props.panel,
dashboard: this.props.dashboard,
},
},
};
this.queryComp = loader.load(this.queryElement, scopeProps, template);
renderQueriesTab() {
return <QueriesTab panel={this.props.panel} dashboard={this.props.dashboard} />;
}
renderVizTab() {
return (
<div className="viz-editor">
<div className="viz-editor-col1">
<VizTypePicker currentType={this.props.panel.type} onTypeChanged={this.onVizTypeChanged} />
</div>
<div className="viz-editor-col2">
<h5 className="page-heading">Options</h5>
</div>
</div>
);
}
onChangeTab = tabName => {};
onVizTypeChanged = (plugin: PanelPlugin) => {
this.props.panel.type = plugin.id;
this.forceUpdate();
};
onChangeTab = (tab: PanelEditorTab) => {
store.view.updateQuery({ tab: tab.id }, false);
};
render() {
const activeTab: string = store.view.query.get('tab') || 'queries';
return (
<div className="tabbed-view tabbed-view--panel-edit-new">
<div className="tabbed-view tabbed-view--new">
<div className="tabbed-view-header">
<ul className="gf-tabs">
<li className="gf-tabs-item">
<a className="gf-tabs-link active">Queries</a>
</li>
<li className="gf-tabs-item">
<a className="gf-tabs-link">Visualization</a>
</li>
{this.tabs.map(tab => {
return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
})}
</ul>
<button className="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
......@@ -64,9 +76,32 @@ export class PanelEditor extends React.Component<PanelEditorProps, any> {
</div>
<div className="tabbed-view-body">
<div ref={element => (this.queryElement = element)} className="panel-height-helper" />
{activeTab === 'queries' && this.renderQueriesTab()}
{activeTab === 'viz' && this.renderVizTab()}
</div>
</div>
);
}
}
interface TabItemParams {
tab: PanelEditorTab;
activeTab: string;
onClick: (tab: PanelEditorTab) => void;
}
function TabItem({ tab, activeTab, onClick }: TabItemParams) {
const tabClasses = classNames({
'gf-tabs-link': true,
active: activeTab === tab.id,
});
return (
<li className="gf-tabs-item" key={tab.id}>
<a className={tabClasses} onClick={() => onClick(tab)}>
<i className={tab.icon} />
{tab.text}
</a>
</li>
);
}
......@@ -11,11 +11,14 @@ interface PanelHeaderProps {
export class PanelHeader extends React.Component<PanelHeaderProps, any> {
onEditPanel = () => {
store.view.updateQuery({
panelId: this.props.panel.id,
edit: true,
fullscreen: true,
});
store.view.updateQuery(
{
panelId: this.props.panel.id,
edit: true,
fullscreen: true,
},
false
);
};
render() {
......
import React from 'react';
import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model';
import { getAngularLoader, AngularComponent } from 'app/core/services/angular_loader';
interface Props {
panel: PanelModel;
dashboard: DashboardModel;
}
export class QueriesTab extends React.Component<Props, any> {
element: any;
component: AngularComponent;
constructor(props) {
super(props);
}
componentDidMount() {
if (!this.element) {
return;
}
let loader = getAngularLoader();
var template = '<metrics-tab />';
let scopeProps = {
ctrl: {
panel: this.props.panel,
dashboard: this.props.dashboard,
panelCtrl: {
panel: this.props.panel,
dashboard: this.props.dashboard,
},
},
};
this.component = loader.load(this.element, scopeProps, template);
}
componentWillUnmount() {
if (this.component) {
this.component.destroy();
}
}
render() {
return <div ref={element => (this.element = element)} className="panel-height-helper" />;
}
}
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import config, { PanelPlugin } from 'app/core/config';
import _ from 'lodash';
interface Props {
currentType: string;
onTypeChanged: (newType: PanelPlugin) => void;
}
interface State {
pluginList: PanelPlugin[];
}
export class VizTypePicker extends PureComponent<Props, State> {
constructor(props) {
super(props);
this.state = {
pluginList: this.getPanelPlugins(''),
};
}
getPanelPlugins(filter) {
let panels = _.chain(config.panels)
.filter({ hideFromList: false })
.map(item => item)
.value();
// add sort by sort property
return _.sortBy(panels, 'sort');
}
renderVizPlugin = (plugin, index) => {
const cssClass = classNames({
'viz-picker__item': true,
'viz-picker__item--selected': plugin.id === this.props.currentType,
});
return (
<div key={index} className={cssClass} onClick={() => this.props.onTypeChanged(plugin)} title={plugin.name}>
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
<div className="viz-picker__item-name">{plugin.name}</div>
</div>
);
};
render() {
return (
<div className="viz-picker">
<div className="gf-form gf-form--grow">
<label className="gf-form--has-input-icon gf-form--grow">
<input type="text" className="gf-form-input" placeholder="Search type" />
<i className="gf-form-input-icon fa fa-search" />
</label>
</div>
<div className="viz-picker-list">{this.state.pluginList.map(this.renderVizPlugin)}</div>
</div>
);
}
}
......@@ -38,6 +38,8 @@ export class DashNavCtrl {
} else if (search.fullscreen) {
delete search.fullscreen;
delete search.edit;
delete search.tab;
delete search.panelId;
}
this.$location.search(search);
}
......
......@@ -23,6 +23,9 @@ describe('AddPanelPanel', () => {
hideFromList: false,
name: 'Singlestat',
sort: 2,
module: '',
baseUrl: '',
meta: {},
info: {
logos: {
small: '',
......@@ -34,6 +37,9 @@ describe('AddPanelPanel', () => {
hideFromList: true,
name: 'Hidden',
sort: 100,
meta: {},
module: '',
baseUrl: '',
info: {
logos: {
small: '',
......@@ -45,6 +51,9 @@ describe('AddPanelPanel', () => {
hideFromList: false,
name: 'Graph',
sort: 1,
meta: {},
module: '',
baseUrl: '',
info: {
logos: {
small: '',
......@@ -56,6 +65,9 @@ describe('AddPanelPanel', () => {
hideFromList: false,
name: 'Zabbix',
sort: 100,
meta: {},
module: '',
baseUrl: '',
info: {
logos: {
small: '',
......@@ -67,6 +79,9 @@ describe('AddPanelPanel', () => {
hideFromList: false,
name: 'Piechart',
sort: 100,
meta: {},
module: '',
baseUrl: '',
info: {
logos: {
small: '',
......
......@@ -26,7 +26,7 @@ var panelTemplate = `
</div>
<div class="panel-full-edit" ng-if="ctrl.panel.isEditing">
<div class="tabbed-view tabbed-view--panel-edit">
<div class="tabbed-view">
<div class="tabbed-view-header">
<h3 class="tabbed-view-panel-title">
{{ctrl.pluginName}}
......
......@@ -95,7 +95,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => {
PanelCtrl.templateUrl = null;
PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-height-helper">${template}</grafana-panel>`;
PanelCtrl.template = `<grafana-panel ctrl="ctrl" class="panel-editor-container">${template}</grafana-panel>`;
return componentInfo;
});
......@@ -110,7 +110,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
let datasource = scope.target.datasource || scope.ctrl.panel.datasource;
return datasourceSrv.get(datasource).then(ds => {
scope.datasource = ds;
console.log('scope', scope);
return importPluginModule(ds.meta.module).then(dsModule => {
return {
......
......@@ -138,11 +138,22 @@ const flotDeps = [
'jquery.flot.stackpercent',
'jquery.flot.events',
];
for (let flotDep of flotDeps) {
exposeToPlugin(flotDep, { fakeDep: 1 });
}
export function importPluginModule(path: string): Promise<any> {
export interface PluginExports {
PanelCtrl?;
any;
PanelComponent?: any;
Datasource?: any;
QueryCtrl?: any;
ConfigCtrl?: any;
AnnotationsQueryCtrl?: any;
}
export function importPluginModule(path: string): Promise<PluginExports> {
let builtIn = builtInPlugins[path];
if (builtIn) {
return Promise.resolve(builtIn);
......
import React from 'react';
import React, { PureComponent } from 'react';
import { PanelProps } from 'app/features/dashboard/dashgrid/DataPanel';
export class ReactTestPanel extends React.Component<any, any> {
export class ReactTestPanel extends PureComponent<PanelProps> {
constructor(props) {
super(props);
}
render() {
return <h2>I am a react panel, haha!</h2>;
const { data } = this.props;
let value = 0;
if (data.length) {
value = data[0].value;
}
return <h2>I am a react value: {value}</h2>;
}
}
......
......@@ -23,8 +23,10 @@ export const ViewStore = types
}))
.actions(self => {
// querystring only
function updateQuery(query: any) {
self.query.clear();
function updateQuery(query: any, clear = true) {
if (clear) {
self.query.clear();
}
for (let key of Object.keys(query)) {
if (query[key]) {
self.query.set(key, query[key]);
......
......@@ -93,6 +93,7 @@
@import 'components/form_select_box';
@import 'components/user-picker';
@import 'components/description-picker';
@import 'components/viz_editor';
// PAGES
@import 'pages/login';
......
......@@ -85,10 +85,6 @@
height: calc(100% - 15px);
}
.add-panel__item-icon {
padding: 2px;
}
.add-panel__searchbar {
width: 100%;
margin-bottom: 10px;
......
.tabbed-view {
padding: $spacer*3;
margin-bottom: $dashboard-padding;
display: flex;
flex-direction: column;
height: 100%;
&.tabbed-view--panel-edit {
padding: 0;
.tabbed-view-header {
padding: 0px 25px;
background: none;
}
}
&.tabbed-view--panel-edit-new {
&.tabbed-view--new {
padding: 10px 0 0 0;
.tabbed-view-header {
padding: 0px;
background: none;
}
height: 100%;
}
}
.tabbed-view-header {
background: $page-header-bg;
box-shadow: $page-header-shadow;
border-bottom: 1px solid $page-header-border-color;
@include clearfix();
......@@ -57,7 +44,10 @@
}
.tabbed-view-body {
padding: $spacer*2 $spacer;
padding: $spacer*2 $spacer $spacer $spacer;
display: flex;
flex-direction: column;
flex: 1;
&--small {
min-height: 0px;
......
.viz-editor {
display: flex;
height: 100%;
}
.viz-editor-col1 {
width: 210px;
height: 100%;
margin-right: 40px;
}
.viz-editor-col2 {
flex-grow: 1;
}
.viz-picker {
display: flex;
flex-direction: column;
height: 100%;
}
.viz-picker-list {
padding-top: $spacer;
display: flex;
flex-direction: column;
overflow: hidden;
flex-grow: 1;
}
.viz-picker__item {
background: $card-background;
box-shadow: $card-shadow;
border-radius: 3px;
padding: $spacer;
width: 100%;
height: 60px;
text-align: center;
margin-bottom: 6px;
cursor: pointer;
display: flex;
flex-shrink: 0;
border: 1px solid transparent;
&:hover {
background: $card-background-hover;
}
&--selected {
border: 1px solid $orange;
.viz-picker__item-name {
color: $text-color;
}
.viz-picker__item-img {
filter: saturate(100%);
}
}
}
.viz-picker__item-name {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
font-size: $font-size-h5;
display: flex;
flex-direction: column;
align-self: center;
padding-left: $spacer;
font-size: $font-size-md;
color: $text-muted;
}
.viz-picker__item-img {
height: 100%;
filter: saturate(30%);
}
.dashboard-container {
padding: $dashboard-padding;
width: 100%;
min-height: 100%;
height: 100%;
box-sizing: border-box;
}
.template-variable {
......@@ -28,12 +29,17 @@ div.flot-text {
height: 100%;
}
.panel-editor-container {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-container {
background-color: $panel-bg;
border: $panel-border;
position: relative;
border-radius: 3px;
height: 100%;
&.panel-transparent {
background-color: transparent;
......@@ -233,5 +239,5 @@ div.flot-text {
}
.panel-full-edit {
margin: $dashboard-padding (-$dashboard-padding) 0 (-$dashboard-padding);
padding-top: $dashboard-padding;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment