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(),
|
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(),
|
||||||
|
|
|
@ -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)}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}) => {
|
||||||
|
|
Loading…
Reference in New Issue