Merge pull request #1112 from influxdata/1080-delete_dashboard
Add ability to delete a dashboardpull/1117/head
commit
1d6bf7e36e
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue