Merge pull request #121 from influxdata/feature/create-new-explorer
Create and switch explorationspull/156/head
commit
b86fb31579
|
@ -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(),
|
||||
|
|
|
@ -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)}`);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}) => {
|
||||
|
|
Loading…
Reference in New Issue