Remove concept of Panels

pull/10616/head
Andrew Watkins 2017-02-08 13:58:15 -08:00
parent 3a7ed1a32f
commit 51b641b2c3
18 changed files with 4 additions and 607 deletions

View File

@ -1,11 +0,0 @@
import reducer from 'src/data_explorer/reducers/dataExplorerUI';
import {activatePanel} from 'src/data_explorer/actions/view';
describe('DataExplorer.Reducers.UI', () => {
it('can set the active panel', () => {
const activePanel = 123;
const actual = reducer({}, activatePanel(activePanel));
expect(actual).to.deep.equal({activePanel});
});
});

View File

@ -1,34 +0,0 @@
import reducer from 'src/data_explorer/reducers/panels';
import {deletePanel} from 'src/data_explorer/actions/view';
const fakeAddPanelAction = (panelID, queryID) => {
return {
type: 'CREATE_PANEL',
payload: {panelID, queryID},
};
};
describe('Chronograf.Reducers.Panel', () => {
let state;
const panelID = 123;
const queryID = 456;
beforeEach(() => {
state = reducer({}, fakeAddPanelAction(panelID, queryID));
});
it('can add a panel', () => {
const actual = state[panelID];
expect(actual).to.deep.equal({
id: panelID,
queryIds: [queryID],
});
});
it('can delete a panel', () => {
const nextState = reducer(state, deletePanel(panelID));
const actual = nextState[panelID];
expect(actual).to.equal(undefined);
});
});

View File

@ -1,35 +1,6 @@
import uuid from 'node-uuid';
export function createPanel() {
return {
type: 'CREATE_PANEL',
payload: {
panelID: uuid.v4(), // for the default Panel
queryID: uuid.v4(), // for the default Query
},
};
}
export function renamePanel(panelId, name) {
return {
type: 'RENAME_PANEL',
payload: {
panelId,
name,
},
};
}
export function deletePanel(panelId) {
return {
type: 'DELETE_PANEL',
payload: {
panelId,
},
};
}
export function addQuery(panelId, options) {
export function addQuery(options = {}) {
return {
type: 'ADD_QUERY',
payload: {
@ -157,12 +128,3 @@ export function updateRawQuery(queryID, text) {
},
};
}
export function activatePanel(panelID) {
return {
type: 'ACTIVATE_PANEL',
payload: {
panelID,
},
};
}

View File

@ -1,183 +0,0 @@
import React, {PropTypes} from 'react';
import classNames from 'classnames';
import QueryEditor from './QueryEditor';
import QueryTabItem from './QueryTabItem';
import RenamePanelModal from './RenamePanelModal';
import SimpleDropdown from 'src/shared/components/SimpleDropdown';
const Panel = React.createClass({
propTypes: {
panel: PropTypes.shape({
id: PropTypes.string.isRequired,
}).isRequired,
queries: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
timeRange: PropTypes.shape({
upper: PropTypes.string,
lower: PropTypes.string,
}).isRequired,
isExpanded: PropTypes.bool.isRequired,
onTogglePanel: PropTypes.func.isRequired,
actions: PropTypes.shape({
chooseNamespace: PropTypes.func.isRequired,
chooseMeasurement: PropTypes.func.isRequired,
chooseTag: PropTypes.func.isRequired,
groupByTag: PropTypes.func.isRequired,
addQuery: PropTypes.func.isRequired,
deleteQuery: PropTypes.func.isRequired,
toggleField: PropTypes.func.isRequired,
groupByTime: PropTypes.func.isRequired,
toggleTagAcceptance: PropTypes.func.isRequired,
applyFuncsToField: PropTypes.func.isRequired,
deletePanel: PropTypes.func.isRequired,
renamePanel: PropTypes.func.isRequired,
}).isRequired,
setActiveQuery: PropTypes.func.isRequired,
activeQueryID: PropTypes.string,
},
handleSetActiveQuery(query) {
this.props.setActiveQuery(query.id);
},
handleAddQuery() {
this.props.actions.addQuery();
},
handleAddRawQuery() {
this.props.actions.addQuery({rawText: `SELECT "fields" from "db"."rp"."measurement"`});
},
handleDeleteQuery(query) {
this.props.actions.deleteQuery(query.id);
},
handleSelectPanel() {
this.props.onTogglePanel(this.props.panel);
},
handleDeletePanel(e) {
e.stopPropagation();
this.props.actions.deletePanel(this.props.panel.id);
},
getActiveQuery() {
const {queries, activeQueryID} = this.props;
const activeQuery = queries.find((query) => query.id === activeQueryID);
const defaultQuery = queries[0];
return activeQuery || defaultQuery;
},
openRenamePanelModal(e) {
e.stopPropagation();
$(`#renamePanelModal-${this.props.panel.id}`).modal('show'); // eslint-disable-line no-undef
},
handleRename(newName) {
this.props.actions.renamePanel(this.props.panel.id, newName);
},
render() {
const {panel, isExpanded} = this.props;
return (
<div className={classNames('panel', {active: isExpanded})}>
<div className="panel--header" onClick={this.handleSelectPanel}>
<div className="panel--name">
<span className="icon caret-right"></span>
{panel.name || "Graph"}
</div>
<div className="panel--actions">
{/* <div title="Export Queries to Dashboard" className="panel--action"><span className="icon export"></span></div> */}
<div title="Rename Graph" className="panel--action" onClick={this.openRenamePanelModal}><span className="icon pencil"></span></div>
<div title="Delete Graph" className="panel--action" onClick={this.handleDeletePanel}><span className="icon trash"></span></div>
</div>
</div>
{this.renderQueryTabList()}
{this.renderQueryEditor()}
<RenamePanelModal panel={panel} onConfirm={this.handleRename} />
</div>
);
},
renderQueryEditor() {
if (!this.props.isExpanded) {
return null;
}
const {timeRange, actions} = this.props;
const query = this.getActiveQuery();
if (!query) {
return (
<div className="qeditor--empty">
<h5>This Graph has no Queries</h5>
<br/>
<div className="btn btn-primary" role="button" onClick={this.handleAddQuery}>Add a Query</div>
</div>
);
}
return (
<QueryEditor
timeRange={timeRange}
query={this.getActiveQuery()}
actions={actions}
onAddQuery={this.handleAddQuery}
/>
);
},
renderQueryTabList() {
const {isExpanded, queries} = this.props;
if (!isExpanded) {
return null;
}
return (
<div className="panel--tabs">
{queries.map((q) => {
let queryTabText;
if (q.rawText) {
queryTabText = 'InfluxQL';
} else {
queryTabText = (q.measurement && q.fields.length !== 0) ? `${q.measurement}.${q.fields[0].field}` : 'Query';
}
return (
<QueryTabItem
isActive={this.getActiveQuery().id === q.id}
key={q.id}
query={q}
onSelect={this.handleSetActiveQuery}
onDelete={this.handleDeleteQuery}
queryTabText={queryTabText}
/>
);
})}
{this.renderAddQuery()}
</div>
);
},
onChoose(item) {
switch (item.text) {
case 'Query Builder':
this.handleAddQuery();
break;
case 'InfluxQL':
this.handleAddRawQuery();
break;
}
},
renderAddQuery() {
return (
<SimpleDropdown onChoose={this.onChoose} items={[{text: 'Query Builder'}, {text: 'InfluxQL'}]} className="panel--tab-new">
<span className="icon plus"></span>
</SimpleDropdown>
);
},
});
export default Panel;

View File

@ -1,63 +0,0 @@
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import PanelList from './PanelList';
import * as viewActions from '../actions/view';
const {string, func} = PropTypes;
const PanelBuilder = React.createClass({
propTypes: {
width: string,
actions: PropTypes.shape({
activatePanel: func.isRequired,
createPanel: func.isRequired,
deleteQuery: func.isRequired,
addQuery: func.isRequired,
editRawText: func.isRequired,
chooseNamespace: func.isRequired,
chooseMeasurement: func.isRequired,
toggleField: func.isRequired,
groupByTime: func.isRequired,
applyFuncsToField: func.isRequired,
chooseTag: func.isRequired,
groupByTag: func.isRequired,
toggleTagAcceptance: func.isRequired,
deletePanel: func.isRequired,
}).isRequired,
setActiveQuery: func.isRequired,
activePanelID: string,
activeQueryID: string,
},
handleCreateExplorer() {
this.props.actions.createPanel();
},
render() {
const {width, actions, setActiveQuery, activePanelID, activeQueryID} = this.props;
return (
<div className="panel-builder" style={{width}}>
<div className="btn btn-block btn-primary" onClick={this.handleCreateExplorer}><span className="icon graphline"></span>&nbsp;&nbsp;Create Graph</div>
<PanelList
actions={actions}
setActiveQuery={setActiveQuery}
activePanelID={activePanelID}
activeQueryID={activeQueryID}
/>
</div>
);
},
});
function mapStateToProps() {
return {};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(viewActions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(PanelBuilder);

View File

@ -1,76 +0,0 @@
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import _ from 'lodash';
import Panel from './Panel';
const {func, string, shape} = PropTypes;
const PanelList = React.createClass({
propTypes: {
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
panels: shape({}).isRequired,
queryConfigs: PropTypes.shape({}),
actions: shape({
activatePanel: func.isRequired,
deleteQuery: func.isRequired,
addQuery: func.isRequired,
}).isRequired,
setActiveQuery: func.isRequired,
activePanelID: string,
activeQueryID: string,
},
handleTogglePanel(panel) {
const panelID = panel.id === this.props.activePanelID ? null : panel.id;
this.props.actions.activatePanel(panelID);
// Reset the activeQueryID when toggling Exporations
this.props.setActiveQuery(null);
},
render() {
const {actions, panels, timeRange, queryConfigs, setActiveQuery, activeQueryID, activePanelID} = this.props;
return (
<div>
{Object.keys(panels).map((panelID) => {
const panel = panels[panelID];
const queries = panel.queryIds.map((configId) => queryConfigs[configId]);
const deleteQueryFromPanel = _.partial(actions.deleteQuery, panelID);
const addQueryToPanel = _.partial(actions.addQuery, panelID);
const allActions = Object.assign({}, actions, {
addQuery: addQueryToPanel,
deleteQuery: deleteQueryFromPanel,
});
return (
<Panel
key={panelID}
panel={panel}
queries={queries}
timeRange={timeRange}
onTogglePanel={this.handleTogglePanel}
setActiveQuery={setActiveQuery}
isExpanded={panelID === activePanelID}
actions={allActions}
activeQueryID={activeQueryID}
/>
);
})}
</div>
);
},
});
function mapStateToProps(state) {
return {
timeRange: state.timeRange,
panels: state.panels,
queryConfigs: state.queryConfigs,
};
}
export default connect(mapStateToProps)(PanelList);

View File

@ -26,8 +26,6 @@ const QueryBuilder = React.createClass({
groupByTime: PropTypes.func.isRequired,
toggleTagAcceptance: PropTypes.func.isRequired,
applyFuncsToField: PropTypes.func.isRequired,
deletePanel: PropTypes.func.isRequired,
renamePanel: PropTypes.func.isRequired,
}).isRequired,
setActiveQuery: PropTypes.func.isRequired,
activeQueryID: PropTypes.string,

View File

@ -1,67 +0,0 @@
import React, {PropTypes} from 'react';
const RenamePanelModal = React.createClass({
propTypes: {
onConfirm: PropTypes.func.isRequired,
panel: PropTypes.shape({
id: PropTypes.string.isRequired,
}),
},
getInitialState() {
return {error: null};
},
componentDidMount() {
this.refs.name.focus();
},
render() {
const {panel} = this.props;
return (
<div className="modal fade in" id={`renamePanelModal-${panel.id}`} tabIndex="-1" role="dialog">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 className="modal-title">Rename Panel</h4>
</div>
<div className="modal-body">
{this.state.error ?
<div className="alert alert-danger" role="alert">{this.state.error}</div>
: null}
<div className="form-grid padding-top">
<div className="form-group col-md-8 col-md-offset-2">
<input ref="name" name="renameExplorer" type="text" placeholder={panel.name} className="form-control input-lg" id="renameExplorer" required={true} />
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-info" data-dismiss="modal">Cancel</button>
<button onClick={this.handleConfirm} className="btn btn-success">Rename</button>
</div>
</div>
</div>
</div>
);
},
handleConfirm() {
const name = this.refs.name.value;
if (name === '') {
this.setState({error: "Name can't be blank"});
return;
}
$(`#renamePanelModal-${this.props.panel.id}`).modal('hide'); // eslint-disable-line no-undef
this.refs.name.value = '';
this.setState({error: null});
this.props.onConfirm(name);
},
});
export default RenamePanelModal;

View File

@ -52,7 +52,7 @@ const Visualization = React.createClass({
const isInDataExplorer = true;
return (
<div ref={(p) => this.panel = p} className={classNames("graph", {active: true})}>
<div className={classNames("graph", {active: true})}>
<div className="graph-heading">
<div className="graph-title">
{name || "Graph"}

View File

@ -1,61 +0,0 @@
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import Visualization from './Visualization';
const {shape, string} = PropTypes;
const Visualizations = React.createClass({
propTypes: {
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
panels: shape({}).isRequired,
queryConfigs: shape({}).isRequired,
width: string,
activePanelID: string,
activeQueryID: string,
},
render() {
const {panels, queryConfigs, timeRange, width, activePanelID} = this.props;
const visualizations = Object.keys(panels).map((panelID) => {
const panel = panels[panelID];
const queries = panel.queryIds.map((id) => queryConfigs[id]);
const isActive = panelID === activePanelID;
return <Visualization activeQueryIndex={this.getActiveQueryIndex(panelID)} name={panel.name} key={panelID} queryConfigs={queries} timeRange={timeRange} isActive={isActive} />;
});
return (
<div className="panels" style={{width}}>
{visualizations}
</div>
);
},
getActiveQueryIndex(panelID) {
const {activeQueryID, activePanelID, panels} = this.props;
const isPanelActive = panelID === activePanelID;
if (!isPanelActive) {
return -1;
}
if (activeQueryID === null) {
return 0;
}
return panels[panelID].queryIds.indexOf(activeQueryID);
},
});
function mapStateToProps(state) {
return {
panels: state.panels,
queryConfigs: state.queryConfigs,
};
}
export default connect(mapStateToProps)(Visualizations);

View File

@ -84,12 +84,11 @@ const DataExplorer = React.createClass({
});
function mapStateToProps(state) {
const {timeRange, queryConfigs, dataExplorerUI} = state;
const {timeRange, queryConfigs} = state;
return {
timeRange,
queryConfigs,
activePanel: dataExplorerUI.activePanel,
};
}

View File

@ -1,11 +0,0 @@
export default function dataExplorerUI(state = {}, action) {
switch (action.type) {
case 'ACTIVATE_PANEL':
case 'CREATE_PANEL': {
const {panelID} = action.payload;
return {...state, activePanel: panelID};
}
}
return state;
}

View File

@ -1,11 +1,7 @@
import queryConfigs from './queryConfigs';
import panels from './panels';
import timeRange from './timeRange';
import dataExplorerUI from './dataExplorerUI';
export {
queryConfigs,
panels,
timeRange,
dataExplorerUI,
};

View File

@ -1,33 +0,0 @@
import update from 'react-addons-update';
export default function panels(state = {}, action) {
switch (action.type) {
case 'CREATE_PANEL': {
const {panelID, queryID} = action.payload;
return {
...state,
[panelID]: {id: panelID, queryIds: [queryID]},
};
}
case 'RENAME_PANEL': {
const {panelId, name} = action.payload;
return update(state, {
[panelId]: {
name: {$set: name},
},
});
}
case 'DELETE_PANEL': {
const {panelId} = action.payload;
return update(state, {$apply: (p) => {
const panelsCopy = Object.assign({}, p);
delete panelsCopy[panelId];
return panelsCopy;
}});
}
}
return state;
}

View File

@ -46,7 +46,6 @@ export default function queryConfigs(state = {}, action) {
return nextState;
}
case 'CREATE_PANEL':
case 'ADD_KAPACITOR_QUERY':
case 'ADD_QUERY': {
const {queryID, options} = action.payload;

View File

@ -19,13 +19,11 @@ export const loadLocalStorage = () => {
}
};
export const saveToLocalStorage = ({panels, queryConfigs, timeRange, dataExplorerUI}) => {
export const saveToLocalStorage = ({queryConfigs, timeRange}) => {
try {
window.localStorage.setItem('state', JSON.stringify({
panels,
queryConfigs,
timeRange,
dataExplorerUI,
}));
} catch (err) {
console.error('Unable to save data explorer: ', JSON.parse(err)); // eslint-disable-line no-console

View File

@ -29,7 +29,6 @@
// DE Specific components
@import 'data-explorer/query-builder';
@import 'data-explorer/page-header';
@import 'data-explorer/panel-builder';
@import 'data-explorer/query-editor';
@import 'data-explorer/raw-text';
@import 'data-explorer/tag-list';

View File

@ -1,15 +0,0 @@
.panel-builder {
width: 399px;
overflow-x: hidden;
background: $g1-raven;
padding: $explorer-page-padding;
@include gradient-v($g2-kevlar,$g0-obsidian);
&::-webkit-scrollbar {
display: none;
}
> .btn {
margin-bottom: 6px;
}
}