From 065e5901fde6417d8ec4c06515912641d2b68163 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Wed, 8 Mar 2017 10:54:03 +0100 Subject: [PATCH] Implement rule deletion --- cmd/console/Action.elm | 5 ++- cmd/console/Main.elm | 8 ++-- cmd/console/Rule/Api.elm | 28 +++++++++++++- cmd/console/Rule/Model.elm | 45 +++++++++++++++++++---- cmd/console/Rule/View.elm | 67 +++++++++++++++++++++++++++------- cmd/console/Update.elm | 15 +++++--- cmd/console/View.elm | 61 ++++++++++++++++++++----------- cmd/console/console.go | 17 ++++++++- cmd/console/styles/console.css | 41 +++++++++++++++------ core/rules.go | 38 +++++++++++++++++++ handler/http/rule.go | 26 +++++++++++++ service/rule/postgres.go | 61 ++++++++++++++++++++++++++++++- service/rule/postgres_test.go | 7 ++++ 13 files changed, 351 insertions(+), 68 deletions(-) diff --git a/cmd/console/Action.elm b/cmd/console/Action.elm index 837da7d..f84f0fe 100644 --- a/cmd/console/Action.elm +++ b/cmd/console/Action.elm @@ -18,9 +18,10 @@ type Msg | FetchApps (WebData (List App)) | FetchRule (WebData Rule) | FetchRules (WebData (List Rule)) - | ListApps | LocationChange Location | Navigate Route | NewApp (WebData App) - | SelectApp String + | RuleDeleteAsk String + | RuleDeleteConfirm String + | RuleDelete (WebData Bool) | Tick Time diff --git a/cmd/console/Main.elm b/cmd/console/Main.elm index dfd8fac..79655b5 100644 --- a/cmd/console/Main.elm +++ b/cmd/console/Main.elm @@ -3,6 +3,7 @@ module Main exposing (main) import AnimationFrame import Navigation import Action exposing (Msg(..)) +import Ask exposing (confirm) import Model exposing (Flags, Model, init) import Update exposing (update) import View exposing (view) @@ -18,10 +19,11 @@ main = } - -- SUBSCRIPTION - subscriptions : Model -> Sub Msg subscriptions model = - AnimationFrame.times Tick + Sub.batch + [ AnimationFrame.times Tick + , confirm RuleDeleteConfirm + ] diff --git a/cmd/console/Rule/Api.elm b/cmd/console/Rule/Api.elm index 5d9247e..9c5ee24 100644 --- a/cmd/console/Rule/Api.elm +++ b/cmd/console/Rule/Api.elm @@ -1,13 +1,26 @@ -module Rule.Api exposing (getRule, listRules) +module Rule.Api exposing (deleteRule, getRule, listRules) import Http import RemoteData exposing (WebData, sendRequest) import Rule.Model exposing (Rule, decode, decodeList) +deleteRule : String -> String -> Cmd (WebData Bool) +deleteRule appId ruleId = + Http.request + { body = Http.emptyBody + , expect = Http.expectStringResponse readDelete + , headers = [] + , method = "DELETE" + , timeout = Nothing + , url = ruleUrl appId ruleId + , withCredentials = False + } + |> sendRequest + getRule : String -> String -> Cmd (WebData Rule) getRule appId ruleId = - Http.get ("/api/apps/" ++ appId ++ "/rules/" ++ ruleId) decode + Http.get (ruleUrl appId ruleId) decode |> sendRequest @@ -15,3 +28,14 @@ listRules : String -> Cmd (WebData (List Rule)) listRules appId = Http.get ("/api/apps/" ++ appId ++ "/rules") decodeList |> sendRequest + +readDelete : Http.Response String -> Result String Bool +readDelete response = + if response.status.code == 204 then + Ok True + else + Err response.status.message + +ruleUrl : String -> String -> String +ruleUrl appId ruleId = + "/api/apps/" ++ appId ++ "/rules/" ++ ruleId diff --git a/cmd/console/Rule/Model.elm b/cmd/console/Rule/Model.elm index 4819367..40c4843 100644 --- a/cmd/console/Rule/Model.elm +++ b/cmd/console/Rule/Model.elm @@ -1,4 +1,4 @@ -module Rule.Model exposing (Rule, decode, decodeList, targetString) +module Rule.Model exposing (Entity(..), Rule, decode, decodeList, targetString) import Dict exposing (Dict) import Json.Decode as Decode @@ -6,6 +6,12 @@ import Json.Decode as Decode -- MODEL +type Criteria + = EventCriteria + | ObjectCriteria + +type Entity + = Connection | Event | Object | Reaction | UnknownEntity type alias Recipient = { query : List Query @@ -17,21 +23,40 @@ type alias Recipient = type alias Rule = { active : Bool + , criteria : Criteria , deleted : Bool , ecosystem : Int - , entity : Int + , entity : Entity , id : String , name : String , recipients : List Recipient } type Target - = Commenters | PostOwner | Unknown + = Commenters | PostOwner | UnknownTarget type alias Query = ( String, String ) +matchEntity : Int -> Entity +matchEntity enum = + case enum of + 0 -> + Connection + + 1 -> + Event + + 2 -> + Object + + 3 -> + Reaction + + _ -> + UnknownEntity + matchTarget : Query -> Target matchTarget query = case query of @@ -42,7 +67,7 @@ matchTarget query = PostOwner ( _, _ ) -> - Unknown + UnknownTarget targetString : Target -> String targetString target = @@ -53,7 +78,7 @@ targetString target = PostOwner -> "PostOwner" - Unknown -> + UnknownTarget -> "Unknown" @@ -63,15 +88,21 @@ targetString target = decode : Decode.Decoder Rule decode = - Decode.map7 Rule + Decode.map8 Rule (Decode.field "active" Decode.bool) + (Decode.field "criteria" decodeCriteria) (Decode.field "deleted" Decode.bool) (Decode.field "ecosystem" Decode.int) - (Decode.field "entity" Decode.int) + (Decode.andThen decodeEntity (Decode.field "entity" Decode.int)) (Decode.field "id" Decode.string) (Decode.field "name" Decode.string) (Decode.field "recipients" (Decode.list decodeRecipient)) +decodeCriteria = + Decode.succeed EventCriteria + +decodeEntity raw = + Decode.succeed (matchEntity raw) decodeList : Decode.Decoder (List Rule) decodeList = diff --git a/cmd/console/Rule/View.elm b/cmd/console/Rule/View.elm index 085a080..140ec6e 100644 --- a/cmd/console/Rule/View.elm +++ b/cmd/console/Rule/View.elm @@ -5,6 +5,7 @@ import Html.Attributes exposing (class, title) import Html.Events exposing (onClick) import Rule.Model exposing (Rule) +import Rule.Model exposing (Entity(..)) viewActivated : Bool -> Html msg viewActivated active = @@ -22,22 +23,22 @@ viewEcosystem ecosystem = _ -> span [ class "nc-icon-outline ui-2_alert", title "unknown" ] [] -viewEntity : Int -> Html msg +viewEntity : Entity -> Html msg viewEntity entity = case entity of - 0 -> + Connection -> span [ class "nc-icon-outline arrows-2_conversion", title "Connection" ] [] - 1 -> + Event -> span [ class "nc-icon-outline ui-1_bell-53", title "event" ] [] - 2 -> + Object -> span [ class "nc-icon-outline ui-1_database", title "Object" ] [] - 3 -> + Reaction -> span [ class "nc-icon-outline ui-2_like", title "Reaction" ] [] - _ -> + UnknownEntity -> span [ class "nc-icon-outline ui-2_alert", title "Unknown" ] [] viewRuleDescription : Rule -> Html msg @@ -59,31 +60,31 @@ viewRuleDescription rule = entity = case rule.entity of - 0 -> + Connection -> div [ class "icon", title "Connections" ] [ span [ class "nc-icon-outline arrows-2_conversion" ] [] , span [] [ text "Connections" ] ] - 1 -> + Event -> div [ class "icon", title "Events" ] [ span [ class "nc-icon-outline ui-1_bell-53" ] [] , span [] [ text "Events" ] ] - 2 -> + Object -> div [ class "icon", title "Objects" ] [ span [ class "nc-icon-outline ui-1_database" ] [] , span [] [ text "Objects" ] ] - 3 -> + Reaction -> div [ class "icon", title "Reactions" ] [ span [ class "nc-icon-outline ui-2_like" ] [] , span [] [ text "Reactions" ] ] - _ -> + UnknownEntity -> div [ class "icon", title "Unknown" ] [ span [ class "nc-icon-outline ui-2_alert" ] [] , span [] [ text "Unknown" ] @@ -115,9 +116,9 @@ viewRuleTable : (Rule -> Html msg) -> List Rule -> Html msg viewRuleTable item rules = let list = - List.sortBy .entity rules + List.sortWith sortByEntity rules in - table [] + table [ class "navigation" ] [ thead [] [ tr [] [ th [ class "icon" ] [ text "active" ] @@ -129,3 +130,43 @@ viewRuleTable item rules = ] , tbody [] (List.map item list) ] + + +sortByEntity : Rule -> Rule -> Order +sortByEntity a b = + case (a.entity, b.entity) of + (Connection, Connection) -> + EQ + + (Connection, _) -> + LT + + (Event, Connection) -> + GT + + (Event, Event) -> + EQ + + (Event, _) -> + LT + + (Object, Connection) -> + GT + + (Object, Event) -> + GT + + (Object, Object) -> + EQ + + (Object, _) -> + LT + + (Reaction, Reaction) -> + EQ + + (Reaction, _) -> + GT + + (UnknownEntity, _) -> + GT \ No newline at end of file diff --git a/cmd/console/Update.elm b/cmd/console/Update.elm index 3181492..1b6f1e2 100644 --- a/cmd/console/Update.elm +++ b/cmd/console/Update.elm @@ -2,11 +2,13 @@ module Update exposing (update) import RemoteData exposing (RemoteData(Loading, NotAsked), WebData) import Action exposing (Msg(..)) +import Ask exposing (ask) import Formo exposing (blurElement, elementValue, focusElement, updateElementValue, validateForm) import Model exposing (Flags, Model, init) import App.Api exposing (createApp) import App.Model exposing (initAppForm) import Route +import Rule.Api exposing (deleteRule) update : Msg -> Model -> ( Model, Cmd Msg ) @@ -48,9 +50,6 @@ update msg model = FetchRules response -> ( { model | rules = response }, Cmd.none ) - ListApps -> - ( model, Cmd.map LocationChange (Route.navigate Route.Apps) ) - LocationChange location -> init (Flags model.zone) location @@ -60,8 +59,14 @@ update msg model = NewApp response -> ( { model | appForm = initAppForm, apps = (appendWebData model.apps response), newApp = NotAsked }, Cmd.none ) - SelectApp id -> - ( model, Cmd.map LocationChange (Route.navigate (Route.App id)) ) + RuleDeleteAsk id -> + ( model, ask id ) + + RuleDeleteConfirm id -> + ( model, Cmd.map RuleDelete (deleteRule model.appId id) ) + + RuleDelete _ -> + ( model, Cmd.map LocationChange (Route.navigate (Route.Rules model.appId)) ) Tick time -> let diff --git a/cmd/console/View.elm b/cmd/console/View.elm index 177d403..2e03ee5 100644 --- a/cmd/console/View.elm +++ b/cmd/console/View.elm @@ -56,28 +56,19 @@ view model = pageApp : Model -> Html Msg pageApp { app, startTime, time } = let + viewEntities app = + List.map viewEntity + [ ( app.counts.rules, "Rules", "education_book-39", (Navigate (Route.Rules app.id)) ) + , ( app.counts.users, "Users", "users_multiple-11", (Navigate (Route.Users app.id)) ) + ] + viewApp app = div [] [ h3 [] [ text app.name ] , p [] [ text app.description ] - , ul [ class "entities" ] - [ li [] - [ a [ onClick (Navigate (Route.Rules app.id)), title "Rules" ] - [ span [ class "icon nc-icon-glyph education_book-39" ] [] - , span [] [ text "Rules -" ] - , span [ class "count" ] [ text (toString app.counts.rules) ] - ] - ] - , li [] - [ a [ onClick (Navigate (Route.Users app.id)), title "Rules" ] - [ span [ class "icon nc-icon-glyph users_multiple-11" ] [] - , span [] [ text "Users -" ] - , span [ class "count" ] [ text (toString app.counts.users) ] - ] - ] - ] + , ul [ class "entities" ] (viewEntities app) ] in @@ -92,7 +83,7 @@ pageApps : Model -> Html Msg pageApps { app, apps, appForm, newApp, startTime, time } = let viewItem = - (\app -> viewAppItem (SelectApp app.id) app) + (\app -> viewAppItem (Navigate (Route.App app.id)) app) viewApps apps = if List.length apps == 0 then @@ -178,14 +169,30 @@ pageRule { app, appId, rule, startTime, time } = ] ) , div [ class "templates" ] - [ span [] [ text "Templates: " ] - , strong [] [ text (toString (List.length (Dict.toList recipient.templates))) ] + [ viewTemplates recipient.templates + ] + ] + + viewActions rule = + ul [ class "actions" ] + [ li [] + [ a [] + [ span [ class "icon nc-icon-glyph ui-1_edit-76" ] [] + , span [] [ text "edit" ] + ] + ] + , li [] + [ a [ onClick (RuleDeleteAsk rule.id) ] + [ span [ class "icon nc-icon-glyph ui-1_trash" ] [] + , span [] [ text "delete" ] + ] ] ] viewRule rule = div [] - [ viewRuleDescription rule + [ viewActions rule + , viewRuleDescription rule , h4 [] [ span [ class "icon nc-icon-outline users_mobile-contact" ] [] , span [] [ text "Recipients" ] @@ -256,7 +263,7 @@ viewContextApps app = viewContextRules : String -> WebData Rule -> Html Msg viewContextRules appId rule = let - ( selected, viewRule ) = + ( _, viewRule ) = case rule of Success rule -> ( True, viewSelected (Navigate (Route.Rule appId rule.id)) rule.name ) @@ -264,7 +271,7 @@ viewContextRules appId rule = _ -> ( False, span [] [] ) in - viewContext "Rules" (Navigate (Route.Rules appId)) viewRule selected "education_book-39" + viewContext "Rules" (Navigate (Route.Rules appId)) viewRule False "education_book-39" viewDebug : Model -> Html Msg @@ -273,6 +280,16 @@ viewDebug model = [ text (toString model) ] +viewEntity : ( Int, String, String, Msg ) -> Html Msg +viewEntity ( count, entity, icon, msg ) = + li [] + [ a [ onClick msg, title entity ] + [ div [ class "icon" ] + [ span [ class ("icon nc-icon-glyph " ++ icon) ] [] ] + , div [] [ span [ class "count" ] [ text (toString count) ] ] + , div [] [ span [] [ text entity ] ] + ] + ] viewHeader : String -> Html Msg viewHeader zone = diff --git a/cmd/console/console.go b/cmd/console/console.go index eee9391..b0676cb 100644 --- a/cmd/console/console.go +++ b/cmd/console/console.go @@ -57,7 +57,15 @@ var ( ` @@ -199,6 +207,13 @@ func main() { ), ) + router.Methods("DELETE").Path("/api/apps/{appID:[0-9]+}/rules/{ruleID:[0-9]+}").Name("ruleDelete").HandlerFunc( + handler.Wrap( + withConstraints, + handler.RuleDelete(core.RuleDelete(apps, rules)), + ), + ) + router.Methods("GET").Path("/api/apps/{appID:[0-9]+}/rules/{ruleID:[0-9]+}").Name("ruleRetrieve").HandlerFunc( handler.Wrap( withConstraints, diff --git a/cmd/console/styles/console.css b/cmd/console/styles/console.css index 23dfb70..94786ce 100644 --- a/cmd/console/styles/console.css +++ b/cmd/console/styles/console.css @@ -306,14 +306,9 @@ table tbody { table tbody tr { border-bottom: 0.1rem solid #efefef; - cursor: pointer; transition: all 0.15s ease; } -table tbody tr:hover { - background: rgb(236, 240, 241); -} - table tbody tr td { padding: 1.4rem 0.8rem 0.8rem 0.4rem; } @@ -324,6 +319,33 @@ table th.icon { width: 7rem; } +table.navigation tbody tr { + cursor: pointer; +} + +table.navigation tbody tr:hover { + background: rgb(236, 240, 241); +} + +ul.actions { + display: flex; + justify-content: flex-end; + padding: 0; +} + +ul.actions li { + font-size: 2rem; + font-weight: 500; + list-style: none; + margin: 0 0 0 2rem; + padding: 0; + text-transform: capitalize; +} + +ul.actions li span.icon { + margin-right: 0.4rem; +} + ul.entities { font-size: 2.2rem; list-style: none; @@ -335,11 +357,6 @@ ul.entities li { margin-bottom: 1.2rem; } -ul.entities span.count { - font-weight: 500; - margin-left: 0.4rem; +ul.entities li a { + display: flex; } - -ul.entities span.icon { - margin-right: 0.4rem; -} \ No newline at end of file diff --git a/core/rules.go b/core/rules.go index d3ff435..09cd467 100644 --- a/core/rules.go +++ b/core/rules.go @@ -5,6 +5,44 @@ import ( "github.com/tapglue/snaas/service/rule" ) +// RuleDeleteFunc removes the rule permanently. +type RuleDeleteFunc func(appID, id uint64) error + +func RuleDelete( + apps app.Service, + rules rule.Service, +) RuleDeleteFunc { + return func(appID, id uint64) error { + currentApp, err := AppFetch(apps)(appID) + if err != nil { + return err + } + + rs, err := rules.Query(currentApp.Namespace(), rule.QueryOptions{ + IDs: []uint64{ + id, + }, + }) + if err != nil { + return err + } + + if len(rs) == 0 { + return wrapError(ErrNotFound, "rule (%d) not found", id) + } + + r := rs[0] + r.Deleted = true + + _, err = rules.Put(currentApp.Namespace(), r) + if err != nil { + return err + } + + return nil + } +} + // RuleFetchFunc returns the Rule for the given appID and id. type RuleFetchFunc func(appID, id uint64) (*rule.Rule, error) diff --git a/handler/http/rule.go b/handler/http/rule.go index f41f6ac..9e25767 100644 --- a/handler/http/rule.go +++ b/handler/http/rule.go @@ -11,6 +11,30 @@ import ( "github.com/tapglue/snaas/service/rule" ) +func RuleDelete(fn core.RuleDeleteFunc) Handler { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { + appID, err := extractAppID(r) + if err != nil { + respondError(w, 0, wrapError(ErrBadRequest, err.Error())) + return + } + + ruleID, err := extractRuleID(r) + if err != nil { + respondError(w, 0, wrapError(ErrBadRequest, err.Error())) + return + } + + err = fn(appID, ruleID) + if err != nil { + respondError(w, 0, err) + return + } + + respondJSON(w, http.StatusNoContent, nil) + } +} + // RuleList returns all rules. func RuleList(fn core.RuleListFunc) Handler { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) { @@ -66,6 +90,7 @@ type payloadRule struct { func (p *payloadRule) MarshalJSON() ([]byte, error) { return json.Marshal(struct { Active bool `json:"active"` + Criteria interface{} `json:"criteria"` Deleted bool `json:"deleted"` Ecosystem int `json:"ecosystem"` Entity int `json:"entity"` @@ -74,6 +99,7 @@ func (p *payloadRule) MarshalJSON() ([]byte, error) { Recipients rule.Recipients `json:"recipients"` }{ Active: p.rule.Active, + Criteria: p.rule.Criteria, Deleted: p.rule.Deleted, Ecosystem: int(p.rule.Ecosystem), Entity: int(p.rule.Type), diff --git a/service/rule/postgres.go b/service/rule/postgres.go index 775cd8c..d35c008 100644 --- a/service/rule/postgres.go +++ b/service/rule/postgres.go @@ -14,6 +14,20 @@ const ( pgInsertRule = `INSERT INTO %s.rules(active, criteria, deleted, ecosystem, id, name, recipients, type, created_at, updated_at) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` + pgUpdateRule = ` + UPDATE + %s.rules + SET + active = $2, + criteria = $3, + deleted = $4, + ecosystem = $5, + name = $6, + recipients = $7, + type = $8, + updated_at = $9 + WHERE + id = $1` pgClauseActive = `active = ?` pgClauseDeleted = `deleted = ?` @@ -64,7 +78,7 @@ func (s *pgService) Put(ns string, r *Rule) (*Rule, error) { return s.insert(ns, r) } - return nil, fmt.Errorf("Put/Update not implementee") + return s.update(ns, r) } func (s *pgService) Query(ns string, opts QueryOptions) (List, error) { @@ -251,6 +265,51 @@ func (s *pgService) listRules( return rs, nil } +func (s *pgService) update(ns string, r *Rule) (*Rule, error) { + now, err := time.Parse(pg.TimeFormat, time.Now().UTC().Format(pg.TimeFormat)) + if err != nil { + return nil, err + } + + r.UpdatedAt = now + + criteria, err := json.Marshal(r.Criteria) + if err != nil { + return nil, err + } + + recipients, err := json.Marshal(r.Recipients) + if err != nil { + return nil, err + } + + var ( + params = []interface{}{ + r.ID, + r.Active, + criteria, + r.Deleted, + r.Ecosystem, + r.Name, + recipients, + r.Type, + r.UpdatedAt, + } + query = fmt.Sprintf(pgUpdateRule, ns) + ) + + _, err = s.db.Exec(query, params...) + if err != nil && pg.IsRelationNotFound(pg.WrapError(err)) { + if err := s.Setup(ns); err != nil { + return nil, err + } + + _, err = s.db.Exec(query, params...) + } + + return r, err +} + func convertOpts(opts QueryOptions) (string, []interface{}, error) { var ( clauses = []string{} diff --git a/service/rule/postgres_test.go b/service/rule/postgres_test.go index 60958dd..bacc10a 100644 --- a/service/rule/postgres_test.go +++ b/service/rule/postgres_test.go @@ -195,6 +195,13 @@ func TestPostgresPut(t *testing.T) { if have, want := list[0], created; !reflect.DeepEqual(have, want) { t.Errorf("\nhave %v\nwant %v", have, want) } + + created.Deleted = true + + _, err = service.Put(namespace, created) + if err != nil { + t.Fatal(err) + } } }