Merge pull request #121 from influxdata/feature/create-new-explorer

Create and switch explorations
pull/156/head
Andrew Watkins 2016-09-29 13:15:21 -07:00 committed by GitHub
commit b86fb31579
7 changed files with 51 additions and 44 deletions

View File

@ -25,7 +25,7 @@ func NewExplorationStore(nowFunc func() time.Time) mrfusion.ExplorationStore {
UpdatedAt: nowFunc(), UpdatedAt: nowFunc(),
} }
e.db[1] = &mrfusion.Exploration{ e.db[1] = &mrfusion.Exploration{
Name: "Ferdinand Magellan", Name: "Your Mom",
Data: "{\"panels\":{\"123\":{\"id\":\"123\",\"queryIds\":[\"456\"]}},\"queryConfigs\":{\"456\":{\"id\":\"456\",\"database\":null,\"measurement\":null,\"retentionPolicy\":null,\"fields\":[],\"tags\":{},\"groupBy\":{\"time\":null,\"tags\":[]},\"areTagsAccepted\":true,\"rawText\":null}}}", Data: "{\"panels\":{\"123\":{\"id\":\"123\",\"queryIds\":[\"456\"]}},\"queryConfigs\":{\"456\":{\"id\":\"456\",\"database\":null,\"measurement\":null,\"retentionPolicy\":null,\"fields\":[],\"tags\":{},\"groupBy\":{\"time\":null,\"tags\":[]},\"areTagsAccepted\":true,\"rawText\":null}}}",
CreatedAt: nowFunc(), CreatedAt: nowFunc(),
UpdatedAt: nowFunc(), UpdatedAt: nowFunc(),

View File

@ -142,14 +142,13 @@ export function toggleTagAcceptance(queryId) {
}; };
} }
export function createExplorer(clusterID, push) { export function createExploration(source, push) {
return (dispatch) => { return (dispatch) => {
const initialState = getInitialState(); const initialState = getInitialState();
AJAX({ AJAX({
url: '/api/int/v1/explorers', url: `${source.links.self}/users/1/explorations`, // TODO: change this to use actual user link once users are introduced
method: 'POST', method: 'POST',
data: JSON.stringify({ data: JSON.stringify({
cluster_id: clusterID,
data: JSON.stringify(initialState), data: JSON.stringify(initialState),
}), }),
headers: { headers: {
@ -157,8 +156,8 @@ export function createExplorer(clusterID, push) {
}, },
}).then((resp) => { }).then((resp) => {
const explorer = parseRawExplorer(resp.data); const explorer = parseRawExplorer(resp.data);
dispatch(loadExplorer(explorer)); dispatch(loadExploration(explorer));
push(`/chronograf/data_explorer/${explorer.id}`); push(`/sources/${source.id}/chronograf/data_explorer/${btoa(explorer.link.href)}`); // Base64 encode explorer URI
}); });
}; };
} }
@ -178,10 +177,10 @@ export function deleteExplorer(clusterID, explorerID, push) {
// If we don't have an explorer to navigate to, it means we're deleting the last // If we don't have an explorer to navigate to, it means we're deleting the last
// explorer and should create a new one. // explorer and should create a new one.
if (explorer) { if (explorer) {
dispatch(loadExplorer(explorer)); dispatch(loadExploration(explorer));
push(`/chronograf/data_explorer/${explorer.id}`); push(`/chronograf/data_explorer/${explorer.id}`);
} else { } else {
dispatch(createExplorer(clusterID, push)); dispatch(createExploration(clusterID, push));
} }
} }
@ -220,14 +219,14 @@ function loadExplorers(explorers) {
}; };
} }
function loadExplorer(explorer) { function loadExploration(explorer) {
return { return {
type: 'LOAD_EXPLORER', type: 'LOAD_EXPLORER',
payload: {explorer}, payload: {explorer},
}; };
} }
export function fetchExplorers({source, userID, explorerID, push}) { export function fetchExplorers({source, userID, explorerURI, push}) {
return (dispatch) => { return (dispatch) => {
dispatch({type: 'FETCH_EXPLORERS'}); dispatch({type: 'FETCH_EXPLORERS'});
AJAX({ AJAX({
@ -239,29 +238,29 @@ export function fetchExplorers({source, userID, explorerID, push}) {
// Create a new explorer session for a user if they don't have any // Create a new explorer session for a user if they don't have any
// saved (e.g. when they visit for the first time). // saved (e.g. when they visit for the first time).
if (!explorers.length) { if (!explorers.length) {
dispatch(createExplorer(push)); dispatch(createExploration(push));
return; return;
} }
// If no explorerID is provided, it means the user wasn't attempting to visit // If no explorerURI is provided, it means the user wasn't attempting to visit
// a specific explorer (i.e. `/data_explorer/:id`). In this case, pick the // a specific explorer (i.e. `/data_explorer/:id`). In this case, pick the
// most recently updated explorer and navigate to it. // most recently updated explorer and navigate to it.
if (!explorerID) { if (!explorerURI) {
const explorer = _.maxBy(explorers, (ex) => ex.updated_at); const explorer = _.maxBy(explorers, (ex) => ex.updated_at);
dispatch(loadExplorer(explorer)); dispatch(loadExploration(explorer));
push(`/sources/${source.id}/chronograf/data_explorer/${btoa(explorer.link.href)}`); push(`/sources/${source.id}/chronograf/data_explorer/${btoa(explorer.link.href)}`);
return; return;
} }
// We have an explorerID, meaning a specific explorer was requested. // We have an explorerURI, meaning a specific explorer was requested.
const explorer = explorers.find((ex) => ex.id === explorerID); const explorer = explorers.find((ex) => ex.id === explorerURI);
// Attempting to request a non-existent explorer // Attempting to request a non-existent explorer
if (!explorer) { if (!explorer) {
return; return;
} }
dispatch(loadExplorer(explorer)); dispatch(loadExploration(explorer));
}); });
}; };
} }
@ -284,12 +283,13 @@ function saveExplorer(error) {
}; };
} }
export function chooseExplorer(clusterID, explorerID, push) { export function chooseExploration(explorerURI, source, push) {
return (dispatch, getState) => { return (dispatch, getState) => {
// Save the previous session explicitly in case an auto-save was unable to complete. // Save the previous session explicitly in case an auto-save was unable to complete.
const {panels, queryConfigs, activeExplorer} = getState(); const {panels, queryConfigs, activeExplorer} = getState();
api.saveExplorer({ api.saveExplorer({
explorerID: activeExplorer.id, explorerID: activeExplorer.id,
name: activeExplorer.name,
panels, panels,
queryConfigs, queryConfigs,
}).then(() => { }).then(() => {
@ -302,11 +302,11 @@ export function chooseExplorer(clusterID, explorerID, push) {
dispatch(fetchExplorer()); dispatch(fetchExplorer());
AJAX({ AJAX({
url: `/api/int/v1/explorers/${explorerID}`, url: explorerURI,
}).then((resp) => { }).then((resp) => {
const explorer = parseRawExplorer(resp.data); const explorer = parseRawExplorer(resp.data);
dispatch(loadExplorer(explorer)); dispatch(loadExploration(explorer));
push(`/chronograf/data_explorer/${explorerID}`); push(`/sources/${source.id}/chronograf/data_explorer/${btoa(explorerURI)}`);
}); });
}; };
} }

View File

@ -1,15 +1,15 @@
import AJAX from 'utils/ajax'; import AJAX from 'utils/ajax';
export function saveExplorer({panels, queryConfigs, explorerID}) { export function saveExplorer({name, panels, queryConfigs, explorerID}) {
return AJAX({ return AJAX({
url: `/api/int/v1/explorers/${explorerID}`, url: explorerID,
method: 'PUT', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
data: JSON.stringify({ data: JSON.stringify({
data: JSON.stringify({panels, queryConfigs}), data: JSON.stringify({panels, queryConfigs}),
name,
}), }),
}); });
} }

View File

@ -7,8 +7,8 @@ import ResizeContainer from 'shared/components/ResizeContainer';
import {FETCHING} from '../reducers/explorers'; import {FETCHING} from '../reducers/explorers';
import { import {
setTimeRange as setTimeRangeAction, setTimeRange as setTimeRangeAction,
createExplorer as createExplorerAction, createExploration as createExplorationAction,
chooseExplorer as chooseExplorerAction, chooseExploration as chooseExplorationAction,
deleteExplorer as deleteExplorerAction, deleteExplorer as deleteExplorerAction,
editExplorer as editExplorerAction, editExplorer as editExplorerAction,
} from '../actions/view'; } from '../actions/view';
@ -28,8 +28,8 @@ const DataExplorer = React.createClass({
lower: PropTypes.string, lower: PropTypes.string,
}).isRequired, }).isRequired,
setTimeRange: PropTypes.func.isRequired, setTimeRange: PropTypes.func.isRequired,
createExplorer: PropTypes.func.isRequired, createExploration: PropTypes.func.isRequired,
chooseExplorer: PropTypes.func.isRequired, chooseExploration: PropTypes.func.isRequired,
deleteExplorer: PropTypes.func.isRequired, deleteExplorer: PropTypes.func.isRequired,
editExplorer: PropTypes.func.isRequired, editExplorer: PropTypes.func.isRequired,
}, },
@ -60,7 +60,7 @@ const DataExplorer = React.createClass({
}, },
render() { render() {
const {timeRange, explorers, explorerID, setTimeRange, createExplorer, chooseExplorer, deleteExplorer, editExplorer} = this.props; const {timeRange, explorers, explorerID, setTimeRange, createExploration, chooseExploration, deleteExplorer, editExplorer} = this.props;
if (explorers === FETCHING) { if (explorers === FETCHING) {
// TODO: page-wide spinner // TODO: page-wide spinner
@ -69,13 +69,13 @@ const DataExplorer = React.createClass({
const activeExplorer = explorers[explorerID]; const activeExplorer = explorers[explorerID];
if (!activeExplorer) { if (!activeExplorer) {
return null; // TODO: handle no explorers; return <div>You have no active explorers</div>; // TODO: handle no explorers;
} }
return ( return (
<div className="data-explorer"> <div className="data-explorer">
<Header <Header
actions={{setTimeRange, createExplorer, chooseExplorer, deleteExplorer, editExplorer}} actions={{setTimeRange, createExploration, chooseExploration, deleteExplorer, editExplorer}}
explorers={explorers} explorers={explorers}
timeRange={timeRange} timeRange={timeRange}
explorerID={explorerID} explorerID={explorerID}
@ -98,8 +98,8 @@ function mapStateToProps(state) {
export default connect(mapStateToProps, { export default connect(mapStateToProps, {
setTimeRange: setTimeRangeAction, setTimeRange: setTimeRangeAction,
createExplorer: createExplorerAction, createExploration: createExplorationAction,
chooseExplorer: chooseExplorerAction, chooseExploration: chooseExplorationAction,
deleteExplorer: deleteExplorerAction, deleteExplorer: deleteExplorerAction,
editExplorer: editExplorerAction, editExplorer: editExplorerAction,
})(DataExplorer); })(DataExplorer);

View File

@ -15,8 +15,8 @@ const Header = React.createClass({
explorerID: PropTypes.string.isRequired, explorerID: PropTypes.string.isRequired,
actions: PropTypes.shape({ actions: PropTypes.shape({
setTimeRange: PropTypes.func.isRequired, setTimeRange: PropTypes.func.isRequired,
createExplorer: PropTypes.func.isRequired, createExploration: PropTypes.func.isRequired,
chooseExplorer: PropTypes.func.isRequired, chooseExploration: PropTypes.func.isRequired,
deleteExplorer: PropTypes.func.isRequired, deleteExplorer: PropTypes.func.isRequired,
editExplorer: PropTypes.func.isRequired, editExplorer: PropTypes.func.isRequired,
}), }),
@ -32,6 +32,10 @@ const Header = React.createClass({
}; };
}, },
contextTypes: {
source: PropTypes.shape(),
},
handleChooseTimeRange(bounds) { handleChooseTimeRange(bounds) {
this.props.actions.setTimeRange(bounds); this.props.actions.setTimeRange(bounds);
}, },
@ -46,10 +50,11 @@ const Header = React.createClass({
return selected ? selected.inputValue : 'Custom'; return selected ? selected.inputValue : 'Custom';
}, },
handleCreateExplorer() { handleCreateExploration() {
// TODO: passing in this.props.router.push is a big smell, getting something like // TODO: passing in this.props.router.push is a big smell, getting something like
// react-router-redux might be better here // react-router-redux might be better here
this.props.actions.createExplorer(this.props.router.push);
this.props.actions.createExploration(this.context.source, this.props.router.push);
}, },
handleChooseExplorer({id}) { handleChooseExplorer({id}) {
@ -57,7 +62,7 @@ const Header = React.createClass({
return; return;
} }
this.props.actions.chooseExplorer(id, this.props.router.push); this.props.actions.chooseExploration(id, this.context.source, this.props.router.push);
}, },
/** /**
@ -99,7 +104,7 @@ const Header = React.createClass({
return ( return (
<div className="enterprise-header data-explorer__header"> <div className="enterprise-header data-explorer__header">
<div className="enterprise-header__left"> <div className="enterprise-header__left">
<h1 className="dropdown-title">Session: </h1> <h1 className="dropdown-title">Exploration: </h1>
<Dropdown <Dropdown
className="sessions-dropdown" className="sessions-dropdown"
items={dropdownItems} items={dropdownItems}
@ -107,7 +112,7 @@ const Header = React.createClass({
onChoose={this.handleChooseExplorer} onChoose={this.handleChooseExplorer}
selected={this.getName(selectedExplorer)} selected={this.getName(selectedExplorer)}
/> />
<div className="btn btn-sm btn-primary sessions-dropdown__btn" onClick={this.handleCreateExplorer}>New Session</div> <div className="btn btn-sm btn-primary sessions-dropdown__btn" onClick={this.handleCreateExploration}>New Exploration</div>
</div> </div>
<div className="enterprise-header__right"> <div className="enterprise-header__right">
<TimeRangeDropdown onChooseTimeRange={this.handleChooseTimeRange} selected={this.findSelected(timeRange)} /> <TimeRangeDropdown onChooseTimeRange={this.handleChooseTimeRange} selected={this.findSelected(timeRange)} />
@ -174,7 +179,7 @@ const EditExplorerModal = React.createClass({
<button type="button" className="close" data-dismiss="modal" aria-label="Close"> <button type="button" className="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
<h4 className="modal-title">Rename Explorer Session</h4> <h4 className="modal-title">Rename Exploration</h4>
</div> </div>
<form onSubmit={this.handleConfirm}> <form onSubmit={this.handleConfirm}>
<div className="modal-body"> <div className="modal-body">

View File

@ -1,7 +1,8 @@
export default function activeExplorer(state = {}, action) { export default function activeExplorer(state = {}, action) {
switch (action.type) { switch (action.type) {
case 'LOAD_EXPLORER': { case 'LOAD_EXPLORER': {
return {id: action.payload.explorer.id}; const {link, name} = action.payload.explorer;
return {id: link.href, name};
} }
} }

View File

@ -33,13 +33,14 @@ export default function persistState() {
store.subscribe(() => { store.subscribe(() => {
const state = Object.assign({}, store.getState()); const state = Object.assign({}, store.getState());
const explorerID = state.activeExplorer.id; const explorerID = state.activeExplorer.id;
const name = state.activeExplorer.name;
if (!explorerID) { if (!explorerID) {
return; return;
} }
const {panels, queryConfigs} = state; const {panels, queryConfigs} = state;
autoSaveTimer.clear(); autoSaveTimer.clear();
autoSaveTimer.set(() => { autoSaveTimer.set(() => {
saveExplorer({panels, queryConfigs, explorerID}).then((_) => { saveExplorer({panels, queryConfigs, explorerID, name}).then((_) => {
// TODO: This is a no-op currently because we don't have any feedback in the UI around saving, but maybe we do something in the future? // TODO: This is a no-op currently because we don't have any feedback in the UI around saving, but maybe we do something in the future?
// If we ever show feedback in the UI, we could potentially indicate to remove it here. // If we ever show feedback in the UI, we could potentially indicate to remove it here.
}).catch(({response}) => { }).catch(({response}) => {