Merge pull request #1112 from influxdata/1080-delete_dashboard

Add ability to delete a dashboard
pull/1117/head
Hunter Trujillo 2017-03-29 12:54:23 -06:00 committed by GitHub
commit 1d6bf7e36e
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
### Features
1. [#1112](https://github.com/influxdata/chronograf/pull/1112): Add ability to delete a dashboard
### UI Improvements
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 timeRanges from 'hson!src/shared/data/timeRanges.hson';
import {
loadDashboards,
setDashboard,
deleteDashboard,
deleteDashboardFailed,
setTimeRange,
updateDashboardCells,
editDashboardCell,
@ -50,6 +54,25 @@ describe('DataExplorer.Reducers.UI', () => {
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', () => {
const expected = {upper: null, lower: 'now() - 1h'}
const actual = reducer(state, setTimeRange(expected))

View File

@ -1,11 +1,15 @@
import {
getDashboards as getDashboardsAJAX,
updateDashboard as updateDashboardAJAX,
deleteDashboard as deleteDashboardAJAX,
updateDashboardCell as updateDashboardCellAJAX,
addDashboardCell as addDashboardCellAJAX,
deleteDashboardCell as deleteDashboardCellAJAX,
} 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'
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) => ({
type: 'UPDATE_DASHBOARD_CELLS',
payload: {
@ -89,10 +107,14 @@ export const deleteDashboardCell = (cell) => ({
// Async Action Creators
export const getDashboards = (dashboardID) => (dispatch) => {
getDashboardsAJAX().then(({data: {dashboards}}) => {
export const getDashboardsAsync = (dashboardID) => async (dispatch) => {
try {
const {data: {dashboards}} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards, dashboardID))
})
} catch (error) {
console.error(error)
throw error
}
}
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) => {
try {
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) => {
try {
return await AJAX({

View File

@ -24,10 +24,10 @@ const {
const DashboardPage = React.createClass({
propTypes: {
source: PropTypes.shape({
links: PropTypes.shape({
proxy: PropTypes.string,
self: PropTypes.string,
source: shape({
links: shape({
proxy: string,
self: string,
}),
}),
params: shape({
@ -39,7 +39,7 @@ const DashboardPage = React.createClass({
}).isRequired,
dashboardActions: shape({
putDashboard: func.isRequired,
getDashboards: func.isRequired,
getDashboardsAsync: func.isRequired,
setDashboard: func.isRequired,
setTimeRange: func.isRequired,
addDashboardCellAsync: func.isRequired,
@ -49,11 +49,11 @@ const DashboardPage = React.createClass({
dashboards: arrayOf(shape({
id: number.isRequired,
cells: arrayOf(shape({})).isRequired,
})).isRequired,
})),
dashboard: shape({
id: number.isRequired,
cells: arrayOf(shape({})).isRequired,
}).isRequired,
}),
handleChooseAutoRefresh: func.isRequired,
autoRefresh: number.isRequired,
timeRange: shape({}).isRequired,
@ -84,10 +84,10 @@ const DashboardPage = React.createClass({
componentDidMount() {
const {
params: {dashboardID},
dashboardActions: {getDashboards},
dashboardActions: {getDashboardsAsync},
} = this.props;
getDashboards(dashboardID)
getDashboardsAsync(dashboardID)
},
componentWillReceiveProps(nextProps) {
@ -224,30 +224,38 @@ const DashboardPage = React.createClass({
onAddCell={this.handleAddCell}
onEditDashboard={this.handleEditDashboard}
>
{(dashboards).map((d, i) => {
return (
<li key={i}>
<Link to={`/sources/${sourceID}/dashboards/${d.id}`} className="role-option">
{d.name}
</Link>
</li>
);
})}
{
dashboards ?
dashboards.map((d, i) => {
return (
<li key={i}>
<Link to={`/sources/${sourceID}/dashboards/${d.id}`} className="role-option">
{d.name}
</Link>
</li>
);
}) :
null
}
</Header>
}
<Dashboard
dashboard={dashboard}
inPresentationMode={inPresentationMode}
source={source}
autoRefresh={autoRefresh}
timeRange={timeRange}
onPositionChange={this.handleUpdatePosition}
onEditCell={this.handleEditDashboardCell}
onRenameCell={this.handleRenameDashboardCell}
onUpdateCell={this.handleUpdateDashboardCell}
onDeleteCell={this.handleDeleteDashboardCell}
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
/>
{
dashboard ?
<Dashboard
dashboard={dashboard}
inPresentationMode={inPresentationMode}
source={source}
autoRefresh={autoRefresh}
timeRange={timeRange}
onPositionChange={this.handleUpdatePosition}
onEditCell={this.handleEditDashboardCell}
onRenameCell={this.handleRenameDashboardCell}
onUpdateCell={this.handleUpdateDashboardCell}
onDeleteCell={this.handleDeleteDashboardCell}
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
/> :
null
}
</div>
);
},

View File

@ -1,11 +1,18 @@
import React, {PropTypes} from 'react'
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'
const {
arrayOf,
func,
string,
shape,
@ -26,22 +33,13 @@ const DashboardsPage = React.createClass({
push: func.isRequired,
}).isRequired,
addFlashMessage: func,
},
getInitialState() {
return {
dashboards: [],
waiting: true,
};
handleGetDashboards: func.isRequired,
handleDeleteDashboard: func.isRequired,
dashboards: arrayOf(shape()),
},
componentDidMount() {
getDashboards().then((resp) => {
this.setState({
dashboards: resp.data.dashboards,
waiting: false,
});
});
this.props.handleGetDashboards()
},
async handleCreateDashbord() {
@ -50,15 +48,20 @@ const DashboardsPage = React.createClass({
push(`/sources/${id}/dashboards/${data.id}`)
},
handleDeleteDashboard(dashboard) {
this.props.handleDeleteDashboard(dashboard)
},
render() {
const {dashboards} = this.props
const dashboardLink = `/sources/${this.props.source.id}`
let tableHeader
if (this.state.waiting) {
if (dashboards === null) {
tableHeader = "Loading Dashboards..."
} else if (this.state.dashboards.length === 0) {
} else if (dashboards.length === 0) {
tableHeader = "1 Dashboard"
} else {
tableHeader = `${this.state.dashboards.length + 1} Dashboards`
tableHeader = `${dashboards.length + 1} Dashboards`
}
return (
@ -93,17 +96,20 @@ const DashboardsPage = React.createClass({
</thead>
<tbody>
{
this.state.dashboards.map((dashboard) => {
dashboards && dashboards.length ?
dashboards.map((dashboard) => {
return (
<tr key={dashboard.id}>
<tr key={dashboard.id} className="">
<td className="monotype">
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
{dashboard.name}
</Link>
</td>
<DeleteConfirmTableCell onDelete={this.handleDeleteDashboard} item={dashboard} />
</tr>
);
})
}) :
null
}
<tr>
<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 initialState = {
dashboards: [],
dashboards: null,
dashboard: EMPTY_DASHBOARD,
timeRange: {lower, upper},
isEditMode: false,
@ -48,6 +48,26 @@ export default function ui(state = initialState, action) {
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': {
const {cells} = action.payload
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_NOTIFICATION_DELAY = 2000 // In milliseconds.
export const SHORT_NOTIFICATION_DISAPPEARING_DELAY = 1500 // in milliseconds
export const RES_UNAUTHORIZED = 401
export const AUTOREFRESH_DEFAULT = 15000 // in milliseconds