Merge pull request #1112 from influxdata/1080-delete_dashboard

Add ability to delete a dashboard
pull/10616/head
Hunter Trujillo 2017-03-29 12:54:23 -06:00 committed by GitHub
commit 11158077f0
8 changed files with 174 additions and 57 deletions

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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({

View File

@ -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>
); );
}, },

View File

@ -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))

View File

@ -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

View File

@ -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