git.iko.soy/typeable/octopod
on master help sign in
Improved frontend performance (#156)

* Actually use -O2 * Inlined rendering * Switched to an external fuzzy finder * Added insane optimization flags to everything * Made searching async * pass system to dev * Use patched ghcjs * Slightly toned down the insanity * bumped all of nix stuff * tuned runtime * Made things even less intense since it doesn't make a difference * bumped everything * Made overrides faster and prettier * Removed hack for key search popup * Removed warnings

commitc840c6e3ae1aeb4f641017211365836f7a93b6d9 authoriko <ilyakooo0@gmail.com> dateWed 8 Dec 2021, 14:00 (5 years ago) parentbe1f20c
14 files changed+330−147
Makefile+3 default.nix+9−2 dev/default.nix+15−5 nix/sources.json+8−8 octopod-common/octopod-common.cabal+3 octopod-common/src/Common/Orphans.hs+10 octopod-common/src/Common/Types.hs+12−3 octopod-frontend/octopod-frontend.cabal+8−1 octopod-frontend/src/Data/Text/Search.hs+40−61 octopod-frontend/src/Frontend/UIKit.hs+72−31 octopod-frontend/src/Frontend/Utils.hs+94−30 octopod-frontend/src/Page/Deployments.hs+6−4 octopod-frontend/src/Reflex/Dom/AsyncEvent.hs+30 octopod-frontend/src/Reflex/Dom/Renderable.hs+20−2
Makefile+3
@@ -36,5 +36,8 @@ run-backend-dev:
3636run-frontend-dev:
3737 `nix-build dev -A frontend -j auto`
3838
39+run-frontend-prod:
40+ `nix-build dev -A frontend --arg prod true -j auto`
41+
3942run-caddy-for-ghcid:
4043 `nix-build dev -A caddyForGhcid -j auto`
default.nix+9−2
@@ -8,8 +8,15 @@
88}:
99let
1010 octopod-css = import ./octopod-css { inherit pkgsSrc; };
11+ insaneOptimizationFlags = [
12+ "-O2"
13+ "-fexpose-all-unfoldings"
14+ "-fspecialise-aggressively"
15+ ];
1116 addLocalOptions = x:
12− if prod then x // { ghcOptions = [ "-Werror" ]; }
17+ if prod then x // {
18+ ghcOptions = [ "-Werror" ] ++ insaneOptimizationFlags;
19+ }
1320 else x // { ghcOptions = [ "-O0" ]; };
1421
1522 hsPkgs = pkgs.haskell-nix.cabalProject {
@@ -28,7 +35,7 @@ let
2835
2936 modules = [
3037 {
31− ghcOptions = [ "-O2" ];
38+ ghcOptions = insaneOptimizationFlags;
3239 dontStrip = false;
3340 dontPatchELF = false;
3441 enableDeadCodeElimination = true;
dev/default.nix+15−5
@@ -1,10 +1,12 @@
11{ pkgs ? hsPkgs.pkgs
22, sources ? import ../nix/sources.nix
33, nix-filter ? import sources.nix-filter
4−, hsPkgs ? import ./.. { }
4+, hsPkgs ? import ./.. { inherit prod system; }
55, migrations ? ../migrations
66, octopod-css ? import ../octopod-css { inherit pkgsSrc; }
77, pkgsSrc ? hsPkgs.pkgsSrc
8+, prod ? false
9+, system ? builtins.currentSystem
810}:
911let frontendConfig = pkgs.writeTextDir "config.json" ''
1012 {
@@ -23,6 +25,8 @@ in
2325 caddyConfig = pkgs.writeText "caddy-config" ''
2426 http://localhost:8000
2527
28+ header Cache-Control no-cache
29+
2630 reverse_proxy /api/* localhost:3002
2731
2832 file_server /config.json {
@@ -52,6 +56,8 @@ in
5256 caddyConfig = pkgs.writeText "caddy-config" ''
5357 http://localhost:8000
5458
59+ header Cache-Control no-cache
60+
5561 reverse_proxy /api/* localhost:3002
5662
5763 @notStatic {
@@ -109,8 +115,10 @@ in
109115
110116 sleep 4
111117
112− echo "key,value"
113− echo "key2,value2"
118+ for i in {1..50}
119+ do
120+ echo "key$i,value"
121+ done
114122
115123 exit 0
116124 '';
@@ -120,8 +128,10 @@ in
120128
121129 sleep 4
122130
123− echo "key"
124− echo "key2"
131+ for i in {1..50}
132+ do
133+ echo "key$i"
134+ done
125135
126136 exit 0
127137 '';
nix/sources.json+8−8
@@ -1,14 +1,14 @@
11{
22 "haskellNix": {
3− "branch": "master",
3+ "branch": "iko/ghcjs-patch",
44 "description": "Alternative Haskell Infrastructure for Nixpkgs",
55 "homepage": "https://input-output-hk.github.io/haskell.nix",
6− "owner": "input-output-hk",
6+ "owner": "ilyakooo0",
77 "repo": "haskell.nix",
8− "rev": "6d42cdee0f5e916119120232e72e4ce97f7156e9",
9− "sha256": "1w8k3723jx3bs98wswg3wxqdgwa3r4spar75gi5xas4kqwgp9c41",
8+ "rev": "8cdc0f62f31be4f55bc7db88bdc0cec44bfd8ab9",
9+ "sha256": "0l0xg0s2vn0lpc2m1gavrlzk691sgifjf6l6k5sfsnhw2xq77g48",
1010 "type": "tarball",
11− "url": "https://github.com/input-output-hk/haskell.nix/archive/6d42cdee0f5e916119120232e72e4ce97f7156e9.tar.gz",
11+ "url": "https://github.com/ilyakooo0/haskell.nix/archive/8cdc0f62f31be4f55bc7db88bdc0cec44bfd8ab9.tar.gz",
1212 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
1313 },
1414 "niv": {
@@ -41,10 +41,10 @@
4141 "homepage": "",
4242 "owner": "NixOS",
4343 "repo": "nixpkgs",
44− "rev": "a7263f1291587a1bd271763cc864d3ef8fd227c1",
45− "sha256": "0jl35s5d1xjhifd5iy19wcf0rrhnhbwhf6z2g5lwak2v95p2lrb1",
44+ "rev": "bb9bd465b625bfc971908c5d3d84ce517e1c0691",
45+ "sha256": "1q558hjfxhbcj6n6kbahswvii95kz6gs5bbzh7ky86cmc2b0l38x",
4646 "type": "tarball",
47− "url": "https://github.com/NixOS/nixpkgs/archive/a7263f1291587a1bd271763cc864d3ef8fd227c1.tar.gz",
47+ "url": "https://github.com/NixOS/nixpkgs/archive/bb9bd465b625bfc971908c5d3d84ce517e1c0691.tar.gz",
4848 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
4949 }
5050}
octopod-common/octopod-common.cabal+3
@@ -20,6 +20,7 @@ library
2020 , Common.Validation
2121 , Data.Map.Ordered.Strict.Extra
2222 , Control.Searchable
23+ , Common.Orphans
2324 -- other-modules:
2425 -- other-extensions:
2526 default-extensions: BlockArguments
@@ -54,6 +55,7 @@ library
5455 , ApplicativeDo
5556 , UndecidableInstances
5657 , FunctionalDependencies
58+ , QuantifiedConstraints
5759 build-depends: base
5860 , aeson
5961 , cassava
@@ -66,6 +68,7 @@ library
6668 , containers
6769 , time
6870 , ordered-containers
71+ , deepseq
6972 hs-source-dirs: src
7073 default-language: Haskell2010
7174 ghc-options:
octopod-common/src/Common/Orphans.hsnew file+10
@@ -0,0 +1,10 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module Common.Orphans () where
4+
5+import Control.DeepSeq
6+import Data.Map.Ordered
7+
8+-- better than nothing
9+instance (NFData a, NFData b) => NFData (OMap a b) where
10+ rnf = rnf . toMap
octopod-common/src/Common/Types.hs+12−3
@@ -7,6 +7,8 @@
77-- This module contains common types between the backend and the frontend.
88module Common.Types where
99
10+import Common.Orphans ()
11+import Control.DeepSeq (NFData)
1012import Control.Lens
1113import Control.Searchable
1214import Data.Aeson hiding (Result)
@@ -46,6 +48,7 @@ instance Searchable needle t => Searchable needle (OverrideValue' t) where
4648data OverrideValue' t = ValueAdded t | ValueDeleted
4749 deriving (ToJSON, FromJSON) via Snake (OverrideValue' t)
4850 deriving stock (Eq, Ord, Show, Generic)
51+ deriving anyclass (NFData)
4952
5053type OverrideValue = OverrideValue' Text
5154
@@ -108,7 +111,7 @@ instance Searchable t x => Searchable t (Overrides' x l) where
108111 {-# INLINE searchWith #-}
109112
110113newtype Overrides' t (l :: OverrideLevel) = Overrides {unOverrides :: OMap t (OverrideValue' t)}
111− deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
114+ deriving newtype (Eq, Ord, Show, ToJSON, FromJSON, NFData)
112115
113116type Overrides = Overrides' Text
114117
@@ -153,7 +156,7 @@ instance Searchable needle t => Searchable needle (DeploymentName' t) where
153156
154157newtype DeploymentName' t = DeploymentName {unDeploymentName :: t}
155158 deriving newtype
156− (Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq, Ord)
159+ (Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq, Ord, NFData)
157160
158161type DeploymentName = DeploymentName' Text
159162
@@ -210,6 +213,7 @@ data DeploymentStatus
210213 | CleanupFailed
211214 deriving stock (Generic, Read, Show, Eq)
212215 deriving (FromJSON, ToJSON) via Snake DeploymentStatus
216+ deriving anyclass (NFData)
213217
214218data FailureType
215219 = GenericFailure
@@ -217,6 +221,7 @@ data FailureType
217221 | PartialAvailability
218222 deriving stock (Generic, Read, Show, Eq)
219223 deriving (FromJSON, ToJSON) via Snake FailureType
224+ deriving anyclass (NFData)
220225
221226data PreciseDeploymentStatus
222227 = -- | The deployment is currently being processed by the server
@@ -224,6 +229,7 @@ data PreciseDeploymentStatus
224229 | DeploymentNotPending {recordedStatus :: DeploymentStatus}
225230 deriving stock (Generic, Read, Show, Eq)
226231 deriving (FromJSON, ToJSON) via Snake PreciseDeploymentStatus
232+ deriving anyclass (NFData)
227233
228234archivedStatuses :: [DeploymentStatus]
229235archivedStatuses = [ArchivePending, Archived]
@@ -238,6 +244,7 @@ data Deployment' t = Deployment
238244 }
239245 deriving stock (Generic, Show, Eq)
240246 deriving (FromJSON, ToJSON) via Snake (Deployment' t)
247+ deriving anyclass (NFData)
241248
242249instance (Searchable needle t) => Searchable needle (Deployment' t) where
243250 type
@@ -271,7 +278,7 @@ data DeploymentLog = DeploymentLog
271278 deriving (ToJSON, FromJSON) via Snake DeploymentLog
272279
273280newtype DeploymentMetadata = DeploymentMetadata {unDeploymentMetadata :: [DeploymentMetadatum]}
274− deriving newtype (Eq, Show, Ord, FromJSON, ToJSON)
281+ deriving newtype (Eq, Show, Ord, FromJSON, ToJSON, NFData)
275282
276283data DeploymentMetadatum = DeploymentMetadatum
277284 { -- | The name of the link
@@ -282,6 +289,7 @@ data DeploymentMetadatum = DeploymentMetadatum
282289 deriving stock (Generic, Show, Eq, Ord)
283290 deriving (FromJSON, ToJSON) via Snake DeploymentMetadatum
284291 deriving anyclass (FromRecord)
292+ deriving anyclass (NFData)
285293
286294data DeploymentInfo = DeploymentInfo
287295 { deployment :: Deployment
@@ -310,6 +318,7 @@ data DeploymentFullInfo' t = DeploymentFullInfo
310318 }
311319 deriving stock (Generic, Show, Eq)
312320 deriving (FromJSON, ToJSON) via Snake (DeploymentFullInfo' t)
321+ deriving anyclass (NFData)
313322
314323type DeploymentFullInfo = DeploymentFullInfo' Text
315324
octopod-frontend/octopod-frontend.cabal+8−1
@@ -23,6 +23,8 @@ flag development
2323executable frontend
2424 if impl(ghcjs >= 0.2.1)
2525 ghcjs-options: -dedupe
26+ ghcjs-options: -DGHCJS_BUSY_YIELD=20
27+ -DGHCJS_GC_INTERVAL=60000
2628 if flag(development)
2729 cpp-options: -DDEVELOPMENT
2830 main-is: Main.hs
@@ -51,6 +53,7 @@ executable frontend
5153 , Frontend.UIKit.Button.Action
5254 , Frontend.UIKit.Button.Sort
5355 , Reflex.Dom.Renderable
56+ , Reflex.Dom.AsyncEvent
5457 ghc-options:
5558 -Weverything
5659 -Wno-implicit-prelude
@@ -99,6 +102,7 @@ executable frontend
99102 , ViewPatterns
100103 , ApplicativeDo
101104 , PartialTypeSignatures
105+ , DeriveAnyClass
102106 build-depends: aeson
103107 , base
104108 , bytestring
@@ -125,8 +129,11 @@ executable frontend
125129 , reflex
126130 , data-default
127131 , free
128− , parallel
129132 , generic-data
130133 , monoidal-containers
134+ , fuzzyfind
135+ , deepseq
136+ , patch
137+ , reflex-dom-core
131138 hs-source-dirs: src
132139 default-language: Haskell2010
octopod-frontend/src/Data/Text/Search.hs+40−61
@@ -1,4 +1,4 @@
1−{-# OPTIONS_GHC -Wno-redundant-constraints #-}
1+{-# OPTIONS_GHC -Wno-redundant-constraints -Wno-orphans #-}
22
33module Data.Text.Search
44 ( fuzzySearch,
@@ -12,75 +12,42 @@ module Data.Text.Search
1212 )
1313where
1414
15−import Control.Applicative
1615import Control.Applicative.Free.Fast
16+import Control.DeepSeq (NFData)
1717import Control.Lens
18−import Control.Parallel.Strategies
1918import Control.Searchable
20−import Data.Bifunctor
21−import Data.Char
22−import Data.Function
2319import qualified Data.List as L
24−import Data.Maybe (catMaybes)
20+import Data.Maybe (listToMaybe)
2521import Data.Ord
2622import Data.Semigroup
23+import Data.Sequence (Seq (..))
2724import Data.Text (Text)
2825import qualified Data.Text as T
26+import GHC.Generics (Generic)
2927import Reflex.Dom
3028import Reflex.Dom.Renderable
29+import Text.FuzzyFind
3130
32−type Needle = Text
31+type Needle = [String]
3332type Haystack = Text
34−type Running = Int
35−type Penalty = Int
36−
37−fuzzySearch' :: Needle -> Haystack -> Running -> Penalty -> [([FuzzySearchStringChunk String], Int)]
38−fuzzySearch' needle haystack i p = case (T.uncons needle, T.uncons haystack) of
39− (Just _, Nothing) -> []
40− (Just (n, eedle), Just (h, aystack)) ->
41− bimap (prependNotMatched h) (+ (i - p)) <$> fuzzySearch' needle aystack 0 p
42− <|> if toLower n == toLower h
43− then first (prependMatched h) <$> fuzzySearch' eedle aystack (i * 2 + 1) 1
44− else empty
45− (Nothing, Nothing) -> [([], i)]
46− (Nothing, Just _) -> [([NotMatched $ T.unpack haystack], i)]
47−
48−prependMatched :: Char -> [FuzzySearchStringChunk String] -> [FuzzySearchStringChunk String]
49−prependMatched c xs@(NotMatched _ : _) = Matched [c] : xs
50−prependMatched c (Matched cs : xs) = Matched (c : cs) : xs
51−prependMatched c [] = [Matched [c]]
52−
53−prependNotMatched :: Char -> [FuzzySearchStringChunk String] -> [FuzzySearchStringChunk String]
54−prependNotMatched c xs@(Matched _ : _) = NotMatched [c] : xs
55−prependNotMatched c (NotMatched cs : xs) = NotMatched (c : cs) : xs
56−prependNotMatched c [] = [NotMatched [c]]
57−
58−fuzzySearch :: Needle -> Haystack -> Maybe ([FuzzySearchStringChunk Text], Int)
59−fuzzySearch "" h = Just ([NotMatched h], 0)
60−fuzzySearch n h = case fuzzySearch' n h 0 0 of
61− [] -> Nothing
62− xs@(_ : _) -> Just . (first . fmap . fmap) T.pack $ L.maximumBy (compare `on` snd) xs
63−
64−fuzzySearchMany :: Needle -> [Haystack] -> [(Haystack, [FuzzySearchStringChunk Text])]
65−fuzzySearchMany needle haystacks =
66− fmap fst . L.sortOn (Down . snd) $
67− mapMaybe
68− ( \haystack ->
69− fuzzySearch needle haystack
70− <&> \(res, score) -> ((haystack, res), score)
71− )
72− haystacks
33+
34+fuzzySearch :: Needle -> Haystack -> Maybe Alignment
35+fuzzySearch [] h = Just (Alignment 0 $ Result $ pure $ Gap $ T.unpack h)
36+fuzzySearch n h = listToMaybe $ fuzzyFind n [T.unpack h]
37+
38+fuzzySearchMany :: Needle -> [Haystack] -> [(Seq FuzzySearchStringChunk, Haystack)]
39+fuzzySearchMany [] ts = ts <&> \t -> (pure $ NotMatched t, t)
40+fuzzySearchMany n h =
41+ (\(unAlignment -> (res, _), initial) -> (res, T.pack initial))
42+ <$> L.sortOn (Down . score . fst) (fuzzyFindOn id n (T.unpack <$> h))
7343
7444searchMany ::
7545 (Searchable Text x, SearchableConstraint Text x SearchResult) =>
76− Text ->
46+ [String] ->
7747 [x] ->
7848 [Searched x SearchResult]
79−searchMany "" = fmap wrapResult
80−searchMany t =
81− fmap snd . L.sortOn (Down . fst) . catMaybes
82− . withStrategy (parListChunk 3 rpar)
83− . fmap (search t)
49+searchMany [] = fmap wrapResult
50+searchMany t = fmap snd . L.sortOn (Down . fst) . mapMaybe (search t)
8451{-# INLINE searchMany #-}
8552
8653-- | Extract initial structure from search result.
@@ -100,7 +67,7 @@ wrapResult =
10067
10168search ::
10269 (Searchable Text x, SearchableConstraint Text x SearchResult) =>
103− Text ->
70+ Needle ->
10471 x ->
10572 Maybe (Int, Searched x SearchResult)
10673search needle x = runSearchApplicative $ searchWith searchSingle x
@@ -108,14 +75,22 @@ search needle x = runSearchApplicative $ searchWith searchSingle x
10875 searchSingle :: Text -> SearchApplicative SearchResult
10976 searchSingle t = case fuzzySearch needle t of
11077 Nothing -> pure $ SearchResult t Nothing
111− Just (res, score) -> liftAp $ TextSearch t res score
78+ Just (unAlignment -> (res, score)) -> liftAp $ TextSearch t res score
11279{-# INLINE search #-}
11380
81+unAlignment :: Alignment -> (Seq FuzzySearchStringChunk, Score)
82+unAlignment (Alignment score (Result res)) = (unResultSegment <$> res, score)
83+
84+unResultSegment :: ResultSegment -> FuzzySearchStringChunk
85+unResultSegment (Gap x) = NotMatched $ T.pack x
86+unResultSegment (Match x) = Matched $ T.pack x
87+
11488data SearchResult = SearchResult
11589 { initialSearchText :: !Text
116− , searchResult :: !(Maybe [FuzzySearchStringChunk Text])
90+ , searchResult :: !(Maybe (Seq FuzzySearchStringChunk))
11791 }
118− deriving stock (Eq, Ord)
92+ deriving stock (Eq, Ord, Generic)
93+ deriving anyclass (NFData)
11994
12095instance Searchable SearchResult SearchResult where
12196 type Searched SearchResult res = res
@@ -125,9 +100,10 @@ instance Searchable SearchResult SearchResult where
125100instance Renderable SearchResult where
126101 rndr (SearchResult _ (Just cs)) = rndr cs
127102 rndr (SearchResult t Nothing) = rndr t
103+ {-# INLINE rndr #-}
128104
129105data SearchF x where
130− TextSearch :: Text -> [FuzzySearchStringChunk Text] -> !Int -> SearchF SearchResult
106+ TextSearch :: Text -> Seq FuzzySearchStringChunk -> !Int -> SearchF SearchResult
131107
132108type SearchApplicative = Ap SearchF
133109
@@ -145,10 +121,13 @@ runSearchApplicative x = case findMaxResult x of
145121 where
146122 findMaxResult :: SearchApplicative a -> Maybe Int
147123 findMaxResult = fmap getMax . runAp_ (\(TextSearch _ _ i) -> Just $ Max i)
124+{-# INLINE runSearchApplicative #-}
148125
149−data FuzzySearchStringChunk a = NotMatched !a | Matched !a
150− deriving stock (Show, Eq, Ord, Functor)
126+data FuzzySearchStringChunk = NotMatched !Text | Matched !Text
127+ deriving stock (Show, Eq, Ord, Generic)
128+ deriving anyclass (NFData)
151129
152−instance Renderable a => Renderable (FuzzySearchStringChunk a) where
130+instance Renderable FuzzySearchStringChunk where
153131 rndr (NotMatched a) = rndr a
154132 rndr (Matched a) = elAttr "span" ("style" =: "text-decoration: underline;") (rndr a)
133+ {-# INLINE rndr #-}
octopod-frontend/src/Frontend/UIKit.hs+72−31
@@ -18,12 +18,21 @@ module Frontend.UIKit
1818 deletedOverride,
1919 showNonEditableWorkingOverride,
2020 NonEditableWorkingOverrideStyle (..),
21+ untilReadyEv',
22+ untilReady',
23+ runWithReplace',
24+ joinEvM,
2125 )
2226where
2327
2428import Control.Lens
29+ ( ASetter',
30+ (%~),
31+ (<>~),
32+ (?~),
33+ (^.),
34+ )
2535import Control.Monad
26−import Control.Monad.IO.Class
2736import Data.Align
2837import Data.Default
2938import Data.Functor
@@ -33,8 +42,10 @@ import qualified Data.List.NonEmpty as NE
3342import qualified Data.Map as M
3443import Data.Maybe (isNothing, maybeToList)
3544import Data.Text (Text)
45+import qualified Data.Text as T
3646import Data.Text.Search
3747import Data.These
48+import Data.Witherable
3849import Data.WorkingOverrides
3950import Frontend.Classes as X
4051import Frontend.UIKit.Button.Action as X
@@ -45,6 +56,7 @@ import Frontend.UIKit.Button.Sort as X
4556import Frontend.UIKit.Button.Static as X
4657import GHC.Generics (Generic)
4758import Reflex.Dom
59+import Reflex.Dom.AsyncEvent
4860import Reflex.Dom.Renderable
4961import Reflex.Network
5062
@@ -193,7 +205,7 @@ octopodTextInput' valuesDyn disabledDyn clssDyn placeholder inValDyn' errEv = md
193205octopodTextInputDyn ::
194206 MonadWidget t m =>
195207 -- | Value to suggest
196− (Dynamic t [Text]) ->
208+ Dynamic t [Text] ->
197209 -- | Disabled?
198210 Dynamic t Bool ->
199211 -- | Input field classes.
@@ -201,7 +213,7 @@ octopodTextInputDyn ::
201213 -- | Placeholder for input field.
202214 Text ->
203215 -- | Init value.
204− (Dynamic t Text) ->
216+ Dynamic t Text ->
205217 -- | Event carrying the error message.
206218 Dynamic t (Maybe (NonEmpty Text)) ->
207219 m (InputElement EventResult GhcjsDomSpace t)
@@ -256,36 +268,39 @@ octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn =
256268 simpleList ((maybeToList >=> NE.toList) <$> errDyn) $ \err ->
257269 divClass "input__output" $ dynText err
258270
259− delayedFalseFocus <- delayFalse $ _inputElement_hasFocus inp'
260− selectedValue' <- networkView >=> switchHoldPromptly never $ do
261− hasFocus <- delayedFalseFocus
262− values <- valuesDyn
263− case hasFocus of
264− True | (_ : _) <- values -> do
271+ let searchValuesEv = catMaybes . updated $ do
272+ hasFocus <- _inputElement_hasFocus inp'
273+ values <- valuesDyn
265274 currVal <- valDyn
266− case fuzzySearchMany currVal values of
267− [] -> pure $ pure never
268− ress ->
269− pure $ do
270− elClass "ul" "overrides__search" $ do
271− fmap leftmost $
272− forM ress $ \(initialText, res) -> do
273− (resEl, ()) <- elClass' "li" "overrides__search-item" $
274− forM_ res $ \case
275− Matched t -> elAttr "span" ("style" =: "font-weight: bold;") $ text t
276− NotMatched t -> text t
277− pure $ domEvent Click resEl $> initialText
278− _ -> pure $ pure never
279− pure (inp', selectedValue')
280− pure inp
275+ pure $ case hasFocus of
276+ True | (_ : _) <- values -> Just (values, currVal)
277+ _ -> Nothing
278+ searchResultEv <- asyncEventLast searchValuesEv $ \(values, currVal) ->
279+ fuzzySearchMany [T.unpack currVal] values
280+ selectedValueEv <-
281+ (>>= joinEvM) $
282+ runWithReplace' (pure ()) $
283+ updated (_inputElement_hasFocus inp') <&> \case
284+ False -> pure never
285+ True ->
286+ (>>= joinEvM) $
287+ runWithReplace' (pure ()) $
288+ searchResultEv <&> \case
289+ [] -> pure never
290+ searchResult -> do
291+ elClass "ul" "overrides__search" $ do
292+ fmap leftmost $
293+ forM searchResult $ \(res, initialText) -> do
294+ (resEl, ()) <- elClass' "li" "overrides__search-item" $
295+ forM_ res $ \case
296+ Matched t -> elAttr "span" ("style" =: "font-weight: bold;") $ text t
297+ NotMatched t -> text t
298+ -- Because 'Click' would fire after the text field loses
299+ -- focus and the popup disappears.
300+ pure $ domEvent Mousedown resEl $> initialText
281301
282−delayFalse :: (MonadHold t m, PerformEvent t m, TriggerEvent t m, MonadIO (Performable m)) => Dynamic t Bool -> m (Dynamic t Bool)
283−delayFalse x = do
284− initVal <- sample . current $ x
285− let trueEv = ffilter id $ updated x
286− falseEv = ffilter not $ updated x
287− delayedFalseEv <- delay 0.1 falseEv
288− holdDyn initVal $ leftmost [trueEv, delayedFalseEv]
302+ pure (inp', selectedValueEv)
303+ pure inp
289304
290305-- | Dark unclickable background for opened sidebar.
291306popupOverlay :: DomBuilder t m => m ()
@@ -346,3 +361,29 @@ showNonEditableWorkingOverride loading loaded style cfg =
346361 divClass "listing__item" $ do
347362 elClass "div" "listing__placeholder" $ pure ()
348363 elClass "div" "listing__spinner" $ pure ()
364+
365+untilReadyEv' ::
366+ (Adjustable t m, PostBuild t m, MonadHold t m) =>
367+ m a ->
368+ m (Event t b) ->
369+ m (Event t b)
370+untilReadyEv' m m' = do
371+ (_, bEvEv) <- untilReady m m'
372+ switchHold never bEvEv
373+
374+untilReady' ::
375+ (Adjustable t m, PostBuild t m) =>
376+ m a ->
377+ m b ->
378+ m (Event t b)
379+untilReady' m m' = do
380+ (_, bEv) <- untilReady m m'
381+ pure bEv
382+
383+runWithReplace' :: Adjustable t m => m a -> Event t (m b) -> m (Event t b)
384+runWithReplace' ma mbEv = do
385+ (_, bEv) <- runWithReplace ma mbEv
386+ pure bEv
387+
388+joinEvM :: (MonadHold t m, Reflex t) => Event t (Event t a) -> m (Event t a)
389+joinEvM = switchHold never
octopod-frontend/src/Frontend/Utils.hs+94−30
@@ -42,17 +42,23 @@ import Control.Lens
4242import Control.Monad
4343import Control.Monad.Reader
4444import Control.Monad.State.Strict
45+import qualified Data.Bifunctor as Bi
4546import qualified Data.Foldable as F
4647import Data.Functor
48+import Data.Functor.Misc
4749import Data.Generics.Labels ()
4850import Data.List.NonEmpty (NonEmpty)
51+import Data.Map (Map)
4952import qualified Data.Map as M
5053import Data.Map.Monoidal.Strict (MonoidalMap)
5154import qualified Data.Map.Monoidal.Strict as MM
5255import Data.Maybe (fromMaybe, isJust, isNothing)
5356import Data.Monoid
57+import Data.Set (Set)
58+import qualified Data.Set as S
5459import Data.Text as T (Text, null, pack)
5560import Data.Text.Search
61+import Data.These
5662import Data.Time
5763import Data.UniqMap
5864import Data.Unique
@@ -362,10 +368,13 @@ deploymentPopupBody hReq defAppOv defDepOv errEv = mdo
362368 ovsDyn <- holdDyn ovs ovsEv
363369 ovsDynDebounced <- holdDyn ovs ovsEvDebounced
364370 x <- attachDyn (current ovsDyn) dCfgDyn
365− res <- dyn (x <&> \(ovs', dCfg) -> envVarsInput values dCfg ovs')
366− ovsEv <- switchHold never (fst <$> res)
371+ (splitE -> (ovsEvEv, isValidDynEv)) <-
372+ (>>= joinEvM) $
373+ runWithReplace' loadingOverrides $
374+ updated x <&> \(ovs', dCfg) -> untilReady' loadingOverrides $ envVarsInput values dCfg ovs'
375+ ovsEv <- joinEvM ovsEvEv
367376 ovsEvDebounced <- debounce 2 ovsEv
368− isValid <- join <$> holdDyn (pure True) (snd <$> res)
377+ isValid <- join <$> holdDyn (pure False) isValidDynEv
369378 pure (ovsDyn, ovsDynDebounced, isValid)
370379
371380 depKeysDyn <- holdDyn [] depKeys
@@ -494,8 +503,16 @@ envVarsInput ::
494503 -- | Updated deployment overrides.
495504 m (Event t (Overrides l), Dynamic t Bool)
496505envVarsInput values dCfg ovs = mdo
497− envsDyn <- foldDyn appEndo (constructWorkingOverrides dCfg ovs) $ leftmost [addEv, updEv]
498− let addEv = clickEv $> Endo (fst . insertUniqStart newWorkingOverride)
506+ let initialOverrides = constructWorkingOverrides dCfg ovs
507+ (envsDyn, addedKeys) <-
508+ mapAccumDyn
509+ ( \prevOvs -> \case
510+ Left () ->
511+ Bi.second S.singleton $ insertUniqStart newWorkingOverride prevOvs
512+ Right (Endo f) -> (f prevOvs, S.empty)
513+ )
514+ initialOverrides
515+ $ leftmost [Left <$> clickEv, Right <$> updEv]
499516 clickEv <-
500517 dashButton
501518 DashButtonConfig
@@ -505,18 +522,58 @@ envVarsInput values dCfg ovs = mdo
505522 , buttonStyle = OverridesDashButtonStyle
506523 }
507524 let workingOverridesWithErrors = validateWorkingOverrides . uniqMap <$> envsDyn
508− isValid = all ((== mempty) . snd) <$> workingOverridesWithErrors
509− updEv <-
525+ erroredOverride = M.filter ((/= mempty) . snd) <$> workingOverridesWithErrors
526+ isValid = M.null <$> erroredOverride
527+ (_, changedValues) <-
528+ mapAccumDyn
529+ ( \prev new ->
530+ ( fmap (\(v, _) -> (v, mempty)) new
531+ , new <> prev
532+ )
533+ )
534+ mempty
535+ (updated erroredOverride)
536+ let updatedValues = restrictMapEv updatedKeys (updated workingOverridesWithErrors)
537+ valChangedEvSelector = fanMap (clearedOvs <> changedValues <> updatedValues)
538+ (splitE -> (clearedOvs, deletedOvs)) =
539+ alignEventWithMaybe
540+ ( \case
541+ These keys m ->
542+ Just $
543+ let cleared = M.restrictKeys m keys
544+ in (cleared, M.fromSet (const Nothing) (keys `S.difference` M.keysSet cleared))
545+ _ -> Nothing
546+ )
547+ removedKeys
548+ (updated workingOverridesWithErrors)
549+ listChange =
550+ deletedOvs <> (fmap . fmap) Just (restrictMapEv addedKeys (updated workingOverridesWithErrors))
551+
552+ (splitE3 -> (updEv, removedKeys, updatedKeys)) <-
510553 switchDyn . fmap F.fold
511− <$> listWithKey
512− workingOverridesWithErrors
513− (\i x -> fmap (performUserOverrideAction (lookupDefaultConfig <$> dCfg) i) <$> envVarInput values x)
554+ <$> listHoldWithKey
555+ (validateWorkingOverrides . uniqMap $ initialOverrides)
556+ listChange
557+ ( \i val -> do
558+ valDyn <- holdDyn val $ select valChangedEvSelector $ Const2 i
559+ userActionEv <- envVarInput values valDyn
560+ pure $ performUserOverrideAction (lookupDefaultConfig <$> dCfg) i <$> userActionEv
561+ )
514562 let addingIsEnabled = all (\(WorkingOverrideKey _ x, _) -> not . T.null $ x) . elemsUniq <$> envsDyn
515563 case dCfg of
516564 Just _ -> pure ()
517565 Nothing -> loadingOverrides
518566 pure (updated $ destructWorkingOverrides <$> envsDyn, isValid)
519567
568+restrictMapEv :: (Reflex t, Ord k) => Event t (Set k) -> Event t (Map k v) -> Event t (Map k v)
569+restrictMapEv =
570+ alignEventWithMaybe
571+ ( \case
572+ These keys _ | S.null keys -> Nothing
573+ These keys m -> Just $ M.restrictKeys m keys
574+ _ -> Nothing
575+ )
576+
520577validateWorkingOverrides ::
521578 forall f.
522579 Traversable f =>
@@ -567,7 +624,7 @@ envVarInput ::
567624 -- | Current variable key and value.
568625 Dynamic t (WorkingOverride, OverrideErrors) ->
569626 m (Event t UserOverrideAction)
570−envVarInput values val = do
627+envVarInput values val = untilReadyEv' loadingOverride $ do
571628 let kDyn = val <&> fst <&> \(WorkingOverrideKey _ x, _) -> x
572629 -- Either <override present> <override deleted>
573630 d <-
@@ -622,25 +679,29 @@ performUserOverrideAction ::
622679 Maybe (Text -> Maybe Text) ->
623680 Int ->
624681 UserOverrideAction ->
625− Endo WorkingOverrides
626−performUserOverrideAction f i (UpdateValue v) = Endo $
627− updateUniq i $ \(k@(WorkingOverrideKey _ kt), _) ->
628− ( k
629− , case f >>= ($ kt) of
630− Just v' | v == v' -> WorkingDefaultValue v
631− _ -> WorkingCustomValue v
632− )
633−performUserOverrideAction _ i (UpdateKey k) = Endo $
634− updateUniq i $
635− \(_, v) -> (WorkingOverrideKey CustomWorkingOverrideKey k, v)
636−performUserOverrideAction f i DeleteOverride = Endo $ \m ->
637− case f of
638− Nothing -> updateUniq i (\(k, _) -> (k, WorkingDeletedValue Nothing)) m
639− Just _ -> case lookupUniq i m of
640− Nothing -> m
641− Just (WorkingOverrideKey DefaultWorkingOverrideKey k, _) ->
642− updateUniq i (const (WorkingOverrideKey DefaultWorkingOverrideKey k, WorkingDeletedValue (f >>= ($ k)))) m
643− Just (WorkingOverrideKey CustomWorkingOverrideKey _, _) -> deleteUniq i m
682+ -- | (endo, removed/cleared keys, updatedKeys)
683+ (Endo WorkingOverrides, Set Int, Set Int)
684+performUserOverrideAction f i (UpdateValue v) = (,S.empty,S.singleton i) $
685+ Endo $
686+ updateUniq i $ \(k@(WorkingOverrideKey _ kt), _) ->
687+ ( k
688+ , case f >>= ($ kt) of
689+ Just v' | v == v' -> WorkingDefaultValue v
690+ _ -> WorkingCustomValue v
691+ )
692+performUserOverrideAction _ i (UpdateKey k) = (,S.empty,S.singleton i) $
693+ Endo $
694+ updateUniq i $
695+ \(_, v) -> (WorkingOverrideKey CustomWorkingOverrideKey k, v)
696+performUserOverrideAction f i DeleteOverride = (,S.singleton i,S.empty) $
697+ Endo $ \m ->
698+ case f of
699+ Nothing -> updateUniq i (\(k, _) -> (k, WorkingDeletedValue Nothing)) m
700+ Just _ -> case lookupUniq i m of
701+ Nothing -> m
702+ Just (WorkingOverrideKey DefaultWorkingOverrideKey k, _) ->
703+ updateUniq i (const (WorkingOverrideKey DefaultWorkingOverrideKey k, WorkingDeletedValue (f >>= ($ k)))) m
704+ Just (WorkingOverrideKey CustomWorkingOverrideKey _, _) -> deleteUniq i m
644705
645706holdDynMaybe :: (Reflex t, MonadHold t m) => Event t a -> m (Dynamic t (Maybe a))
646707holdDynMaybe ev = holdDyn Nothing $ fmapCheap Just ev
@@ -685,3 +746,6 @@ catchReturns :: (DomBuilder t m, MonadFix m) => (Event t () -> m a) -> m a
685746catchReturns f = mdo
686747 (divEl, a) <- el' "div" $ f $ ($> ()) . ffilter (== 13) $ domEvent Keypress divEl
687748 pure a
749+
750+splitE3 :: Reflex t => Event t (a, b, c) -> (Event t a, Event t b, Event t c)
751+splitE3 e = ((\(a, _, _) -> a) <$> e, (\(_, b, _) -> b) <$> e, (\(_, _, c) -> c) <$> e)
octopod-frontend/src/Page/Deployments.hs+6−4
@@ -24,7 +24,6 @@ import Common.Utils
2424import Control.Applicative
2525import Control.Monad.Reader
2626import Data.Align
27−import Data.Char
2827import Data.Functor
2928import qualified Data.Semigroup as S
3029import qualified Data.Text as T
@@ -39,6 +38,7 @@ import Page.ClassicPopup
3938import Page.Elements.Links
4039import Page.Popup.EditDeployment
4140import Page.Popup.NewDeployment
41+import Reflex.Dom.AsyncEvent
4242import Reflex.Dom.Renderable
4343import Reflex.MultiEventWriter.Class
4444import Servant.Reflex.Extra
@@ -190,9 +190,11 @@ deploymentsListWidget hReq updAllEv termDyn ds = dataWidgetWrapper $ mdo
190190 let okUpdEv = fmapMaybe reqSuccess updRespEv
191191 errUpdEv = fmapMaybe reqErrorBody updRespEv
192192 dsDyn <- holdDyn ds okUpdEv
193− let searchedDyn = ffor2 termDyn dsDyn $ \term ds' ->
194− searchMany (T.filter (not . isSpace) term) ds'
195− (archivedDsDyn, activeDsDyn) =
193+ let searchInput = updated $ ffor2 termDyn dsDyn (,)
194+ searchedEv <- asyncEventLast searchInput $ \(term, ds') ->
195+ searchMany (T.unpack <$> T.words term) ds'
196+ searchedDyn <- holdDyn (wrapResult <$> ds) searchedEv
197+ let (archivedDsDyn, activeDsDyn) =
196198 splitDynPure $ L.partition isDeploymentArchived <$> searchedDyn
197199 searchSorting = termDyn $> Nothing
198200 clickedEv <- elementClick
octopod-frontend/src/Reflex/Dom/AsyncEvent.hsnew file+30
@@ -0,0 +1,30 @@
1+module Reflex.Dom.AsyncEvent
2+ ( NFData,
3+ asyncEventLast,
4+ )
5+where
6+
7+import Control.Concurrent
8+import Control.DeepSeq (NFData, force)
9+import Control.Exception (evaluate)
10+import Control.Monad.IO.Class
11+import Data.Functor
12+import Data.IORef
13+import Reflex
14+
15+asyncEventLast ::
16+ (TriggerEvent t m, PerformEvent t m, MonadIO (Performable m), MonadIO m, NFData b) =>
17+ Event t a ->
18+ (a -> b) ->
19+ m (Event t b)
20+asyncEventLast aEv f = do
21+ lastThreadIdRef <- liftIO $ newIORef Nothing
22+ performEventAsync $
23+ aEv <&> \a fire -> liftIO $ do
24+ readIORef lastThreadIdRef >>= \case
25+ Nothing -> pure ()
26+ Just lastThreadId -> killThread lastThreadId
27+ newThreadId <- forkIO $ do
28+ b <- evaluate $ force $ f a
29+ fire b
30+ writeIORef lastThreadIdRef (Just newThreadId)
octopod-frontend/src/Reflex/Dom/Renderable.hs+20−2
@@ -3,15 +3,33 @@ module Reflex.Dom.Renderable
33 )
44where
55
6+import Data.Sequence (Seq (..))
67import Data.Text (Text)
8+import qualified Data.Text as T
79import Reflex.Dom
810
911class Renderable a where
1012 rndr :: DomBuilder t m => a -> m ()
13+ rndrList :: DomBuilder t m => [a] -> m ()
14+ rndrList [] = pure ()
15+ rndrList (a : aa) = rndr a >> rndr aa
16+ {-# INLINE rndrList #-}
1117
1218instance Renderable Text where
1319 rndr = text
20+ {-# INLINE rndr #-}
1421
1522instance Renderable a => Renderable [a] where
16− rndr [] = pure ()
17− rndr (a : aa) = rndr a >> rndr aa
23+ rndr = rndrList
24+ {-# INLINE rndr #-}
25+
26+instance Renderable Char where
27+ rndr = text . T.singleton
28+ {-# INLINE rndr #-}
29+ rndrList = text . T.pack
30+ {-# INLINE rndrList #-}
31+
32+instance Renderable a => Renderable (Seq a) where
33+ rndr Empty = pure ()
34+ rndr (a :<| aa) = rndr a >> rndr aa
35+ {-# INLINE rndr #-}