Merge pull request #1112 from influxdata/1080-delete_dashboard
Add ability to delete a dashboardpull/10616/head
commit
11158077f0
|
@ -4,6 +4,8 @@
|
||||||
1. [#1104](https://github.com/influxdata/chronograf/pull/1104): Fix windows hosts on host list
|
1. [#1104](https://github.com/influxdata/chronograf/pull/1104): Fix windows hosts on host list
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
1. [#1112](https://github.com/influxdata/chronograf/pull/1112): Add ability to delete a dashboard
|
||||||
|
|
||||||
### UI Improvements
|
### UI Improvements
|
||||||
1. [#1101](https://github.com/influxdata/chronograf/pull/1101): Compress InfluxQL responses with gzip
|
1. [#1101](https://github.com/influxdata/chronograf/pull/1101): Compress InfluxQL responses with gzip
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
import reducer from 'src/dashboards/reducers/ui'
|
import reducer from 'src/dashboards/reducers/ui'
|
||||||
import timeRanges from 'hson!src/shared/data/timeRanges.hson';
|
import timeRanges from 'hson!src/shared/data/timeRanges.hson';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
loadDashboards,
|
loadDashboards,
|
||||||
setDashboard,
|
setDashboard,
|
||||||
|
deleteDashboard,
|
||||||
|
deleteDashboardFailed,
|
||||||
setTimeRange,
|
setTimeRange,
|
||||||
updateDashboardCells,
|
updateDashboardCells,
|
||||||
editDashboardCell,
|
editDashboardCell,
|
||||||
|
@ -50,6 +54,25 @@ describe('DataExplorer.Reducers.UI', () => {
|
||||||
expect(actual.dashboard).to.deep.equal(d2)
|
expect(actual.dashboard).to.deep.equal(d2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('can handle a successful dashboard deletion', () => {
|
||||||
|
const loadedState = reducer(state, loadDashboards(dashboards))
|
||||||
|
const expected = [d1]
|
||||||
|
const actual = reducer(loadedState, deleteDashboard(d2))
|
||||||
|
|
||||||
|
expect(actual.dashboards).to.deep.equal(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can handle a failed dashboard deletion', () => {
|
||||||
|
const loadedState = reducer(state, loadDashboards([d1]))
|
||||||
|
const actual = reducer(loadedState, deleteDashboardFailed(d2))
|
||||||
|
const actualFirst = _.first(actual.dashboards)
|
||||||
|
|
||||||
|
expect(actual.dashboards).to.have.length(2)
|
||||||
|
_.forOwn(d2, (v, k) => {
|
||||||
|
expect(actualFirst[k]).to.deep.equal(v)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('can set the time range', () => {
|
it('can set the time range', () => {
|
||||||
const expected = {upper: null, lower: 'now() - 1h'}
|
const expected = {upper: null, lower: 'now() - 1h'}
|
||||||
const actual = reducer(state, setTimeRange(expected))
|
const actual = reducer(state, setTimeRange(expected))
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import {
|
import {
|
||||||
getDashboards as getDashboardsAJAX,
|
getDashboards as getDashboardsAJAX,
|
||||||
updateDashboard as updateDashboardAJAX,
|
updateDashboard as updateDashboardAJAX,
|
||||||
|
deleteDashboard as deleteDashboardAJAX,
|
||||||
updateDashboardCell as updateDashboardCellAJAX,
|
updateDashboardCell as updateDashboardCellAJAX,
|
||||||
addDashboardCell as addDashboardCellAJAX,
|
addDashboardCell as addDashboardCellAJAX,
|
||||||
deleteDashboardCell as deleteDashboardCellAJAX,
|
deleteDashboardCell as deleteDashboardCellAJAX,
|
||||||
} from 'src/dashboards/apis'
|
} from 'src/dashboards/apis'
|
||||||
|
|
||||||
|
import {publishNotification, delayDismissNotification} from 'src/shared/actions/notifications'
|
||||||
|
|
||||||
|
import {SHORT_NOTIFICATION_DISAPPEARING_DELAY} from 'shared/constants'
|
||||||
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
|
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
|
||||||
|
|
||||||
export const loadDashboards = (dashboards, dashboardID) => ({
|
export const loadDashboards = (dashboards, dashboardID) => ({
|
||||||
|
@ -37,6 +41,20 @@ export const updateDashboard = (dashboard) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const deleteDashboard = (dashboard) => ({
|
||||||
|
type: 'DELETE_DASHBOARD',
|
||||||
|
payload: {
|
||||||
|
dashboard,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteDashboardFailed = (dashboard) => ({
|
||||||
|
type: 'DELETE_DASHBOARD_FAILED',
|
||||||
|
payload: {
|
||||||
|
dashboard,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const updateDashboardCells = (cells) => ({
|
export const updateDashboardCells = (cells) => ({
|
||||||
type: 'UPDATE_DASHBOARD_CELLS',
|
type: 'UPDATE_DASHBOARD_CELLS',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -89,10 +107,14 @@ export const deleteDashboardCell = (cell) => ({
|
||||||
|
|
||||||
// Async Action Creators
|
// Async Action Creators
|
||||||
|
|
||||||
export const getDashboards = (dashboardID) => (dispatch) => {
|
export const getDashboardsAsync = (dashboardID) => async (dispatch) => {
|
||||||
getDashboardsAJAX().then(({data: {dashboards}}) => {
|
try {
|
||||||
|
const {data: {dashboards}} = await getDashboardsAJAX()
|
||||||
dispatch(loadDashboards(dashboards, dashboardID))
|
dispatch(loadDashboards(dashboards, dashboardID))
|
||||||
})
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const putDashboard = () => (dispatch, getState) => {
|
export const putDashboard = () => (dispatch, getState) => {
|
||||||
|
@ -109,6 +131,18 @@ export const updateDashboardCell = (cell) => (dispatch) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const deleteDashboardAsync = (dashboard) => async (dispatch) => {
|
||||||
|
dispatch(deleteDashboard(dashboard))
|
||||||
|
try {
|
||||||
|
await deleteDashboardAJAX(dashboard)
|
||||||
|
dispatch(publishNotification('success', 'Dashboard deleted successfully.'))
|
||||||
|
dispatch(delayDismissNotification('success', SHORT_NOTIFICATION_DISAPPEARING_DELAY))
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(deleteDashboardFailed(dashboard))
|
||||||
|
dispatch(publishNotification('error', `Failed to delete dashboard: ${error.data.message}.`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const addDashboardCellAsync = (dashboard) => async (dispatch) => {
|
export const addDashboardCellAsync = (dashboard) => async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
const {data} = await addDashboardCellAJAX(dashboard, NEW_DEFAULT_DASHBOARD_CELL)
|
const {data} = await addDashboardCellAJAX(dashboard, NEW_DEFAULT_DASHBOARD_CELL)
|
||||||
|
|
|
@ -36,6 +36,18 @@ export const createDashboard = async (dashboard) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const deleteDashboard = async (dashboard) => {
|
||||||
|
try {
|
||||||
|
return await AJAX({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: dashboard.links.self,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const addDashboardCell = async (dashboard, cell) => {
|
export const addDashboardCell = async (dashboard, cell) => {
|
||||||
try {
|
try {
|
||||||
return await AJAX({
|
return await AJAX({
|
||||||
|
|
|
@ -24,10 +24,10 @@ const {
|
||||||
|
|
||||||
const DashboardPage = React.createClass({
|
const DashboardPage = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
source: PropTypes.shape({
|
source: shape({
|
||||||
links: PropTypes.shape({
|
links: shape({
|
||||||
proxy: PropTypes.string,
|
proxy: string,
|
||||||
self: PropTypes.string,
|
self: string,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
params: shape({
|
params: shape({
|
||||||
|
@ -39,7 +39,7 @@ const DashboardPage = React.createClass({
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
dashboardActions: shape({
|
dashboardActions: shape({
|
||||||
putDashboard: func.isRequired,
|
putDashboard: func.isRequired,
|
||||||
getDashboards: func.isRequired,
|
getDashboardsAsync: func.isRequired,
|
||||||
setDashboard: func.isRequired,
|
setDashboard: func.isRequired,
|
||||||
setTimeRange: func.isRequired,
|
setTimeRange: func.isRequired,
|
||||||
addDashboardCellAsync: func.isRequired,
|
addDashboardCellAsync: func.isRequired,
|
||||||
|
@ -49,11 +49,11 @@ const DashboardPage = React.createClass({
|
||||||
dashboards: arrayOf(shape({
|
dashboards: arrayOf(shape({
|
||||||
id: number.isRequired,
|
id: number.isRequired,
|
||||||
cells: arrayOf(shape({})).isRequired,
|
cells: arrayOf(shape({})).isRequired,
|
||||||
})).isRequired,
|
})),
|
||||||
dashboard: shape({
|
dashboard: shape({
|
||||||
id: number.isRequired,
|
id: number.isRequired,
|
||||||
cells: arrayOf(shape({})).isRequired,
|
cells: arrayOf(shape({})).isRequired,
|
||||||
}).isRequired,
|
}),
|
||||||
handleChooseAutoRefresh: func.isRequired,
|
handleChooseAutoRefresh: func.isRequired,
|
||||||
autoRefresh: number.isRequired,
|
autoRefresh: number.isRequired,
|
||||||
timeRange: shape({}).isRequired,
|
timeRange: shape({}).isRequired,
|
||||||
|
@ -84,10 +84,10 @@ const DashboardPage = React.createClass({
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {
|
const {
|
||||||
params: {dashboardID},
|
params: {dashboardID},
|
||||||
dashboardActions: {getDashboards},
|
dashboardActions: {getDashboardsAsync},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
getDashboards(dashboardID)
|
getDashboardsAsync(dashboardID)
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
|
@ -224,30 +224,38 @@ const DashboardPage = React.createClass({
|
||||||
onAddCell={this.handleAddCell}
|
onAddCell={this.handleAddCell}
|
||||||
onEditDashboard={this.handleEditDashboard}
|
onEditDashboard={this.handleEditDashboard}
|
||||||
>
|
>
|
||||||
{(dashboards).map((d, i) => {
|
{
|
||||||
return (
|
dashboards ?
|
||||||
<li key={i}>
|
dashboards.map((d, i) => {
|
||||||
<Link to={`/sources/${sourceID}/dashboards/${d.id}`} className="role-option">
|
return (
|
||||||
{d.name}
|
<li key={i}>
|
||||||
</Link>
|
<Link to={`/sources/${sourceID}/dashboards/${d.id}`} className="role-option">
|
||||||
</li>
|
{d.name}
|
||||||
);
|
</Link>
|
||||||
})}
|
</li>
|
||||||
|
);
|
||||||
|
}) :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Header>
|
</Header>
|
||||||
}
|
}
|
||||||
<Dashboard
|
{
|
||||||
dashboard={dashboard}
|
dashboard ?
|
||||||
inPresentationMode={inPresentationMode}
|
<Dashboard
|
||||||
source={source}
|
dashboard={dashboard}
|
||||||
autoRefresh={autoRefresh}
|
inPresentationMode={inPresentationMode}
|
||||||
timeRange={timeRange}
|
source={source}
|
||||||
onPositionChange={this.handleUpdatePosition}
|
autoRefresh={autoRefresh}
|
||||||
onEditCell={this.handleEditDashboardCell}
|
timeRange={timeRange}
|
||||||
onRenameCell={this.handleRenameDashboardCell}
|
onPositionChange={this.handleUpdatePosition}
|
||||||
onUpdateCell={this.handleUpdateDashboardCell}
|
onEditCell={this.handleEditDashboardCell}
|
||||||
onDeleteCell={this.handleDeleteDashboardCell}
|
onRenameCell={this.handleRenameDashboardCell}
|
||||||
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
|
onUpdateCell={this.handleUpdateDashboardCell}
|
||||||
/>
|
onDeleteCell={this.handleDeleteDashboardCell}
|
||||||
|
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
import React, {PropTypes} from 'react'
|
import React, {PropTypes} from 'react'
|
||||||
import {Link, withRouter} from 'react-router'
|
import {Link, withRouter} from 'react-router'
|
||||||
import SourceIndicator from '../../shared/components/SourceIndicator'
|
import {connect} from 'react-redux'
|
||||||
|
import {bindActionCreators} from 'redux'
|
||||||
|
|
||||||
|
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||||
|
import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
|
||||||
|
|
||||||
|
import {createDashboard} from 'src/dashboards/apis'
|
||||||
|
import {getDashboardsAsync, deleteDashboardAsync} from 'src/dashboards/actions'
|
||||||
|
|
||||||
import {getDashboards, createDashboard} from '../apis'
|
|
||||||
import {NEW_DASHBOARD} from 'src/dashboards/constants'
|
import {NEW_DASHBOARD} from 'src/dashboards/constants'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
arrayOf,
|
||||||
func,
|
func,
|
||||||
string,
|
string,
|
||||||
shape,
|
shape,
|
||||||
|
@ -26,22 +33,13 @@ const DashboardsPage = React.createClass({
|
||||||
push: func.isRequired,
|
push: func.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
addFlashMessage: func,
|
addFlashMessage: func,
|
||||||
},
|
handleGetDashboards: func.isRequired,
|
||||||
|
handleDeleteDashboard: func.isRequired,
|
||||||
getInitialState() {
|
dashboards: arrayOf(shape()),
|
||||||
return {
|
|
||||||
dashboards: [],
|
|
||||||
waiting: true,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
getDashboards().then((resp) => {
|
this.props.handleGetDashboards()
|
||||||
this.setState({
|
|
||||||
dashboards: resp.data.dashboards,
|
|
||||||
waiting: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleCreateDashbord() {
|
async handleCreateDashbord() {
|
||||||
|
@ -50,15 +48,20 @@ const DashboardsPage = React.createClass({
|
||||||
push(`/sources/${id}/dashboards/${data.id}`)
|
push(`/sources/${id}/dashboards/${data.id}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleDeleteDashboard(dashboard) {
|
||||||
|
this.props.handleDeleteDashboard(dashboard)
|
||||||
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {dashboards} = this.props
|
||||||
const dashboardLink = `/sources/${this.props.source.id}`
|
const dashboardLink = `/sources/${this.props.source.id}`
|
||||||
let tableHeader
|
let tableHeader
|
||||||
if (this.state.waiting) {
|
if (dashboards === null) {
|
||||||
tableHeader = "Loading Dashboards..."
|
tableHeader = "Loading Dashboards..."
|
||||||
} else if (this.state.dashboards.length === 0) {
|
} else if (dashboards.length === 0) {
|
||||||
tableHeader = "1 Dashboard"
|
tableHeader = "1 Dashboard"
|
||||||
} else {
|
} else {
|
||||||
tableHeader = `${this.state.dashboards.length + 1} Dashboards`
|
tableHeader = `${dashboards.length + 1} Dashboards`
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -93,17 +96,20 @@ const DashboardsPage = React.createClass({
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{
|
{
|
||||||
this.state.dashboards.map((dashboard) => {
|
dashboards && dashboards.length ?
|
||||||
|
dashboards.map((dashboard) => {
|
||||||
return (
|
return (
|
||||||
<tr key={dashboard.id}>
|
<tr key={dashboard.id} className="">
|
||||||
<td className="monotype">
|
<td className="monotype">
|
||||||
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
|
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
|
||||||
{dashboard.name}
|
{dashboard.name}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
|
<DeleteConfirmTableCell onDelete={this.handleDeleteDashboard} item={dashboard} />
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})
|
}) :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
<tr>
|
<tr>
|
||||||
<td className="monotype">
|
<td className="monotype">
|
||||||
|
@ -125,4 +131,14 @@ const DashboardsPage = React.createClass({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withRouter(DashboardsPage)
|
const mapStateToProps = ({dashboardUI: {dashboards, dashboard}}) => ({
|
||||||
|
dashboards,
|
||||||
|
dashboard,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
handleGetDashboards: bindActionCreators(getDashboardsAsync, dispatch),
|
||||||
|
handleDeleteDashboard: bindActionCreators(deleteDashboardAsync, dispatch),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(DashboardsPage))
|
||||||
|
|
|
@ -5,7 +5,7 @@ import timeRanges from 'hson!../../shared/data/timeRanges.hson';
|
||||||
const {lower, upper} = timeRanges[1]
|
const {lower, upper} = timeRanges[1]
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
dashboards: [],
|
dashboards: null,
|
||||||
dashboard: EMPTY_DASHBOARD,
|
dashboard: EMPTY_DASHBOARD,
|
||||||
timeRange: {lower, upper},
|
timeRange: {lower, upper},
|
||||||
isEditMode: false,
|
isEditMode: false,
|
||||||
|
@ -48,6 +48,26 @@ export default function ui(state = initialState, action) {
|
||||||
return {...state, ...newState}
|
return {...state, ...newState}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'DELETE_DASHBOARD': {
|
||||||
|
const {dashboard} = action.payload
|
||||||
|
const newState = {
|
||||||
|
dashboards: state.dashboards.filter((d) => d.id !== dashboard.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...state, ...newState}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'DELETE_DASHBOARD_FAILED': {
|
||||||
|
const {dashboard} = action.payload
|
||||||
|
const newState = {
|
||||||
|
dashboards: [
|
||||||
|
_.cloneDeep(dashboard),
|
||||||
|
...state.dashboards,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return {...state, ...newState}
|
||||||
|
}
|
||||||
|
|
||||||
case 'UPDATE_DASHBOARD_CELLS': {
|
case 'UPDATE_DASHBOARD_CELLS': {
|
||||||
const {cells} = action.payload
|
const {cells} = action.payload
|
||||||
const {dashboard} = state
|
const {dashboard} = state
|
||||||
|
|
|
@ -471,6 +471,8 @@ export const STROKE_WIDTH = {
|
||||||
export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // In milliseconds.
|
export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // In milliseconds.
|
||||||
export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds.
|
export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds.
|
||||||
|
|
||||||
|
export const SHORT_NOTIFICATION_DISAPPEARING_DELAY = 1500 // in milliseconds
|
||||||
|
|
||||||
export const RES_UNAUTHORIZED = 401
|
export const RES_UNAUTHORIZED = 401
|
||||||
|
|
||||||
export const AUTOREFRESH_DEFAULT = 15000 // in milliseconds
|
export const AUTOREFRESH_DEFAULT = 15000 // in milliseconds
|
||||||
|
|
Loading…
Reference in New Issue