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(),
}
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}}}",
CreatedAt: 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) => {
const initialState = getInitialState();
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',
data: JSON.stringify({
cluster_id: clusterID,
data: JSON.stringify(initialState),
}),
headers: {
@ -157,8 +156,8 @@ export function createExplorer(clusterID, push) {
},
}).then((resp) => {
const explorer = parseRawExplorer(resp.data);
dispatch(loadExplorer(explorer));
push(`/chronograf/data_explorer/${explorer.id}`);
dispatch(loadExploration(explorer));
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
// explorer and should create a new one.
if (explorer) {
dispatch(loadExplorer(explorer));
dispatch(loadExploration(explorer));
push(`/chronograf/data_explorer/${explorer.id}`);
} else {
dispatch(createExplorer(clusterID, push));
dispatch(createExploration(clusterID, push));
}
}
@ -220,14 +219,14 @@ function loadExplorers(explorers) {
};
}
function loadExplorer(explorer) {
function loadExploration(explorer) {
return {
type: 'LOAD_EXPLORER',
payload: {explorer},
};
}
export function fetchExplorers({source, userID, explorerID, push}) {
export function fetchExplorers({source, userID, explorerURI, push}) {
return (dispatch) => {
dispatch({type: 'FETCH_EXPLORERS'});
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
// saved (e.g. when they visit for the first time).
if (!explorers.length) {
dispatch(createExplorer(push));
dispatch(createExploration(push));
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
// most recently updated explorer and navigate to it.
if (!explorerID) {
if (!explorerURI) {
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)}`);
return;
}
// We have an explorerID, meaning a specific explorer was requested.
const explorer = explorers.find((ex) => ex.id === explorerID);
// We have an explorerURI, meaning a specific explorer was requested.
const explorer = explorers.find((ex) => ex.id === explorerURI);
// Attempting to request a non-existent explorer
if (!explorer) {
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) => {
// Save the previous session explicitly in case an auto-save was unable to complete.
const {panels, queryConfigs, activeExplorer} = getState();
api.saveExplorer({
explorerID: activeExplorer.id,
name: activeExplorer.name,
panels,
queryConfigs,
}).then(() => {
@ -302,11 +302,11 @@ export function chooseExplorer(clusterID, explorerID, push) {
dispatch(fetchExplorer());
AJAX({
url: `/api/int/v1/explorers/${explorerID}`,
url: explorerURI,
}).then((resp) => {
const explorer = parseRawExplorer(resp.data);
dispatch(loadExplorer(explorer));
push(`/chronograf/data_explorer/${explorerID}`);
dispatch(loadExploration(explorer));
push(`/sources/${source.id}/chronograf/data_explorer/${btoa(explorerURI)}`);
});
};
}

View File

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

View File

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

View File

@ -15,8 +15,8 @@ const Header = React.createClass({
explorerID: PropTypes.string.isRequired,
actions: PropTypes.shape({
setTimeRange: PropTypes.func.isRequired,
createExplorer: PropTypes.func.isRequired,
chooseExplorer: PropTypes.func.isRequired,
createExploration: PropTypes.func.isRequired,
chooseExploration: PropTypes.func.isRequired,
deleteExplorer: PropTypes.func.isRequired,
editExplorer: PropTypes.func.isRequired,
}),
@ -32,6 +32,10 @@ const Header = React.createClass({
};
},
contextTypes: {
source: PropTypes.shape(),
},
handleChooseTimeRange(bounds) {
this.props.actions.setTimeRange(bounds);
},
@ -46,10 +50,11 @@ const Header = React.createClass({
return selected ? selected.inputValue : 'Custom';
},
handleCreateExplorer() {
handleCreateExploration() {
// TODO: passing in this.props.router.push is a big smell, getting something like
// 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}) {
@ -57,7 +62,7 @@ const Header = React.createClass({
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 (
<div className="enterprise-header data-explorer__header">
<div className="enterprise-header__left">
<h1 className="dropdown-title">Session: </h1>
<h1 className="dropdown-title">Exploration: </h1>
<Dropdown
className="sessions-dropdown"
items={dropdownItems}
@ -107,7 +112,7 @@ const Header = React.createClass({
onChoose={this.handleChooseExplorer}
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 className="enterprise-header__right">
<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">
<span aria-hidden="true">×</span>
</button>
<h4 className="modal-title">Rename Explorer Session</h4>
<h4 className="modal-title">Rename Exploration</h4>
</div>
<form onSubmit={this.handleConfirm}>
<div className="modal-body">

View File

@ -1,7 +1,8 @@
export default function activeExplorer(state = {}, action) {
switch (action.type) {
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(() => {
const state = Object.assign({}, store.getState());
const explorerID = state.activeExplorer.id;
const name = state.activeExplorer.name;
if (!explorerID) {
return;
}
const {panels, queryConfigs} = state;
autoSaveTimer.clear();
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?
// If we ever show feedback in the UI, we could potentially indicate to remove it here.
}).catch(({response}) => {