Skip to content

Commit 71d0b00

Browse files
committed
Add webhook explainer modal
1 parent 64d6e93 commit 71d0b00

File tree

8 files changed

+396
-6
lines changed

8 files changed

+396
-6
lines changed

src/UnisonShare/Api.elm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,14 @@ deleteProjectRoleAssignment projectRef collaborator =
485485
-- PROJECT WEBHOOKS
486486

487487

488+
webhookExamples : Endpoint
489+
webhookExamples =
490+
GET
491+
{ path = [ "webhooks", "examples" ]
492+
, queryParams = []
493+
}
494+
495+
488496
projectWebhooks : ProjectRef -> Endpoint
489497
projectWebhooks projectRef =
490498
let

src/UnisonShare/ErrorDetails.elm

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ view session error =
2020

2121

2222

23-
{- details []
24-
[ summary [] [ text "Error Details" ]
25-
, pre [] [ text (Util.httpErrorToString error) ]
26-
]
23+
{-
24+
details []
25+
[ summary [] [ text "Error Details" ]
26+
, pre [] [ text (Util.httpErrorToString error) ]
27+
]
2728
-}

src/UnisonShare/Page/ProjectSettingsPage.elm

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef)
3737
import UnisonShare.ProjectCollaborator as ProjectCollaborator exposing (ProjectCollaborator)
3838
import UnisonShare.ProjectRole as ProjectRole
3939
import UnisonShare.ProjectWebhook as ProjectWebhook exposing (ProjectWebhook)
40+
import UnisonShare.ProjectWebhookExamplesModal as ProjectWebhookExamplesModal
4041
import UnisonShare.Session as Session exposing (Session)
4142
import UnisonShare.User as User exposing (UserSummary)
4243
import Url
@@ -66,6 +67,7 @@ type Modal
6667
= NoModal
6768
| AddCollaboratorModal AddProjectCollaboratorModal.Model
6869
| AddWebhookModal AddProjectWebhookModal.Model
70+
| WebhookExamplesModal ProjectWebhookExamplesModal.Model
6971

7072

7173
type ProjectOwner
@@ -135,13 +137,15 @@ type Msg
135137
| ShowDeleteProjectModal
136138
| ShowAddCollaboratorModal
137139
| ShowAddWebhookModal
140+
| ShowWebhookExamplesModal
138141
| CloseModal
139142
| RemoveCollaborator ProjectCollaborator
140143
| RemoveWebhook ProjectWebhook
141144
| RemoveCollaboratorFinished (HttpResult ())
142145
| RemoveWebhookFinished (HttpResult ())
143146
| AddProjectCollaboratorModalMsg AddProjectCollaboratorModal.Msg
144147
| AddProjectWebhookModalMsg AddProjectWebhookModal.Msg
148+
| ProjectWebhookExamplesModalMsg ProjectWebhookExamplesModal.Msg
145149

146150

147151
type OutMsg
@@ -206,6 +210,13 @@ update appContext project msg model =
206210
( ShowAddWebhookModal, _ ) ->
207211
( { model | modal = AddWebhookModal AddProjectWebhookModal.init }, Cmd.none, None )
208212

213+
( ShowWebhookExamplesModal, _ ) ->
214+
let
215+
( examples, cmd ) =
216+
ProjectWebhookExamplesModal.init appContext project.ref
217+
in
218+
( { model | modal = WebhookExamplesModal examples }, Cmd.map ProjectWebhookExamplesModalMsg cmd, None )
219+
209220
( CloseModal, _ ) ->
210221
( { model | modal = NoModal }, Cmd.none, None )
211222

@@ -295,6 +306,29 @@ update appContext project msg model =
295306
_ ->
296307
( model, Cmd.none, None )
297308

309+
( ProjectWebhookExamplesModalMsg examplesMsg, _ ) ->
310+
case model.modal of
311+
WebhookExamplesModal m ->
312+
let
313+
( modal, cmd, out ) =
314+
ProjectWebhookExamplesModal.update appContext project.ref examplesMsg m
315+
in
316+
case out of
317+
ProjectWebhookExamplesModal.NoOutMsg ->
318+
( { model | modal = WebhookExamplesModal modal }
319+
, Cmd.map ProjectWebhookExamplesModalMsg cmd
320+
, None
321+
)
322+
323+
ProjectWebhookExamplesModal.RequestCloseModal ->
324+
( { model | modal = NoModal }
325+
, Cmd.map ProjectWebhookExamplesModalMsg cmd
326+
, None
327+
)
328+
329+
_ ->
330+
( model, Cmd.none, None )
331+
298332
_ ->
299333
( model, Cmd.none, None )
300334

@@ -541,6 +575,12 @@ viewWebhooks session model =
541575
divider =
542576
Divider.divider |> Divider.small |> Divider.view
543577

578+
examplesButton =
579+
Button.iconThenLabel ShowWebhookExamplesModal Icon.docs "Webhook example docs"
580+
|> Button.small
581+
|> Button.subdued
582+
|> Button.view
583+
544584
addButton =
545585
Button.iconThenLabel ShowAddWebhookModal Icon.plus "Add a webhook"
546586
|> Button.small
@@ -550,15 +590,15 @@ viewWebhooks session model =
550590
case model.webhooks of
551591
Success webhooks ->
552592
if List.isEmpty webhooks then
553-
[ header [ class "project-settings_card_header" ] [ h2 [] [ text "Webhooks" ], addButton ]
593+
[ header [ class "project-settings_card_header" ] [ h2 [] [ text "Webhooks" ], div [ class "webhook-buttons" ] [ examplesButton, addButton ] ]
554594
, div [ class "list_empty-state" ]
555595
[ div [ class "list_empty-state_text" ]
556596
[ Icon.view Icon.wireframeGlobe, text "You haven't set up any webhooks yet" ]
557597
]
558598
]
559599

560600
else
561-
[ header [ class "project-settings_card_header" ] [ h2 [] [ text "Webhooks" ], addButton ]
601+
[ header [ class "project-settings_card_header" ] [ h2 [] [ text "Webhooks" ], div [ class "webhook-buttons" ] [ examplesButton, addButton ] ]
562602
, div [ class "webhooks" ] (webhooks |> List.map viewWebhook |> List.intersperse divider)
563603
]
564604

@@ -756,6 +796,9 @@ view session project model =
756796
AddWebhookModal m ->
757797
Just (Html.map AddProjectWebhookModalMsg (AddProjectWebhookModal.view m))
758798

799+
WebhookExamplesModal m ->
800+
Just (Html.map ProjectWebhookExamplesModalMsg (ProjectWebhookExamplesModal.view session m))
801+
759802
_ ->
760803
Nothing
761804
in
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module UnisonShare.ProjectWebhookExample exposing (..)
2+
3+
import Json.Decode as Decode
4+
import Json.Encode as Encode
5+
import Lib.Decode.Helpers as DecodeH
6+
import List.Nonempty as NEL exposing (Nonempty)
7+
import String.Extra as StringE
8+
9+
10+
type alias ProjectWebhookExample =
11+
{ topic : String
12+
, payload : String
13+
}
14+
15+
16+
topicFromString : String -> String
17+
topicFromString raw =
18+
raw
19+
|> String.replace ":" " "
20+
|> String.replace "project" ""
21+
|> StringE.humanize
22+
23+
24+
25+
-- DECODE
26+
27+
28+
decode : Decode.Decoder ProjectWebhookExample
29+
decode =
30+
let
31+
jsonToString jsonValue =
32+
Encode.encode 2 jsonValue
33+
in
34+
Decode.map2 ProjectWebhookExample
35+
(Decode.field "topic" (Decode.map topicFromString Decode.string))
36+
(Decode.map jsonToString Decode.value)
37+
38+
39+
decodeList : Decode.Decoder (Nonempty ProjectWebhookExample)
40+
decodeList =
41+
let
42+
sortByTopic examples =
43+
NEL.sortBy .topic examples
44+
in
45+
Decode.map sortByTopic (DecodeH.nonEmptyList decode)
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
module UnisonShare.ProjectWebhookExamplesModal exposing (..)
2+
3+
import Html exposing (Html, div, p, pre, text)
4+
import Html.Attributes exposing (class, classList)
5+
import Lib.HttpApi as HttpApi exposing (HttpResult)
6+
import List.Extra as ListE
7+
import List.Nonempty as NEL exposing (Nonempty)
8+
import RemoteData exposing (RemoteData(..), WebData)
9+
import UI.Button as Button
10+
import UI.Click as Click
11+
import UI.CopyOnClick as CopyOnClick
12+
import UI.Modal as Modal
13+
import UI.Placeholder as Placeholder
14+
import UI.StatusBanner as StatusBanner
15+
import UnisonShare.Api as ShareApi
16+
import UnisonShare.AppContext exposing (AppContext)
17+
import UnisonShare.ErrorDetails as ErrorDetails
18+
import UnisonShare.Project.ProjectRef exposing (ProjectRef)
19+
import UnisonShare.ProjectWebhookExample as ProjectWebhookExample exposing (ProjectWebhookExample)
20+
import UnisonShare.Session as Session
21+
22+
23+
type alias LoadedModel =
24+
{ examples : Nonempty ProjectWebhookExample
25+
, activeTopic : String
26+
}
27+
28+
29+
type alias Model =
30+
WebData LoadedModel
31+
32+
33+
init : AppContext -> ProjectRef -> ( Model, Cmd Msg )
34+
init appContext projectRef =
35+
( Loading, fetchWebhookExamples appContext projectRef )
36+
37+
38+
39+
-- UPDATE
40+
41+
42+
type Msg
43+
= CloseModal
44+
| FetchWebhookExamplesFinished (HttpResult (Nonempty ProjectWebhookExample))
45+
| ChangeTopic String
46+
47+
48+
type OutMsg
49+
= NoOutMsg
50+
| RequestCloseModal
51+
52+
53+
update : AppContext -> ProjectRef -> Msg -> Model -> ( Model, Cmd Msg, OutMsg )
54+
update _ _ msg model =
55+
case msg of
56+
CloseModal ->
57+
( model, Cmd.none, RequestCloseModal )
58+
59+
ChangeTopic newTopic ->
60+
case model of
61+
Success loaded ->
62+
( Success { loaded | activeTopic = newTopic }, Cmd.none, NoOutMsg )
63+
64+
_ ->
65+
( model, Cmd.none, NoOutMsg )
66+
67+
FetchWebhookExamplesFinished res ->
68+
case res of
69+
Ok exs ->
70+
( Success
71+
{ examples = exs
72+
, activeTopic = exs |> NEL.map .topic |> NEL.head
73+
}
74+
, Cmd.none
75+
, NoOutMsg
76+
)
77+
78+
Err e ->
79+
( Failure e, Cmd.none, NoOutMsg )
80+
81+
82+
83+
-- EFFECTS
84+
85+
86+
fetchWebhookExamples : AppContext -> ProjectRef -> Cmd Msg
87+
fetchWebhookExamples appContext _ =
88+
ShareApi.webhookExamples
89+
|> HttpApi.toRequest ProjectWebhookExample.decodeList
90+
FetchWebhookExamplesFinished
91+
|> HttpApi.perform appContext.api
92+
93+
94+
95+
-- VIEW
96+
97+
98+
viewPayload : String -> Html Msg
99+
viewPayload json =
100+
div [ class "example-json-format" ]
101+
[ CopyOnClick.copyButton json
102+
, pre [] [ text json ]
103+
]
104+
105+
106+
viewExamples : LoadedModel -> Html Msg
107+
viewExamples { examples, activeTopic } =
108+
let
109+
matchingExample =
110+
examples
111+
|> NEL.toList
112+
|> ListE.find (\e -> e.topic == activeTopic)
113+
114+
json =
115+
case matchingExample of
116+
Just e ->
117+
e.payload
118+
119+
Nothing ->
120+
""
121+
122+
viewExampleTab e =
123+
Click.onClick (ChangeTopic e.topic)
124+
|> Click.view
125+
[ class "example-topic"
126+
, classList [ ( "active-topic", activeTopic == e.topic ) ]
127+
]
128+
[ text e.topic ]
129+
in
130+
div [ class "examples" ]
131+
[ div [ class "examples-list" ]
132+
(examples |> NEL.map viewExampleTab |> NEL.toList)
133+
, viewPayload json
134+
]
135+
136+
137+
view : Session.Session -> Model -> Html Msg
138+
view session model =
139+
let
140+
modal_ c =
141+
Modal.content c
142+
|> Modal.modal "project-webhook-examples-modal" CloseModal
143+
|> Modal.withHeader "Project webhook examples"
144+
145+
modal =
146+
case model of
147+
Success loaded ->
148+
modal_
149+
(div []
150+
[ p [] [ text "Overview of the JSON formats that are POSTed to a configured webhook URL when those events are triggered." ]
151+
, viewExamples loaded
152+
]
153+
)
154+
155+
Failure e ->
156+
modal_ (div [] [ StatusBanner.bad "Failed to load webhook examples", ErrorDetails.view session e ])
157+
158+
_ ->
159+
let
160+
shape length =
161+
Placeholder.text
162+
|> Placeholder.withLength length
163+
|> Placeholder.subdued
164+
|> Placeholder.tiny
165+
|> Placeholder.view
166+
in
167+
modal_
168+
(div
169+
[ class "examples-loading" ]
170+
[ shape Placeholder.Large
171+
, shape Placeholder.Small
172+
, shape Placeholder.Medium
173+
, shape Placeholder.Large
174+
, shape Placeholder.Small
175+
, shape Placeholder.Medium
176+
]
177+
)
178+
in
179+
modal
180+
|> Modal.withActions [ Button.button CloseModal "Close" ]
181+
|> Modal.view

src/css/unison-share.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
@import "./unison-share/publish-project-release-modal.css";
1111
@import "./unison-share/add-project-collaborator-modal.css";
1212
@import "./unison-share/add-project-webhook-modal.css";
13+
@import "./unison-share/project-webhook-examples-modal.css";
1314
@import "./unison-share/add-org-member-modal.css";
1415
@import "./unison-share/project-contribution-form-modal.css";
1516
@import "./unison-share/project-ticket-form-modal.css";

src/css/unison-share/page/project-settings-page.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
flex-direction: row;
1515
justify-content: space-between;
1616
width: 100%;
17+
& .webhook-buttons {
18+
display: flex;
19+
flex-direction: row;
20+
gap: 0.5rem;
21+
}
1722
}
1823

1924
.project-settings-page .settings-content .disabled-overlay {

0 commit comments

Comments
 (0)