git.iko.soy/typeable/octopod
on master help sign in
Deployments list (#25)
commitc707be2421984726a9307fd90f44a42980c7c326 authorNikita <anisimoff.nikita@gmail.com> dateTue 16 Jun 2020, 19:01 (6 years ago) parent22c6952
12 files changed+554−8
.gitmodules+3 Caddyfile+41 Makefile+4−1 default.nix+1 dm-common/src/Common/Types.hs+5−5 dm-css+1 dm-frontend/dm-frontend.cabal+41−1 dm-frontend/src/Frontend/API.hs+114 dm-frontend/src/Main.hs+21−1 dm-frontend/src/Page/Deployments.hs+270 dm-frontend/src/Page/Utils.hs+41 nix/sources.json+12
.gitmodules+3
@@ -2,3 +2,6 @@
22 path = b2b-helm
33 url = https://github.com/Aviora/b2b-helm.git
44 branch = master
5+[submodule "dm-css"]
6+ path = dm-css
7+ url = git@github.com:Aviora/dm-css.git
Caddyfilenew file+41
@@ -0,0 +1,41 @@
1+localhost:8000
2+
3+mime .css text/css
4+
5+rewrite /static/styles {
6+ r (.*)
7+ to /dm-css/production/styles/{1}
8+}
9+
10+mime .js application/javascript
11+
12+rewrite /static/vendors {
13+ r (.*)
14+ to /dm-css/production/vendors/{1}
15+}
16+
17+rewrite /static/scripts {
18+ r (.*)
19+ to /dm-css/production/scripts/{1}
20+}
21+
22+rewrite /static/images {
23+ r (.*)
24+ to /dm-css/production/images/{1}
25+}
26+
27+proxy /api localhost:3002 {
28+ transparent
29+}
30+
31+rewrite /ghcjs /frontend-result/bin/frontend.jsexe/index.html
32+
33+proxy / localhost:3003 {
34+ transparent
35+ websocket
36+ except /dm-css/
37+ except /frontend-result/
38+}
39+
40+
41+log / stdout
Makefile+4−1
@@ -3,6 +3,9 @@
33build:
44 nix-build . -A ghc.dm-backend
55
6+build-frontend:
7+ nix-build . -A ghcjs.dm-frontend -o frontend-result
8+
69repl:
710 nix-shell . -A shells.ghc --run "cabal repl lib:dm-backend"
811
@@ -19,7 +22,7 @@ ghcid:
1922 nix-shell . -A shells.ghc --run 'ghcid -c "cabal new-repl dm-backend"'
2023
2124ghcid-frontend:
22− nix-shell . -A shells.ghc --run 'ghcid -c "cabal new-repl dm-frontend"'
25+ nix-shell . -A shells.ghc --run 'ghcid -c "cabal new-repl dm-frontend -fdevelopment" --test 'Main.main''
2326
2427push-dms: build-docker
2528 ./release.sh
default.nix+1
@@ -17,6 +17,7 @@
1717
1818 overrides = hself: hsuper: {
1919 deriving-aeson = hsuper.callCabal2nix "deriving-aeson" sources.deriving-aeson { };
20+ servant-reflex = hsuper.callCabal2nix "servant-reflex" sources.servant-reflex { };
2021 servant = pkgs.haskell.lib.overrideCabal hsuper.servant (old: {
2122 postInstall = "";
2223 });
dm-common/src/Common/Types.hs+5−5
@@ -15,11 +15,11 @@ type EnvPairs = [EnvPair]
1515
1616newtype DeploymentName = DeploymentName { unDeploymentName :: Text }
1717 deriving
18− ( Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData)
18+ ( Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq)
1919
2020newtype DeploymentTag = DeploymentTag { unDeploymentTag :: Text }
2121 deriving
22− ( Show, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData)
22+ ( Show, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq)
2323
2424newtype Action = Action { unAction :: Text }
2525 deriving (Show, FromJSON, ToJSON, IsString)
@@ -32,7 +32,7 @@ data Deployment = Deployment
3232 , tag :: DeploymentTag
3333 , envs :: EnvPairs
3434 }
35− deriving (Generic, Show)
35+ deriving (Generic, Show, Eq)
3636 deriving (FromJSON, ToJSON) via Snake Deployment
3737
3838data DeploymentLog = DeploymentLog
@@ -59,7 +59,7 @@ data DeploymentFullInfo = DeploymentFullInfo
5959 , updatedAt :: Int
6060 , urls :: [(Text, Text)]
6161 }
62− deriving (Generic, Show)
62+ deriving (Generic, Show, Eq)
6363 deriving (FromJSON, ToJSON) via Snake DeploymentFullInfo
6464
6565data DeploymentUpdate = DeploymentUpdate
@@ -77,4 +77,4 @@ data Status
7777
7878newtype DeploymentStatus = DeploymentStatus { status :: Status }
7979 deriving (Generic, Show)
80− deriving (FromJSON, ToJSON) via Snake DeploymentStatus
80+ deriving (FromJSON, ToJSON) via Snake DeploymentStatus
dm-cssnew file+1

Submodule changed.

dm-frontend/dm-frontend.cabal+41−1
@@ -15,12 +15,52 @@ build-type: Simple
1515extra-source-files: CHANGELOG.md
1616cabal-version: >=1.10
1717
18+
19+flag development
20+ default: False
21+ description: mock host
22+
1823executable frontend
24+ if flag(development)
25+ cpp-options: -DDEVELOPMENT
1926 main-is: Main.hs
20− -- other-modules:
27+ other-modules: Frontend.API
28+ , Page.Deployments
29+ , Page.Utils
30+ ghc-options: -Wall
31+ -Werror
32+ -Wno-missing-home-modules
33+ -Wno-error=missing-home-modules
34+ default-extensions: BlockArguments
35+ , ConstraintKinds
36+ , DataKinds
37+ , DeriveFunctor
38+ , DeriveGeneric
39+ , DerivingVia
40+ , DuplicateRecordFields
41+ , FlexibleContexts
42+ , GeneralizedNewtypeDeriving
43+ , LambdaCase
44+ , OverloadedStrings
45+ , ScopedTypeVariables
46+ , StandaloneDeriving
47+ , TypeApplications
48+ , TypeOperators
49+ , MonoLocalBinds
50+ , RecordWildCards
51+ , RecursiveDo
2152 -- other-extensions:
2253 build-depends: base >=4.12 && <4.13
2354 , dm-common
55+ , generic-lens
56+ , ghcjs-dom
57+ , jsaddle
58+ , lens
2459 , reflex-dom
60+ , reflex-dom-helpers
61+ , servant
62+ , servant-reflex
63+ , text
64+ , time
2565 hs-source-dirs: src
2666 default-language: Haskell2010
dm-frontend/src/Frontend/API.hsnew file+114
@@ -0,0 +1,114 @@
1+{-# LANGUAGE PartialTypeSignatures #-}
2+{-# LANGUAGE NoMonomorphismRestriction #-}
3+{-# LANGUAGE CPP #-}
4+
5+module Frontend.API where
6+
7+#ifdef DEVELOPMENT
8+import Control.Lens
9+#endif
10+import Data.Proxy
11+import Data.Text as T
12+import Reflex.Dom as R
13+import Servant.API as S
14+import Servant.Reflex as SR
15+
16+import Common.API
17+import Common.Types
18+
19+
20+apiClients
21+ :: forall t m. MonadWidget t m
22+ => SR.Client t m API ()
23+apiClients = clientWithOpts
24+ (Proxy @API)
25+ (Proxy @m)
26+ (Proxy @())
27+#ifdef DEVELOPMENT
28+ (constDyn $ SR.BaseFullUrl SR.Https "dm-genfly-app.stage.thebestagent.pro" 443 "/")
29+#else
30+ (constDyn $ SR.BasePath "/")
31+#endif
32+ tweakRequest
33+ where
34+ tweakRequest = ClientOptions $ \req ->
35+#ifdef DEVELOPMENT
36+ pure $ req
37+ & xhrRequest_config
38+ . xhrRequestConfig_headers
39+ . at "Authorization" ?~ "Basic dWk6cVoxZGRKMHZpWkRMbXlKaG5JNVh1QnZMQnZ3MkMwUVBqOTRaZ0VwOQ=="
40+#else
41+ pure req
42+#endif
43+
44+listEndpoint
45+ :: MonadWidget t m
46+ => Event t ()
47+ -> m (Event t (ReqResult () [DeploymentFullInfo]))
48+createEndpoint
49+ :: MonadWidget t m
50+ => Dynamic t (Either Text Deployment)
51+ -> Event t ()
52+ -> m (Event t (ReqResult () NoContent))
53+getEndpoint
54+ :: MonadWidget t m
55+ => Dynamic t (Either Text DeploymentName)
56+ -> Event t ()
57+ -> m (Event t (ReqResult () Deployment))
58+editEndpoint
59+ :: MonadWidget t m
60+ => Dynamic t (Either Text DeploymentName)
61+ -> Dynamic t (Either Text [EnvPair])
62+ -> Event t ()
63+ -> m (Event t (ReqResult () NoContent))
64+deleteEndpoint
65+ :: MonadWidget t m
66+ => Dynamic t (Either Text DeploymentName)
67+ -> Event t ()
68+ -> m (Event t (ReqResult () NoContent))
69+updateEndpoint
70+ :: MonadWidget t m
71+ => Dynamic t (Either Text DeploymentName)
72+ -> Dynamic t (Either Text DeploymentUpdate)
73+ -> Event t ()
74+ -> m (Event t (ReqResult () NoContent))
75+infoEndpoint
76+ :: MonadWidget t m
77+ => Dynamic t (Either Text DeploymentName)
78+ -> Event t ()
79+ -> m (Event t (ReqResult () [DeploymentInfo]))
80+statusEndpoint
81+ :: MonadWidget t m
82+ => Dynamic t (Either Text DeploymentName)
83+ -> Event t ()
84+ -> m (Event t (ReqResult () DeploymentStatus))
85+cleanupEndpoint
86+ :: MonadWidget t m
87+ => Dynamic t (Either Text DeploymentName)
88+ -> Event t ()
89+ -> m (Event t (ReqResult () NoContent))
90+restoreEndpoint
91+ :: MonadWidget t m
92+ => Dynamic t (Either Text DeploymentName)
93+ -> Event t ()
94+ -> m (Event t (ReqResult () NoContent))
95+pingEndpoint
96+ :: MonadWidget t m
97+ => Event t ()
98+ -> m (Event t (ReqResult () NoContent))
99+cleanArchiveEndpoint
100+ :: MonadWidget t m
101+ => Event t ()
102+ -> m (Event t (ReqResult () NoContent))
103+(listEndpoint
104+ :<|> createEndpoint
105+ :<|> getEndpoint
106+ :<|> editEndpoint
107+ :<|> deleteEndpoint
108+ :<|> updateEndpoint
109+ :<|> infoEndpoint
110+ :<|> statusEndpoint
111+ :<|> cleanupEndpoint
112+ :<|> restoreEndpoint)
113+ :<|> pingEndpoint
114+ :<|> cleanArchiveEndpoint = apiClients
dm-frontend/src/Main.hs+21−1
@@ -4,5 +4,25 @@ module Main where
44
55import Reflex.Dom
66
7+import Page.Deployments
8+
9+
710main :: IO ()
8−main = mainWidget $ text "Hello, Reflex!"
11+main = mainWidgetWithHead headWidget $
12+ deploymentsPage
13+
14+headWidget :: DomBuilder t m => m ()
15+headWidget = do
16+ elAttr "meta" ( "charset" =: "urf8 ") blank
17+ elAttr "meta"
18+ ( "http-equiv" =: "x-ua-compatible"
19+ <> "content" =: "ie=edge" ) blank
20+ elAttr "name"
21+ ( "name" =: "viewport"
22+ <> "content" =: "width=device-width, initial-scale=1" ) blank
23+ elAttr "link"
24+ ( "href" =: "https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Roboto:wght@400;500;700;900&display=swap"
25+ <> "rel" =: "stylesheet") blank
26+ elAttr "link"
27+ ( "href" =: "/static/styles/style.css"
28+ <> "rel" =: "stylesheet") blank
dm-frontend/src/Page/Deployments.hsnew file+270
@@ -0,0 +1,270 @@
1+module Page.Deployments
2+ ( deploymentsPage ) where
3+
4+import Control.Lens
5+import Control.Monad
6+import Data.Coerce
7+import Data.Generics.Product
8+import Data.List as L (null, filter)
9+import Data.Text as T (pack)
10+import Data.Time
11+import Data.Time.Clock.POSIX
12+import Reflex.Dom
13+import Servant.Reflex
14+
15+import Common.Types
16+import Frontend.API
17+import Page.Utils
18+
19+deploymentsPage :: MonadWidget t m => m ()
20+deploymentsPage = do
21+ headWidget
22+ bodyLoadWidget
23+
24+headWidget :: MonadWidget t m => m ()
25+headWidget =
26+ elClass "header" "header" $
27+ divClass "header__wrap container" $ do
28+ elClass "b" "header__logo" $
29+ text "Deployment Manager"
30+ elClass "div" "header__project" $
31+ text "<Project name>"
32+
33+bodyLoadWidget :: MonadWidget t m => m ()
34+bodyLoadWidget = do
35+ pb <- getPostBuild
36+ respEv <- listEndpoint pb
37+ let
38+ okEv = fmapMaybe reqSuccess respEv
39+ errEv = fmapMaybe reqSuccess respEv
40+ widgetHold_ blank $ leftmost
41+ [ deploymentsWidget <$> okEv
42+ , errDeploymentsWidget <$ errEv ]
43+ blank
44+
45+errDeploymentsWidget :: MonadWidget t m => m ()
46+errDeploymentsWidget =
47+ divClass "no-page" $
48+ divClass "no-page__inner" $
49+ divClass "null null--data" $
50+ divClass "null__content" $
51+ divClass "null__heading" $
52+ text "Cannot retrieve the data"
53+
54+deploymentsWidgetWrapper :: MonadWidget t m => m a -> m a
55+deploymentsWidgetWrapper m =
56+ divClass "page" $
57+ divClass "page__wrap container" m
58+
59+deploymentsWidget :: MonadWidget t m => [DeploymentFullInfo] -> m ()
60+deploymentsWidget ds = deploymentsWidgetWrapper $ do
61+ dsDyn <- holdDyn ds never
62+ deploymentsHeadWidget
63+ deploymentsListWidget dsDyn
64+
65+deploymentsHeadWidget :: MonadWidget t m => m ()
66+deploymentsHeadWidget =
67+ divClass "page__head" $ do
68+ elClass "h1" "page__heading title" $ text "All stagings"
69+ divClass "page__note" $ text "Updated <n> mins ago"
70+ divClass "page__search input input--search input--has-clear-type" $ do
71+ _search <- inputElement $ def
72+ & initialAttributes .~
73+ ( "type" =: "text"
74+ <> "class" =: "input__widget"
75+ <> "placeholder" =: "Search for stagings" )
76+ (_, _delete) <- elClass' "button" "input__clear-type spot spot--cancel" $
77+ text "Delete"
78+ blank
79+ elClass "a" "page__add-staging button button--add popup-handler" $
80+ text "New staging"
81+
82+deploymentsListWidget
83+ :: MonadWidget t m
84+ => Dynamic t [DeploymentFullInfo]
85+ -> m ()
86+deploymentsListWidget dsDyn = do
87+ let
88+ emptyDyn' = L.null <$> dsDyn
89+ emptyDyn <- holdUniqDyn emptyDyn'
90+ divClass "page__body" $
91+ dyn_ $ ffor emptyDyn $ \case
92+ True -> noDeploymentsWidget
93+ False -> deploymentsListWidget' dsDyn
94+
95+noDeploymentsWidget :: MonadWidget t m => m ()
96+noDeploymentsWidget =
97+ divClass "no-data" $
98+ divClass "null null--search" $ do
99+ elClass "b" "null__heading" $ text "No results found"
100+ divClass "null__message" $ do
101+ text "It seems we can’t find any results"
102+ el "br" blank
103+ text "based on your search"
104+
105+
106+deploymentsListWidget'
107+ :: MonadWidget t m
108+ => Dynamic t [DeploymentFullInfo]
109+ -> m ()
110+deploymentsListWidget' dsDyn = divClass "data" $ mdo
111+ let
112+ isArchived = view (field @"archived")
113+ activeDsDyn = L.filter (not . isArchived) <$> dsDyn
114+ archivedDsDyn = L.filter isArchived <$> dsDyn
115+ activeDeploymentsWidget activeDsDyn
116+ showDyn <- toggle False $ domEvent Click archivedBtnEl
117+ let
118+ btnClassDyn = ffor showDyn $ \case
119+ True -> "data__show-archive expander expander--stand-alone expander--open"
120+ False -> "data__show-archive expander expander--stand-alone"
121+ btnAttrsDyn = ffor btnClassDyn $ \btnClass ->
122+ ( "class" =: btnClass
123+ <> "type" =: "button" )
124+ (archivedBtnEl, _) <- elDynAttr' "button" btnAttrsDyn $
125+ text "Show Archived stagings"
126+ archivedDeploymentsWidget showDyn archivedDsDyn
127+
128+
129+
130+activeDeploymentsWidget
131+ :: MonadWidget t m
132+ => Dynamic t [DeploymentFullInfo]
133+ -> m ()
134+activeDeploymentsWidget dsDyn =
135+ divClass "data__primary" $
136+ elClass "table" "stagings-table panel" $ do
137+ el "thead" $
138+ el "tr" $ do
139+ el "th" $ text "Name"
140+ el "th" $ text "Links"
141+ el "th" $ text "Tag"
142+ el "th" $ text "Overrides"
143+ el "th" $ do
144+ _sortBtn <- elAttr "button"
145+ ( "class" =: "sort sort--active sort--asc"
146+ <> "type" =: "button" ) $ text "Created"
147+ blank
148+ el "th" $ text "Changed"
149+ el "th" $
150+ elClass "span" "visuallyhidden" $ text "Menu"
151+ el "tbody" $
152+ void $ simpleList dsDyn activeDeploymentWidget
153+
154+activeDeploymentWidget
155+ :: MonadWidget t m
156+ => Dynamic t DeploymentFullInfo
157+ -> m ()
158+activeDeploymentWidget dDyn' = do
159+ dDyn <- holdUniqDyn dDyn'
160+ dyn_ $ ffor dDyn $ \DeploymentFullInfo{..} -> do
161+ el "tr" $ do
162+ el "td" $ do
163+ text $ coerce $ deployment ^. field @"name"
164+ divClass "status status--success" $ text "<Status>"
165+ el "td" $ do
166+ divClass "listing" $
167+ forM_ urls $ \(_, url) ->
168+ elAttr "a"
169+ ( "class" =: "listing__item external bar"
170+ <> "href" =: ("https://" <> url)
171+ <> "target" =: "_blank") $ text url
172+ el "td" $
173+ text $ coerce $ deployment ^. field @"tag"
174+ el "td" $ do
175+ divClass "listing" $
176+ forM_ (deployment ^. field @"envs") $ \(var, val) ->
177+ divClass "listing__item bar" $ do
178+ el "b" $ text $ var <> ": "
179+ text val
180+ el "td" $
181+ text $ pack $ formatTime defaultTimeLocale (iso8601DateFormat Nothing)
182+ $ intToUTCTime createdAt
183+ el "td" $
184+ text $ pack $ formatTime defaultTimeLocale (iso8601DateFormat Nothing)
185+ $ intToUTCTime updatedAt
186+ el "td" $ do
187+ let
188+ elId = "deployment_row_" <> (coerce $ deployment ^. field @"name")
189+ btn = elAttr "button"
190+ ( "class" =: "drop__handler"
191+ <> "type" =: "button"
192+ <> "id" =: elId) $ text "Actions"
193+ body = do
194+ elAttr "button"
195+ ( "class" =: "action action--edit"
196+ <> "type" =: "button") $ text "Edit"
197+ elAttr "button"
198+ ( "class" =: "action action--delete"
199+ <> "type" =: "button") $ text "Move to archive"
200+ dropdownWidget elId btn body
201+
202+intToUTCTime :: Int -> UTCTime
203+intToUTCTime = posixSecondsToUTCTime . realToFrac
204+
205+archivedDeploymentsWidget
206+ :: MonadWidget t m
207+ => Dynamic t Bool
208+ -> Dynamic t [DeploymentFullInfo]
209+ -> m ()
210+archivedDeploymentsWidget showDyn dsDyn = do
211+ let
212+ classDyn = ffor showDyn $ \case
213+ True -> "data__archive data__archive--open"
214+ False -> "data__archive"
215+ elDynClass "div" classDyn $
216+ elClass "table" "stagings-table panel" $ do
217+ el "thead" $
218+ el "tr" $ do
219+ el "th" $ text "Name"
220+ el "th" $ text "Links"
221+ el "th" $ text "Tag"
222+ el "th" $ text "Overrides"
223+ el "th" $ do
224+ _sortBtn <- elAttr "button"
225+ ( "class" =: "sort sort--active sort--asc"
226+ <> "type" =: "button" ) $ text "Created"
227+ blank
228+ el "th" $ text "Changed"
229+ el "th" $
230+ elClass "span" "visuallyhidden" $ text "Menu"
231+ el "tbody" $
232+ void $ simpleList dsDyn archivedDeploymentWidget
233+
234+archivedDeploymentWidget
235+ :: MonadWidget t m
236+ => Dynamic t DeploymentFullInfo
237+ -> m ()
238+archivedDeploymentWidget dDyn' = do
239+ dDyn <- holdUniqDyn dDyn'
240+ dyn_ $ ffor dDyn $ \DeploymentFullInfo{..} -> do
241+ el "tr" $ do
242+ el "td" $ do
243+ text $ coerce $ deployment ^. field @"name"
244+ divClass "status status--archived" $ text "Archived"
245+ el "td" $ text "..."
246+ el "td" $
247+ text $ coerce $ deployment ^. field @"tag"
248+ el "td" $ do
249+ divClass "listing" $
250+ forM_ (deployment ^. field @"envs") $ \(var, val) ->
251+ divClass "listing__item bar" $ do
252+ el "b" $ text $ var <> ": "
253+ text val
254+ el "td" $
255+ text $ pack $ formatTime defaultTimeLocale (iso8601DateFormat Nothing)
256+ $ intToUTCTime createdAt
257+ el "td" $
258+ text $ pack $ formatTime defaultTimeLocale (iso8601DateFormat Nothing)
259+ $ intToUTCTime updatedAt
260+ el "td" $ do
261+ let
262+ elId = "deployment_row_" <> (coerce $ deployment ^. field @"name")
263+ btn = elAttr "button"
264+ ( "class" =: "drop__handler"
265+ <> "type" =: "button"
266+ <> "id" =: elId) $ text "Actions"
267+ body = elAttr "button"
268+ ( "class" =: "action action--delete"
269+ <> "type" =: "button") $ text "Restore from archive"
270+ dropdownWidget elId btn body
dm-frontend/src/Page/Utils.hsnew file+41
@@ -0,0 +1,41 @@
1+module Page.Utils where
2+
3+import Data.Text
4+import GHCJS.DOM
5+import GHCJS.DOM.Element as DOM
6+import GHCJS.DOM.EventM (on, target)
7+import GHCJS.DOM.GlobalEventHandlers as Events (click)
8+import GHCJS.DOM.Node as DOM
9+import Reflex.Dom
10+
11+clickInside :: MonadWidget t m => DOM.Element -> m (Event t Bool)
12+clickInside elm = do
13+ doc <- currentDocumentUnchecked
14+ wrapDomEvent doc (`on` Events.click) $ do
15+ t :: Maybe DOM.Element <- target
16+ DOM.contains elm t
17+
18+documentClickWidget :: MonadWidget t m => m (Event t (Maybe Text))
19+documentClickWidget = do
20+ doc <- currentDocumentUnchecked
21+ wrapDomEvent doc (`on` Events.click) $ do
22+ t :: Maybe DOM.Element <- target
23+ sequence $ getId <$> t
24+
25+dropdownWidget
26+ :: MonadWidget t m
27+ => Text
28+ -> m ()
29+ -> m ()
30+ -> m ()
31+dropdownWidget elId btn body = mdo
32+ docClickEv <- documentClickWidget
33+ let
34+ openEv = ffilter (Just elId ==) docClickEv
35+ closeEv = docClickEv
36+ wrapperClassDyn <- holdDyn "drop drop--actions" $ leftmost
37+ [ "drop drop--actions drop--expanded" <$ openEv
38+ , "drop drop--actions" <$ closeEv ]
39+ elDynClass "div" wrapperClassDyn $ do
40+ btn
41+ divClass "drop__dropdown panel panel--flying" body
nix/sources.json+12
@@ -34,5 +34,17 @@
3434 "type": "tarball",
3535 "url": "https://github.com/NixOS/nixpkgs/archive/1484e1acfeee4c5ec039df93282e78ffc0be2236.tar.gz",
3636 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
37+ },
38+ "servant-reflex": {
39+ "branch": "master",
40+ "description": "Generate reflex client functions for querying a servant API",
41+ "homepage": null,
42+ "owner": "imalsogreg",
43+ "repo": "servant-reflex",
44+ "rev": "37a3e8f2566627d910df140982bd49bf4dba171e",
45+ "sha256": "1yqxf6f81n4y4527rl69hfqymrnmj7lskgns2qsh59ibisp3y9rg",
46+ "type": "tarball",
47+ "url": "https://github.com/imalsogreg/servant-reflex/archive/37a3e8f2566627d910df140982bd49bf4dba171e.tar.gz",
48+ "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
3749 }
3850}