Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report Bulk Delete #138

Merged
merged 9 commits into from
Oct 31, 2019
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement delete bulk report
bojand committed Oct 29, 2019
commit a4d954abdb36cd211cf3ae2f727b1f6b8c7015d4
4 changes: 2 additions & 2 deletions web/api/project_test.go
Original file line number Diff line number Diff line change
@@ -242,7 +242,7 @@ func TestProjectAPI(t *testing.T) {

t.Run("ListProjects", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPut, "/", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
@@ -273,7 +273,7 @@ func TestProjectAPI(t *testing.T) {

t.Run("ListProjects sorted", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPut, "/?sort=id&order=asc", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodGet, "/?sort=id&order=asc", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
32 changes: 32 additions & 0 deletions web/api/report.go
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ type ReportDatabase interface {
FindReportByID(uint) (*model.Report, error)
FindPreviousReport(uint) (*model.Report, error)
DeleteReport(*model.Report) error
DeleteReportBulk([]uint) (int, error)
ListReports(limit, page uint, sortField, order string) ([]*model.Report, error)
ListReportsForProject(pid, limit, page uint, sortField, order string) ([]*model.Report, error)
}
@@ -31,6 +32,11 @@ type ReportList struct {
Data []*model.Report `json:"data"`
}

// DeleteReportBulkRequest is the request to delete bulk reports
type DeleteReportBulkRequest struct {
IDs []uint `json:"ids"`
}

// ListReportsForProject lists reports for a project
func (api *ReportAPI) ListReportsForProject(ctx echo.Context) error {
var projectID uint64
@@ -159,6 +165,32 @@ func (api *ReportAPI) DeleteReport(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, report)
}

// DeleteReportBulk deletes bulk reports
func (api *ReportAPI) DeleteReportBulk(ctx echo.Context) error {
del := new(DeleteReportBulkRequest)
var err error

if err := ctx.Bind(del); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

if ctx.Echo().Validator != nil {
if err := ctx.Validate(del); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
}

n, err := api.DB.DeleteReportBulk(del.IDs)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

m := make(map[string]int)
m["deleted"] = n

return ctx.JSON(http.StatusOK, m)
}

// GetPreviousReport gets a previous report
func (api *ReportAPI) GetPreviousReport(ctx echo.Context) error {
var id uint64
37 changes: 30 additions & 7 deletions web/api/report_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package api

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
@@ -462,7 +463,7 @@ func TestReportAPI(t *testing.T) {

t.Run("ListReportsAll", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPut, "/", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
@@ -488,7 +489,7 @@ func TestReportAPI(t *testing.T) {

t.Run("ListReportsAll page 1", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPut, "/?page=1", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodGet, "/?page=1", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
@@ -516,7 +517,7 @@ func TestReportAPI(t *testing.T) {

t.Run("ListReportsAll page 2 empty", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPut, "/?page=2", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodGet, "/?page=2", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
@@ -538,7 +539,7 @@ func TestReportAPI(t *testing.T) {

t.Run("ListReportsForProject p1", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPut, "/"+pid+"/reports", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodGet, "/"+pid+"/reports", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
@@ -567,7 +568,7 @@ func TestReportAPI(t *testing.T) {

t.Run("ListReportsForProject p2", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPut, "/"+pid2+"/reports", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodGet, "/"+pid2+"/reports", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
@@ -596,7 +597,7 @@ func TestReportAPI(t *testing.T) {

t.Run("ListReportsForProject p1 page 1", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPut, "/"+pid+"/reports?page=1", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodGet, "/"+pid+"/reports?page=1", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
@@ -623,7 +624,7 @@ func TestReportAPI(t *testing.T) {

t.Run("ListReportsForProject p2 page 1 empty", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPut, "/"+pid2+"/reports?page=1", strings.NewReader(`{}`))
req := httptest.NewRequest(http.MethodGet, "/"+pid2+"/reports?page=1", strings.NewReader(`{}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
@@ -644,4 +645,26 @@ func TestReportAPI(t *testing.T) {
assert.Empty(t, list.Data)
}
})

t.Run("DeleteReportBulk p2", func(t *testing.T) {
e := echo.New()
idsStr := fmt.Sprintf(`[%+v, 123, %+v]`, p2reportID5, p2reportID6)
reqJSON := `{"ids":` + idsStr + `}`
req := httptest.NewRequest(http.MethodPost, "/bulk_delete", strings.NewReader(reqJSON))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()

c := e.NewContext(req, rec)

if assert.NoError(t, api.DeleteReportBulk(c)) {
assert.Equal(t, http.StatusOK, rec.Code)

m := make(map[string]int)
err = json.NewDecoder(rec.Body).Decode(&m)

assert.NoError(t, err)
assert.Equal(t, 2, m["deleted"])
}
})
}
26 changes: 23 additions & 3 deletions web/database/report.go
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ func (d *Database) DeleteReport(r *model.Report) error {
}

// DeleteReportBulk performans a bulk of deletes
func (d *Database) DeleteReportBulk(ids []uint) error {
func (d *Database) DeleteReportBulk(ids []uint) (int, error) {
nItems := len(ids)
ids2 := make([]string, nItems, nItems)
for i, id := range ids {
@@ -52,16 +52,36 @@ func (d *Database) DeleteReportBulk(ids []uint) error {

query := "id IN ("
for i, id := range ids2 {

query += id
if i < nItems-1 {
query += ", "
}
}
query += ")"

existing := make([]*model.Report, 0, nItems)

err := d.DB.Where(query).Find(&existing).Error
if err != nil {
return 0, err
}

nExisting := len(existing)
query = "id IN ("
for i, rep := range existing {
query += strconv.FormatUint(uint64(rep.ID), 10)
if i < nExisting-1 {
query += ", "
}
}
query += ")"

return d.DB.Where(query).Delete(&model.Report{}).Error
err = d.DB.Where(query).Delete(&model.Report{}).Error
if err != nil {
return 0, err
}

return nExisting, err
}

// FindPreviousReport find previous report for the report id
7 changes: 4 additions & 3 deletions web/database/report_test.go
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ func TestDatabase_Report(t *testing.T) {

defer os.Remove(dbName)

db, err := New("sqlite3", dbName, false)
db, err := New("sqlite3", dbName, true)
if err != nil {
assert.FailNow(t, err.Error())
}
@@ -517,11 +517,12 @@ func TestDatabase_Report(t *testing.T) {
})

t.Run("DeleteReportBulk()", func(t *testing.T) {
ids := []uint{rid, rid2}
ids := []uint{rid, 123, rid2}

err := db.DeleteReportBulk(ids)
n, err := db.DeleteReportBulk(ids)

assert.NoError(t, err)
assert.Equal(t, 2, n)

r2 := new(model.Report)
err = db.DB.First(r2, rid).Error
1 change: 1 addition & 0 deletions web/router/router.go
Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@ func New(db *database.Database, appInfo *api.ApplicationInfo, conf *config.Confi
reportGroup.GET("/:rid/", reportAPI.GetReport).Name = "ghz api: get report"
reportGroup.DELETE("/:rid/", reportAPI.DeleteReport).Name = "ghz api: delete report"
reportGroup.GET("/:rid/previous/", reportAPI.GetPreviousReport).Name = "ghz api: get previous report"
reportGroup.POST("/bulk_delete/", reportAPI.DeleteReportBulk).Name = "ghz api: delete bulk report"

optionsAPI := api.OptionsAPI{DB: db}
reportGroup.GET("/:rid/options/", optionsAPI.GetOptions).Name = "ghz api: get options"
16 changes: 9 additions & 7 deletions web/ui/src/components/ProjectDetailPane.jsx
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ class ProjectDetailPane extends Component {
const ok = await this.props.projectStore.deleteProject(id)
if (ok) {
toaster.success(`Project ${name} deleted.`)
this.props.history.push(`/projects`)
this.props.history.push('/projects')
}
}

@@ -62,14 +62,15 @@ class ProjectDetailPane extends Component {
project={currentProject}
isShown={this.state.editProjectVisible}
onDone={() => this.setState({ editProjectVisible: false })}
/> : null
}
/> : null}
<Button
onClick={() => this.setState({ editProjectVisible: !this.state.editProjectVisible })}
marginLeft={14}
iconBefore='edit'
appearance='minimal'
intent='none'>EDIT</Button>
intent='none'
>EDIT
</Button>
</Pane>
<Pane display='flex'>
{this.state.deleteVisible
@@ -79,13 +80,14 @@ class ProjectDetailPane extends Component {
isShown={this.state.deleteVisible}
onConfirm={() => this.deleteProject()}
onCancel={() => this.setState({ deleteVisible: !this.state.deleteVisible })}
/> : null
}
/> : null}
<Button
iconBefore='trash'
appearance='minimal'
intent='danger'
onClick={() => this.setState({ deleteVisible: !this.state.deleteVisible })}>DELETE</Button>
onClick={() => this.setState({ deleteVisible: !this.state.deleteVisible })}
>DELETE
</Button>
</Pane>
</Pane>
<Paragraph>{currentProject.description}</Paragraph>
16 changes: 12 additions & 4 deletions web/ui/src/components/ReportList.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Component } from 'react'
import { Table, Heading, IconButton, Pane, Tooltip, Button, Text, Checkbox } from 'evergreen-ui'
import { Table, Heading, IconButton, Pane, Tooltip, Button, Text, Checkbox, toaster } from 'evergreen-ui'
import { Link as RouterLink, withRouter } from 'react-router-dom'
import { format as formatAgo } from 'timeago.js'

@@ -14,7 +14,7 @@ import {
import StatusBadge from './StatusBadge'

class ReportList extends Component {
constructor (props) {
constructor(props) {
super(props)

this.state = {
@@ -60,8 +60,15 @@ class ReportList extends Component {
}
}

deleteBulk () {
console.log('delete bulk')
async deleteBulk () {
console.log(this.state.selected)
const selectedIds = (Object.keys(this.state.selected)).map(v => Number.parseInt(v))
const res = await this.props.reportStore.deleteReports(selectedIds)
if (res && typeof res.deleted === 'number') {
toaster.success(`Deleted ${res.deleted} reports.`)
this.props.reportStore.fetchReports(
this.state.ordering, this.state.sort, this.state.page, this.state.projectId)
}
}

fetchPage (page) {
@@ -91,6 +98,7 @@ class ReportList extends Component {

onCheckChange (id, checked) {
console.log(id, checked)

const { selected } = this.state
if (checked) {
selected[id] = checked
18 changes: 18 additions & 0 deletions web/ui/src/containers/ReportContainer.js
Original file line number Diff line number Diff line change
@@ -88,4 +88,22 @@ export default class ReportContainer extends Container {
console.log('error: ', err)
}
}

async deleteReports (ids) {
this.setState({
isFetching: true
})

try {
const res = await api.post('reports/bulk_delete', { json: { ids } }).json()
this.setState({
isFetching: false
})

return res
} catch (err) {
toaster.danger(err.message)
console.log('error: ', err)
}
}
}