Added tree configuration UI (#167)

* bumped nix things

* Made CLI show more helpful errors

* wip

* done server part

* Tree overrides

* iwp

* I did everything i could

* Made it compile I don't care anymore

* ordered-containers

* wip

* Fixed formatting

* Fixed macos build

* bump nix

* Rollback nix sources
This commit is contained in:
iko 2022-04-20 09:50:51 +03:00 committed by GitHub
parent 602ae2314c
commit 83d275df7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1148 additions and 827 deletions

View File

@ -29,7 +29,7 @@ jobs:
- uses: cachix/install-nix-action@v12
- name: Run fourmolu
run: |
nix-shell nix/ci.nix -j auto --run "git ls-files '*.hs' | xargs fourmolu -m inplace --ghc-opt -XRecursiveDo --ghc-opt -XTypeApplications"
nix-shell nix/ci.nix -j auto --run "git ls-files '*.hs' | xargs fourmolu -m inplace --ghc-opt -XRecursiveDo --ghc-opt -XTypeApplications --ghc-opt -XPatternSynonyms"
git diff --exit-code
octo-macOS:

5
.nova/Configuration.json Normal file
View File

@ -0,0 +1,5 @@
{
"env-generator" : "nix-shell --run env",
"haskell.checkProject" : false,
"haskell.formattingProvider" : "fourmolu"
}

View File

@ -1,4 +1,7 @@
{
"nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix",
"haskell.formattingProvider": "fourmolu"
"haskell.formattingProvider": "fourmolu",
"haskell.plugin.hlint.diagnosticsOn": false,
"haskell.hlint.logLevel": "none",
"haskell.hlint.run": "never"
}

View File

@ -31,5 +31,11 @@ source-repository-package
subdir: lib/executable-config/lookup
--sha256: 0dlk8y6rxc87crw7764zq2py7nqn38lw496ca1y893m9gdq8qdkz
source-repository-package
type: git
location: https://github.com/typeable/ordered-containers.git
tag: 3eb05fb2f44fe482e9092aff447e9c68fbd6a7f6
--sha256: 0q3cp3nar41bqh2ldl1ykzaycf0bjxh0n58za5g19x4g7ab8h9gm
package reflex-dom
flags: +use-warp

View File

@ -104,6 +104,7 @@ in
#!${pkgs.bash}/bin/bash
echo "You did something wrong :("
echo "You did something wrong twice :("
1>&2 echo "You did something wrong, but you shouldn't see this."
@ -117,7 +118,27 @@ in
for i in {1..10}
do
echo "key$i,value"
for j in {1..10}
do
echo "foo.vat$i.fjij.key$j,value"
done
done
for i in {1..10}
do
echo "fjij.key$i,value"
done
exit 0
'';
smallInfoScript = pkgs.writeScript "info.sh" ''
#!${pkgs.bash}/bin/bash
sleep 4
for i in {1..10}
do
echo "fjij.key$i,value"
done
exit 0
@ -130,7 +151,16 @@ in
for i in {1..10}
do
echo "key$i"
echo "foo.vat.fkijf.fjij.key$i"
done
for i in {1..10}
do
echo "foo.vat.fjij.key$i"
done
for i in {1..10}
do
echo "fjij.key$i"
done
exit 0
@ -162,8 +192,8 @@ in
export CHECKING_COMMAND=${echoScript}
export CLEANUP_COMMAND=${echoScript}
export ARCHIVE_CHECKING_COMMAND=${echoScript}
export CONFIG_CHECKING_COMMAND=${echoScript}
export INFO_COMMAND=${infoScript}
export CONFIG_CHECKING_COMMAND=${failScript}
export INFO_COMMAND=${smallInfoScript}
export NOTIFICATION_COMMAND=${writeScript}
export DEPLOYMENT_CONFIG_COMMAND=${infoScript}
export DEPLOYMENT_KEYS_COMMAND=${keysScript}

View File

@ -59,7 +59,6 @@ executable octo
transformers,
table-layout ^>= 0.9.0.0,
servant-auth,
ordered-containers,
http-types,
hs-source-dirs: src
default-language: Haskell2010

View File

@ -13,9 +13,10 @@ import Data.Aeson (decode)
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy.Char8 as LBSC
import Data.Coerce
import qualified Data.ConfigTree as CT
import Data.Foldable
import Data.Generics.Labels ()
import Data.Generics.Product
import qualified Data.Map.Ordered.Strict as OM
import Data.Maybe
import Data.Proxy
import Data.Text (Text)
@ -123,24 +124,10 @@ handleUpdate ::
handleUpdate auth dName dNewAppOvs removedAppOvs dNewDepOvs removedDepOvs = do
clientEnv <- ask
dep <- runClientM' (_fullInfoH auth dName) clientEnv
let removeAll :: Ord k => [k] -> OM.OMap k v -> Either k (OM.OMap k v)
removeAll [] m = Right m
removeAll (k : kk) m =
if k `OM.member` m
then removeAll kk $ OM.delete k m
else Left k
removeAllM :: MonadIO m => [Text] -> Overrides l -> m (Overrides l)
removeAllM ks (Overrides m) =
either
(\k -> dieT $ "Override " <> k <> " not present in deployment.")
(pure . Overrides)
$ removeAll ks m
appOverrides' <-
fmap (<> dNewAppOvs) $
removeAllM removedAppOvs $ dep ^. #deployment . #appOverrides
deploymentOverrides' <-
fmap (<> dNewDepOvs) $
removeAllM removedDepOvs $ dep ^. #deployment . #deploymentOverrides
let removeAll :: [Text] -> Overrides l -> (Overrides l)
removeAll ks (Overrides (CT.toFlatList -> m)) = Overrides $ CT.fromFlatList $ filter (flip elem ks . fst) m
appOverrides' = dNewAppOvs <> (removeAll removedAppOvs $ dep ^. #deployment . #appOverrides)
deploymentOverrides' = dNewDepOvs <> (removeAll removedDepOvs $ dep ^. #deployment . #deploymentOverrides)
liftIO $ do
let dUpdate =
DeploymentUpdate
@ -295,7 +282,7 @@ printInfo (DeploymentInfo (Deployment _ dAppOvs dStOvs) (DeploymentMetadata dMet
putStrLn $ unlines $ formatOverrides False dStOvs
T.putStrLn ""
T.putStrLn $ "Metadata: "
forM_ dMeta $ \m ->
for_ dMeta $ \m ->
T.putStrLn $
" " <> m ^. #name <> ": " <> m ^. #link
T.putStrLn ""
@ -352,7 +339,7 @@ formatOverrides splitlines (Overrides m) =
]
unicodeS
def
$ showOverride <$> (reverse . OM.assocs) m
$ showOverride <$> (reverse . CT.toFlatList) m
where
showOverride (k, v) =
colsAllG top $ [if splitlines then T.chunksOf 15 k else [k], showValue v]

View File

@ -68,7 +68,8 @@ readLogOutput _ = Nothing
-- | Parses octo CLI subcommands and arguments.
parseArgs :: IO Args
parseArgs = execParser $ info (commandArgs <**> helper) fullDesc
parseArgs =
customExecParser (prefs showHelpOnError) $ info (commandArgs <**> helper) fullDesc
-- | Parses octo CLI subcommands.
commandArgs :: Parser Args

View File

@ -1,3 +1,4 @@
{- ORMOLU_DISABLE -}
{-# LANGUAGE CPP #-}
module Octopod.API.WebSocket

View File

@ -113,7 +113,6 @@ library
, time
, hasql
, hasql-transaction
, ordered-containers
, unordered-containers
, vector
, process

View File

@ -12,6 +12,7 @@ import Control.Monad.Base
import Control.Monad.Except
import Control.Monad.Trans.Control
import Data.Fixed
import Data.Foldable
import Data.IORef.Lifted
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
@ -81,7 +82,7 @@ lookupBlocking (CacheMap ref createValue update) k = do
res@(Right _) -> do
putMVar var res
pure $ (var, if recompute then Just blank else Nothing)
forM_ mwVar $ \wVar -> do
for_ mwVar $ \wVar -> do
fork $ do
v <- (Right <$> createValue k) `catchError` (pure . Left)
putMVar wVar v

View File

@ -23,6 +23,7 @@ import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.Char8 as BSLC
import Data.Coerce
import Data.Conduit (ConduitT, yield)
import qualified Data.ConfigTree as CT
import qualified Data.Csv as C
import Data.Fixed
import Data.Foldable
@ -32,7 +33,6 @@ import Data.Generics.Labels ()
import Data.Generics.Product
import Data.IORef.Lifted
import Data.Int
import qualified Data.Map.Ordered.Strict as OM
import Data.Maybe
import Data.Pool
import Data.Text (pack)
@ -192,7 +192,7 @@ runOctopodServer sha = do
decodeCSVDefaultConfig :: BSL.ByteString -> Either String (DefaultConfig l)
decodeCSVDefaultConfig bs = do
x <- C.decode C.NoHeader bs
pure $ DefaultConfig . OM.fromList . V.toList $ x
pure $ DefaultConfig . CT.fromFlatList . V.toList $ x
either500S :: (KatipContext m, MonadError ServerError m) => Either String x -> m x
either500S (Right x) = pure x
either500S (Left err) = do
@ -478,7 +478,7 @@ getSingleFullInfo dName = do
d <- each deploymentSchema
where_ $ d ^. #name ==. litExpr dName
pure d
deployments <- forM deploymentsSchema extractDeploymentFullInfo
deployments <- for deploymentsSchema extractDeploymentFullInfo
logLocM DebugS $ "get deployments: " <> show' deployments
return $ listToMaybe deployments
@ -489,7 +489,7 @@ getFullInfo = do
deploymentsSchema <-
runStatement . select . orderBy (view #updatedAt >$< desc) $
each deploymentSchema
deployments <- forM deploymentsSchema extractDeploymentFullInfo
deployments <- for deploymentsSchema extractDeploymentFullInfo
logLocM DebugS $ "get deployments: " <> show' deployments
return deployments
@ -739,7 +739,7 @@ transitionToStatus dName s = do
(deps, oldS) <- either throwError pure res
dep <- lift $ ensureOne deps
lift $
forM_ (processOutput s) $ \(output, act) ->
for_ (processOutput s) $ \(output, act) ->
createDeploymentLog
dep
act
@ -748,7 +748,7 @@ transitionToStatus dName s = do
(output ^. #stdout)
(output ^. #stderr)
notificationCmd <- asks notificationCommand
forM_ notificationCmd $ \nCmd ->
for_ notificationCmd $ \nCmd ->
runBgWorker . void $
runCommandArgs' nCmd
=<< notificationCommandArgs dName oldS newS
@ -1307,7 +1307,7 @@ runDeploymentBgWorker newS dName pre post = do
dName
(respondSync $ Left err409 {errBody = "The deployment is currently being processed."})
( do
let preWithAssert = pre <* forM newS (assertDeploymentTransitionPossibleS dName)
let preWithAssert = pre <* for newS (assertDeploymentTransitionPossibleS dName)
result <- catchError (Right <$> preWithAssert) (pure . Left)
respondSync result
liftEither result >>= \res -> do

View File

@ -37,9 +37,9 @@ import Control.Monad.Base
import Control.Monad.Reader
import qualified Data.ByteString.Lazy as TL
import Data.Coerce
import qualified Data.ConfigTree as CT
import Data.Fixed
import Data.Generics.Product.Typed
import qualified Data.Map.Ordered.Strict as MO
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Time
@ -257,7 +257,7 @@ overridesArgs :: forall l. KnownOverrideLevel l => Config l -> ControlScriptArgs
overridesArgs (Config cc) =
ControlScriptArgs
. concatMap (\(T.unpack -> k, T.unpack -> v) -> [argumentName, k <> "=" <> v])
. MO.assocs
. CT.toFlatList
$ cc
where
argumentName = case knownOverrideLevel @l of

View File

@ -1,8 +1,8 @@
module Octopod.Server.Posix (installShutdownHandler) where
import Control.Monad
import Control.Monad.Trans.Control
import Data.Aeson
import Data.Traversable
import Octopod.Server.Logging
import System.Posix.Signals
@ -21,7 +21,7 @@ installShutdownHandler ::
m () ->
m [Handler]
installShutdownHandler signals action =
forM signals $ \signal -> liftBaseWith $ \run ->
for signals $ \signal -> liftBaseWith $ \run ->
installHandler signal (Catch $ run (handler signal)) Nothing
where
handler signal = katipAddNamespace "signal handler" $

View File

@ -18,7 +18,7 @@ library
exposed-modules: Common.Types
, Common.Utils
, Common.Validation
, Data.Map.Ordered.Strict.Extra
, Data.ConfigTree
, Control.Searchable
, Common.Orphans
-- other-modules:
@ -56,6 +56,7 @@ library
, UndecidableInstances
, FunctionalDependencies
, QuantifiedConstraints
, PatternSynonyms
build-depends: base
, aeson
, cassava
@ -69,6 +70,7 @@ library
, time
, ordered-containers
, deepseq
, these
hs-source-dirs: src
default-language: Haskell2010
ghc-options:

View File

@ -3,8 +3,18 @@
module Common.Orphans () where
import Control.DeepSeq
import Data.Map.Ordered
import Data.Aeson
import Data.Map.Internal
import Data.Map.Ordered.Internal
import qualified Data.Map.Ordered.Strict as OM
import GHC.Generics (Generic)
-- better than nothing
instance (NFData a, NFData b) => NFData (OMap a b) where
rnf = rnf . toMap
deriving stock instance Generic (Map k v)
deriving stock instance Generic (OMap k v)
deriving anyclass instance (NFData k, NFData v) => NFData (OMap k v)
instance (ToJSON k, ToJSON v) => ToJSON (OMap k v) where
toJSON = toJSON . OM.assocs
instance (FromJSON k, FromJSON v, Ord k) => FromJSON (OMap k v) where
parseJSON = fmap OM.fromList . parseJSON

View File

@ -12,12 +12,12 @@ import Control.DeepSeq (NFData)
import Control.Lens
import Control.Searchable
import Data.Aeson hiding (Result)
import Data.ConfigTree (ConfigTree)
import qualified Data.ConfigTree as CT
import Data.Csv
import Data.Generics.Labels ()
import Data.Int
import Data.Map.Ordered.Strict (OMap, (<>|))
import qualified Data.Map.Ordered.Strict as OM
import Data.Map.Ordered.Strict.Extra ()
import Data.List.NonEmpty (NonEmpty)
import Data.Maybe
import Data.Text (Text)
import qualified Data.Text as T
@ -54,21 +54,21 @@ type OverrideValue = OverrideValue' Text
type DefaultConfig = DefaultConfig' Text
newtype DefaultConfig' te (l :: OverrideLevel) = DefaultConfig (OMap te te)
deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
newtype DefaultConfig' te (l :: OverrideLevel) = DefaultConfig (ConfigTree te te)
deriving newtype (Eq, Ord, Show)
deriving newtype instance FromJSON (DefaultConfig' Text l)
deriving newtype instance ToJSON (DefaultConfig' Text l)
instance Searchable t x => Searchable t (DefaultConfig' x l) where
type SearchableConstraint t (DefaultConfig' x l) res = (Ord (Searched x res), SearchableConstraint t x res)
type Searched (DefaultConfig' x l) res = DefaultConfig' (Searched x res) l
searchWith f (DefaultConfig oMap) = do
oMap' <- searchWith f . OM.assocs $ oMap
pure $ DefaultConfig $ OM.fromList oMap'
searchWith f (DefaultConfig ct) = DefaultConfig <$> searchWith f ct
{-# INLINE searchWith #-}
lookupDefaultConfig :: DefaultConfig l -> Text -> Maybe Text
lookupDefaultConfig (DefaultConfig m) k = OM.lookup k m
type ConfigKey = [Text]
newtype Config (l :: OverrideLevel) = Config {unConfig :: OMap Text Text}
newtype Config (l :: OverrideLevel) = Config {unConfig :: ConfigTree Text Text}
deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
data FullDefaultConfig = FullDefaultConfig
@ -87,63 +87,58 @@ data FullConfig = FullConfig
applyOverrides :: Overrides l -> DefaultConfig l -> Config l
applyOverrides (Overrides oo) (DefaultConfig dd) =
Config . extract $ oo <>| (ValueAdded <$> dd)
Config . extract $ oo <> (ValueAdded <$> dd)
where
extract :: OMap Text OverrideValue -> OMap Text Text
extract :: ConfigTree Text OverrideValue -> ConfigTree Text Text
extract =
fmap
( \case
ValueAdded v -> v
ValueDeleted -> error "invariant"
)
. OM.filter
( \_ -> \case
. CT.filter
( \case
ValueAdded _ -> True
ValueDeleted -> False
)
newtype Overrides' t (l :: OverrideLevel) = Overrides {unOverrides :: ConfigTree t (OverrideValue' t)}
deriving newtype (Eq, Ord, Show, NFData, Semigroup, Monoid)
deriving newtype instance FromJSON (Overrides' Text l)
deriving newtype instance ToJSON (Overrides' Text l)
instance Searchable t x => Searchable t (Overrides' x l) where
type SearchableConstraint t (Overrides' x l) res = (Ord (Searched x res), SearchableConstraint t x res)
type Searched (Overrides' x l) res = Overrides' (Searched x res) l
searchWith f (Overrides oMap) = do
oMap' <- searchWith f . OM.assocs $ oMap
pure $ Overrides $ OM.fromList oMap'
searchWith f (Overrides ct) = Overrides <$> searchWith f ct
{-# INLINE searchWith #-}
newtype Overrides' t (l :: OverrideLevel) = Overrides {unOverrides :: OMap t (OverrideValue' t)}
deriving newtype (Eq, Ord, Show, ToJSON, FromJSON, NFData)
type Overrides = Overrides' Text
extractOverrides :: DefaultConfig l -> Config l -> Overrides l
extractOverrides (DefaultConfig dCfg) (Config cfg) =
Overrides . OM.fromList $ removed <> present
Overrides . CT.fromList $ removed <> present
where
present :: [(Text, OverrideValue)]
present = mapMaybe processPresent . OM.assocs $ cfg
present :: [(NonEmpty Text, OverrideValue)]
present = mapMaybe processPresent . CT.toList $ cfg
processPresent :: (Text, Text) -> Maybe (Text, OverrideValue)
processPresent (k, v) = case OM.lookup k dCfg of
processPresent :: (NonEmpty Text, Text) -> Maybe (NonEmpty Text, OverrideValue)
processPresent (k, v) = case CT.lookup k dCfg of
Just v' | v == v' -> Nothing
_ -> Just (k, ValueAdded v)
processRemoved :: (Text, Text) -> Maybe (Text, OverrideValue)
processRemoved :: (NonEmpty Text, Text) -> Maybe (NonEmpty Text, OverrideValue)
processRemoved (k, _) =
if OM.member k cfg
if CT.member k cfg
then Nothing
else Just (k, ValueDeleted)
removed :: [(Text, OverrideValue)]
removed = mapMaybe processRemoved . OM.assocs $ dCfg
removed :: [(NonEmpty Text, OverrideValue)]
removed = mapMaybe processRemoved . CT.toList $ dCfg
ov :: Text -> OverrideValue -> Overrides l
ov k v = Overrides $ OM.singleton (k, v)
instance Semigroup (Overrides l) where
(Overrides lhs) <> (Overrides rhs) = Overrides $ rhs <>| lhs
instance Monoid (Overrides l) where
mempty = Overrides OM.empty
ov :: NonEmpty Text -> OverrideValue -> Overrides l
ov k v = Overrides $ CT.singleton k v
newtype DeploymentId = DeploymentId {unDeploymentId :: Int64}
deriving stock (Show)
@ -243,9 +238,11 @@ data Deployment' t = Deployment
, deploymentOverrides :: Overrides' t 'DeploymentLevel
}
deriving stock (Generic, Show, Eq)
deriving (FromJSON, ToJSON) via Snake (Deployment' t)
deriving anyclass (NFData)
deriving via Snake (Deployment' Text) instance FromJSON (Deployment' Text)
deriving via Snake (Deployment' Text) instance ToJSON (Deployment' Text)
instance (Searchable needle t) => Searchable needle (Deployment' t) where
type
SearchableConstraint needle (Deployment' t) res =
@ -317,9 +314,11 @@ data DeploymentFullInfo' t = DeploymentFullInfo
, updatedAt :: UTCTime
}
deriving stock (Generic, Show, Eq)
deriving (FromJSON, ToJSON) via Snake (DeploymentFullInfo' t)
deriving anyclass (NFData)
deriving via Snake (DeploymentFullInfo' Text) instance FromJSON (DeploymentFullInfo' Text)
deriving via Snake (DeploymentFullInfo' Text) instance ToJSON (DeploymentFullInfo' Text)
type DeploymentFullInfo = DeploymentFullInfo' Text
isDeploymentArchived :: DeploymentFullInfo' t -> Bool
@ -386,14 +385,14 @@ parseSetOverrides texts = do
Just x -> Right x
Nothing ->
Left $ "Malformed override key-value pair " <> text <> ", should be similar to FOO=bar"
return . Overrides $ OM.fromList pairs'
return . Overrides $ CT.fromList pairs'
where
parseSingleOverride :: Text -> Maybe (Text, OverrideValue)
parseSingleOverride :: Text -> Maybe (NonEmpty Text, OverrideValue)
parseSingleOverride t
| Just i <- T.findIndex (== '=') t =
let (key, value) = bimap T.strip (T.tail . T.strip) $ T.splitAt i t
let (CT.deconstructConfigKey -> key, value) = bimap T.strip (T.tail . T.strip) $ T.splitAt i t
in Just (key, ValueAdded value)
parseSingleOverride _ = Nothing
parseUnsetOverrides :: [Text] -> Overrides l
parseUnsetOverrides = Overrides . OM.fromList . fmap (,ValueDeleted)
parseUnsetOverrides = Overrides . CT.fromList . fmap ((,ValueDeleted) . CT.deconstructConfigKey)

View File

@ -2,11 +2,14 @@
module Control.Searchable
( Searchable (..),
traverseOMap,
)
where
import Control.Lens
import Data.Kind
import Data.Map.Ordered (OMap)
import qualified Data.Map.Ordered.Strict as OM
import Data.Text (Text)
-- | This says that you can search 'needle's in a 'haystack'. What precisely
@ -38,10 +41,26 @@ instance Searchable needle x => Searchable needle [x] where
{-# INLINE searchWith #-}
instance (Searchable needle a, Searchable needle b) => Searchable needle (a, b) where
type SearchableConstraint needle (a, b) res = (SearchableConstraint needle a res, SearchableConstraint needle b res)
type
SearchableConstraint needle (a, b) res =
(SearchableConstraint needle a res, SearchableConstraint needle b res)
type Searched (a, b) res = (Searched a res, Searched b res)
searchWith f (a, b) = do
a' <- searchWith f a
b' <- searchWith f b
pure (a', b')
{-# INLINE searchWith #-}
instance Searchable needle haystack => Searchable needle (Maybe haystack) where
type SearchableConstraint needle (Maybe haystack) res = SearchableConstraint needle haystack res
type Searched (Maybe haystack) res = Maybe (Searched haystack res)
searchWith _ Nothing = pure Nothing
searchWith f (Just h) = Just <$> searchWith f h
{-# INLINE searchWith #-}
traverseOMap ::
forall h k x y.
Ord k =>
(forall f. Applicative f => (h -> f k) -> (x -> f y) -> OMap h x -> f (OMap k y))
traverseOMap kf vf (OM.assocs -> l) =
fmap OM.fromList $ (\(h, x) -> (,) <$> kf h <*> vf x) `traverse` l

View File

@ -0,0 +1,165 @@
module Data.ConfigTree
( ConfigTree (..),
null,
filter,
toList,
singleton,
fromList,
lookup,
member,
deconstructConfigKey,
reconstructConfigKey,
toFlatList,
fromFlatList,
Data.ConfigTree.zip,
catMaybes,
insert,
markValues,
)
where
import Common.Orphans ()
import Control.Applicative
import Control.DeepSeq (NFData)
import Control.Searchable
import Data.Aeson
import qualified Data.Bifunctor as Bi
import Data.Foldable (fold)
import qualified Data.List as L
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
import Data.Map.Ordered.Strict (OMap)
import qualified Data.Map.Ordered.Strict as OM
import Data.Maybe (fromJust, fromMaybe, isJust)
import Data.String
import Data.Text (Text)
import qualified Data.Text as T
import Data.These
import GHC.Generics (Generic)
import Prelude hiding (filter, lookup, null)
zip :: Ord k => ConfigTree k v -> ConfigTree k h -> ConfigTree k (These v h)
zip (ConfigTree m) (ConfigTree w) =
let intersection =
OM.intersectionWith
( \_ (mv, vMap) (mh, hMap) ->
( case (mv, mh) of
(Nothing, Nothing) -> Nothing
(Just v, Nothing) -> Just $ This v
(Nothing, Just h) -> Just $ That h
(Just v, Just h) -> Just $ These v h
, Data.ConfigTree.zip vMap hMap
)
)
m
w
vCT = ConfigTree $ m OM.\\ intersection
hCT = ConfigTree $ w OM.\\ intersection
in fmap This vCT <> ConfigTree intersection <> fmap That hCT
newtype ConfigTree k v = ConfigTree (OMap k (Maybe v, ConfigTree k v))
deriving newtype (Eq, Ord, Show, NFData)
deriving stock (Functor, Generic)
instance ToJSON v => ToJSON (ConfigTree Text v) where
toJSON = toJSON . toFlatList
instance FromJSON v => FromJSON (ConfigTree Text v) where
parseJSON = fmap fromFlatList . parseJSON
markValues :: (IsString t, Monoid t, Ord t) => ConfigTree t c -> ConfigTree t (t, c)
markValues = fromList . fmap (\(k, v) -> (k, (reconstructConfigKey k, v))) . toList
{-# INLINE markValues #-}
toFlatList :: (IsString t, Monoid t) => ConfigTree t c -> [(t, c)]
toFlatList = fmap (Bi.first reconstructConfigKey) . toList
{-# INLINE toFlatList #-}
fromFlatList :: [(Text, v)] -> ConfigTree Text v
fromFlatList = fromList . fmap (Bi.first deconstructConfigKey)
{-# INLINE fromFlatList #-}
toList :: ConfigTree k v -> [(NonEmpty k, v)]
toList (ConfigTree m) =
OM.assocs m >>= \(k, (mv, s)) ->
maybe id (\v -> (:) (pure k, v)) mv $ Bi.first (NE.cons k) <$> toList s
{-# INLINE toList #-}
fromList :: Ord k => [(NonEmpty k, v)] -> ConfigTree k v
fromList = foldMap $ uncurry singleton
{-# INLINE fromList #-}
singleton :: Ord k => NonEmpty k -> v -> ConfigTree k v
singleton (NE k) v = ConfigTree $ OM.singleton (k, (Just v, mempty))
singleton (NECons k ks) v = ConfigTree $ OM.singleton (k, (Nothing, singleton ks v))
{-# INLINE singleton #-}
lookup :: Ord k => NonEmpty k -> ConfigTree k v -> Maybe v
lookup (NE k) (ConfigTree m) = OM.lookup k m >>= fst
lookup (NECons k ks) (ConfigTree m) = OM.lookup k m >>= lookup ks . snd
{-# INLINE lookup #-}
member :: Ord k => NonEmpty k -> ConfigTree k v -> Bool
member (NE k) (ConfigTree m) = OM.member k m
member (NECons k ks) (ConfigTree m) = maybe False (member ks . snd) $ OM.lookup k m
{-# INLINE member #-}
null :: ConfigTree l v -> Bool
null (ConfigTree m) = OM.null m
{-# INLINE null #-}
filter :: Ord k => (v -> Bool) -> ConfigTree k v -> ConfigTree k v
filter f (ConfigTree m) =
ConfigTree $
OM.filter (\_ (mv, ConfigTree m') -> isJust mv || not (OM.null m'))
. fmap (\(mv, ct) -> (mv >>= (\v -> if f v then Just v else Nothing), filter f ct))
$ m
{-# INLINE filter #-}
instance Ord k => Semigroup (ConfigTree k v) where
(ConfigTree lMap) <> (ConfigTree rMap) =
ConfigTree $ OM.unionWithR (\_ (lvm, lm) (rvm, rm) -> (lvm <|> rvm, lm <> rm)) lMap rMap
{-# INLINE (<>) #-}
instance Ord k => Monoid (ConfigTree k v) where
mempty = ConfigTree OM.empty
instance (Searchable needle k, Searchable needle v) => Searchable needle (ConfigTree k v) where
type
SearchableConstraint needle (ConfigTree k v) res =
(SearchableConstraint needle k res, SearchableConstraint needle v res, Ord (Searched k res))
type Searched (ConfigTree k v) res = ConfigTree (Searched k res) (Searched v res)
searchWith f (ConfigTree oMap) = do
oMap' <- traverseOMap (searchWith f) (searchWith f) oMap
pure $ ConfigTree oMap'
{-# INLINE searchWith #-}
deconstructConfigKey :: Text -> NonEmpty Text
deconstructConfigKey =
fromMaybe (pure "") . NE.nonEmpty . L.filter (not . T.null) . fmap T.strip . T.splitOn "."
{-# INLINE deconstructConfigKey #-}
reconstructConfigKey :: (IsString t, Monoid t) => NonEmpty t -> t
reconstructConfigKey = fold . NE.intersperse "."
{-# INLINE reconstructConfigKey #-}
pattern NE :: a -> NonEmpty a
pattern NE x = x NE.:| []
pattern NECons :: a -> NonEmpty a -> NonEmpty a
pattern NECons x y <- (NE.uncons -> (x, Just y))
{-# COMPLETE NE, NECons #-}
catMaybes :: Ord k => ConfigTree k (Maybe v) -> ConfigTree k v
catMaybes = fmap fromJust . filter isJust
{-# INLINE catMaybes #-}
insert :: Ord k => NonEmpty k -> v -> ConfigTree k v -> ConfigTree k v
insert (NE k) v (ConfigTree m) = ConfigTree $ (k, (Just v, subCfg)) OM.<| m
where
subCfg = case OM.lookup k m of
Nothing -> mempty
Just (_, m') -> m'
insert (NECons k ks) v (ConfigTree m) = ConfigTree $ case OM.lookup k m of
Nothing -> (k, (Nothing, singleton ks v)) OM.<| m
Just (v', m') -> (k, (v', insert ks v m')) OM.<| m
{-# INLINE insert #-}

View File

@ -1,15 +0,0 @@
{-# OPTIONS_GHC -Wno-orphans #-}
module Data.Map.Ordered.Strict.Extra
(
)
where
import Data.Aeson
import Data.Map.Ordered.Strict
instance (ToJSON k, ToJSON v) => ToJSON (OMap k v) where
toJSON = toJSON . assocs
instance (FromJSON k, FromJSON v, Ord k) => FromJSON (OMap k v) where
parseJSON = fmap fromList . parseJSON

View File

@ -9,6 +9,12 @@
font-family: inherit;
text-align: left;
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.collapse__body {
@ -87,7 +93,3 @@
padding-left: 32px;
border-left: 1px solid #C4C4C4;
}

View File

@ -61,7 +61,7 @@
}
.table--clickable tbody tr {
cursor: default;
cursor: pointer;
}
.table--clickable tbody tr:hover {

View File

@ -52,6 +52,7 @@ executable frontend
, Frontend.UIKit.Button.Static
, Frontend.UIKit.Button.Action
, Frontend.UIKit.Button.Sort
, Frontend.UIKit.Button.Tree
, Reflex.Dom.Renderable
, Reflex.Dom.AsyncEvent
ghc-options:
@ -103,6 +104,7 @@ executable frontend
, ApplicativeDo
, PartialTypeSignatures
, DeriveAnyClass
, MultiWayIf
build-depends: aeson
, base
, bytestring
@ -135,5 +137,7 @@ executable frontend
, deepseq
, patch
, reflex-dom-core
, these
, ref-tf
hs-source-dirs: src
default-language: Haskell2010

View File

@ -21,6 +21,7 @@ import Data.Maybe (listToMaybe)
import Data.Ord
import Data.Semigroup
import Data.Sequence (Seq (..))
import Data.String
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)
@ -92,6 +93,19 @@ data SearchResult = SearchResult
deriving stock (Eq, Ord, Generic)
deriving anyclass (NFData)
instance IsString SearchResult where
fromString s = SearchResult (T.pack s) Nothing
instance Semigroup SearchResult where
SearchResult a aRes <> SearchResult b bRes = SearchResult (a <> b) $ case (aRes, bRes) of
(Nothing, Nothing) -> Nothing
(Just a', Nothing) -> Just $ a' :|> NotMatched b
(Nothing, Just b') -> Just $ NotMatched a :<| b'
(Just a', Just b') -> Just $ a' <> b'
instance Monoid SearchResult where
mempty = SearchResult mempty Nothing
instance Searchable SearchResult SearchResult where
type Searched SearchResult res = res
searchWith f t = f t

View File

@ -1,32 +1,94 @@
module Data.WorkingOverrides
( WorkingOverrides,
WorkingOverride,
WorkingOverride',
WorkingOverrideKey' (..),
WorkingOverrideKey,
WorkingOverrideKeyType (..),
WorkingOverrideValue' (..),
WorkingOverrideValue,
destructWorkingOverrides,
constructWorkingOverrides,
newWorkingOverride,
ConfigValue (..),
CustomKey (..),
CustomConfigValue (..),
WorkingConfigTree,
WorkingOverride,
WorkingOverride',
configTreeHasLeaf,
destructWorkingOverridesDyn,
)
where
import Common.Orphans ()
import Common.Types
import qualified Data.List as L
import qualified Data.Map.Ordered.Strict as OM
import Data.Maybe
import Control.Monad.Reader
import Control.Monad.Ref
import Data.ConfigTree (ConfigTree)
import qualified Data.ConfigTree as CT
import Data.Foldable
import Data.Functor
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (catMaybes)
import qualified Data.Maybe as M
import Data.Text (Text)
import Data.UniqMap
import Data.These
import GHC.Generics (Generic)
import Reflex
type WorkingConfigTree k v = ConfigTree k (ConfigValue v)
data ConfigValue te
= DefaultConfigValue !te
| CustomConfigValue !(Either (CustomKey te) (CustomConfigValue te))
deriving stock (Show, Generic, Eq, Ord)
newtype CustomKey v = CustomKey v
deriving stock (Show, Generic, Eq, Ord)
data CustomConfigValue te
= CustomValue !te
| DeletedValue !(Maybe te)
deriving stock (Show, Generic, Eq, Ord)
-- Super inefficient but i dont care
configTreeHasLeaf ::
forall m kv te.
( MonadReader (Ref m (Map (ConfigTree kv te) Bool)) m
, MonadRef m
, Ord kv
, Ord te
) =>
(te -> Bool) ->
ConfigTree kv te ->
m Bool
configTreeHasLeaf f = memo $ \(CT.ConfigTree x) -> go $ toList x
where
go :: [(Maybe te, ConfigTree kv te)] -> m Bool
go [] = pure False
go ((Just y, _) : _) | f y = pure True
go ((_, ct) : rest) =
configTreeHasLeaf f ct >>= \case
True -> pure True
False -> go rest
memo ::
(MonadReader (Ref m (Map x y)) m, Ord x, MonadRef m) =>
(x -> m y) ->
x ->
m y
memo f x = do
memoRef <- ask
m <- readRef memoRef
case M.lookup x m of
Just y -> pure y
Nothing -> do
y <- f x
modifyRef memoRef $ M.insert x y
pure y
type WorkingOverrides = WorkingOverrides' Text
type WorkingOverrides' te = UniqKeyMap (WorkingOverride' te)
type WorkingOverrides' te = ConfigTree te (ConfigValue te)
type WorkingOverride = WorkingOverride' Text
type WorkingOverride' te = (WorkingOverrideKey' te, WorkingOverrideValue' te)
type WorkingOverride' te = (te, ConfigValue te)
type WorkingOverrideKey = WorkingOverrideKey' Text
@ -36,75 +98,31 @@ data WorkingOverrideKey' te = WorkingOverrideKey !WorkingOverrideKeyType !te
data WorkingOverrideKeyType = CustomWorkingOverrideKey | DefaultWorkingOverrideKey
deriving stock (Show, Eq)
type WorkingOverrideValue = WorkingOverrideValue' Text
destructWorkingOverridesDyn :: Reflex t => [Dynamic t WorkingOverride] -> Dynamic t (Overrides l)
destructWorkingOverridesDyn =
fmap (Overrides . CT.fromFlatList . catMaybes) . sequenceA . (fmap . fmap) (\(k, v) -> (k,) <$> unConfigValue v)
data WorkingOverrideValue' te
= WorkingCustomValue !te
| WorkingDefaultValue !te
| WorkingDeletedValue !(Maybe te)
deriving stock (Show)
destructWorkingOverrides :: WorkingOverrides -> Overrides l
destructWorkingOverrides :: [WorkingOverride] -> Overrides l
destructWorkingOverrides =
Overrides
. foldr
( \pair@(k, v) m -> case OM.lookup k m of
Just _ | v == ValueDeleted -> m
_ -> m OM.|> pair
)
OM.empty
. mapMaybe
( \case
(WorkingOverrideKey CustomWorkingOverrideKey k, getWorkingOverrideValue -> v) -> Just (k, v)
(WorkingOverrideKey _ k, WorkingCustomValue v) -> Just (k, ValueAdded v)
(WorkingOverrideKey _ k, WorkingDeletedValue _) -> Just (k, ValueDeleted)
(WorkingOverrideKey DefaultWorkingOverrideKey _, WorkingDefaultValue _) -> Nothing
)
. elemsUniq
where
getWorkingOverrideValue :: WorkingOverrideValue -> OverrideValue
getWorkingOverrideValue (WorkingCustomValue x) = ValueAdded x
getWorkingOverrideValue (WorkingDefaultValue x) = ValueAdded x
getWorkingOverrideValue (WorkingDeletedValue _) = ValueDeleted
Overrides . CT.fromFlatList . M.catMaybes . fmap (\(k, v) -> (k,) <$> unConfigValue v)
unConfigValue :: ConfigValue te -> Maybe (OverrideValue' te)
unConfigValue (DefaultConfigValue _) = Nothing
unConfigValue (CustomConfigValue (Left (CustomKey te))) = Just $ ValueAdded te
unConfigValue (CustomConfigValue (Right (CustomValue te))) = Just $ ValueAdded te
unConfigValue (CustomConfigValue (Right (DeletedValue _))) = Just ValueDeleted
constructWorkingOverrides ::
Ord te =>
Maybe (DefaultConfig' te l) ->
DefaultConfig' te l ->
Overrides' te l ->
WorkingOverrides' te
constructWorkingOverrides (Just (DefaultConfig dCfg)) (Overrides ovsM) =
let custom =
uniqMapFromList
. mapMaybe
( \(k, v) ->
let k' = WorkingOverrideKey (if OM.member k dCfg then DefaultWorkingOverrideKey else CustomWorkingOverrideKey) k
in case v of
ValueAdded x -> Just (k', WorkingCustomValue x)
ValueDeleted -> (k',) . WorkingDeletedValue . Just <$> OM.lookup k dCfg
)
. OM.assocs
$ ovsM
in L.foldl'
( \m (k, v) ->
if OM.member k ovsM
then m
else
fst $
insertUniqEnd (WorkingOverrideKey DefaultWorkingOverrideKey k, WorkingDefaultValue v) m
)
custom
(OM.assocs dCfg)
constructWorkingOverrides Nothing (Overrides ovsM) =
uniqMapFromList
. fmap
( \(k, v) ->
let k' = WorkingOverrideKey CustomWorkingOverrideKey k
in case v of
ValueAdded x -> (k', WorkingCustomValue x)
ValueDeleted -> (k', WorkingDeletedValue Nothing)
)
. OM.assocs
$ ovsM
newWorkingOverride :: WorkingOverride
newWorkingOverride = (WorkingOverrideKey CustomWorkingOverrideKey "", WorkingCustomValue "")
constructWorkingOverrides (DefaultConfig defCT) (Overrides newCT) =
CT.catMaybes $
CT.zip newCT defCT <&> \case
That x -> Just $ DefaultConfigValue x
This x -> case x of
ValueDeleted -> Nothing
ValueAdded v -> Just $ CustomConfigValue $ Left $ CustomKey v
These ValueDeleted v -> Just $ CustomConfigValue $ Right $ DeletedValue $ Just v
These (ValueAdded v) _ -> Just $ CustomConfigValue $ Right $ CustomValue v

View File

@ -7,53 +7,63 @@ module Frontend.UIKit
octopodTextInputDyn,
loadingOverride,
loadingOverrides,
overrideField,
OverrideField (..),
configField,
popupOverlay,
Default (..),
module X,
(.~~),
(?~~),
OverrideFieldType (..),
deletedOverride,
showNonEditableWorkingOverride,
NonEditableWorkingOverrideStyle (..),
untilReadyEv',
untilReady',
runWithReplace',
joinEvM,
showNonEditableWorkingOverrideTree,
showFlatConfig,
ConfigFieldButtonAction (..),
showOverrideTree,
nonEditableLoading,
)
where
import Common.Types
import Control.Lens
( ASetter',
(%~),
(<>~),
(?~),
(^.),
)
import Control.Lens.Extras (is)
import Control.Monad
import Control.Monad.Reader
import Control.Monad.Ref
import Data.Align
import Data.ConfigTree (ConfigTree (ConfigTree))
import qualified Data.ConfigTree as CT
import Data.Default
import Data.Either
import Data.Foldable
import Data.Functor
import Data.Generics.Labels ()
import Data.Generics.Sum
import Data.List.NonEmpty
import qualified Data.List.NonEmpty as NE
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (isNothing, maybeToList)
import qualified Data.Map.Ordered.Strict as OM
import Data.Maybe (fromMaybe, isNothing, maybeToList)
import Data.Monoid (Endo (..))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Search
import Data.These
import Data.Traversable
import Data.Witherable
import Data.WorkingOverrides
import Frontend.Classes as X
import Frontend.UIKit.Button.Action as X
import Frontend.UIKit.Button.Common
import Frontend.UIKit.Button.Dash as X
import Frontend.UIKit.Button.Expander as X
import Frontend.UIKit.Button.Large as X
import Frontend.UIKit.Button.Sort as X
import Frontend.UIKit.Button.Static as X
import Frontend.UIKit.Button.Tree
import GHC.Generics (Generic)
import Reflex.Dom
import Reflex.Dom.AsyncEvent
@ -85,64 +95,84 @@ errorCommonWidget =
elClass "b" "null__heading" $ text "Cannot retrieve the data"
divClass "null__message" $ text "Try to reload the page"
data OverrideField t = OverrideField
data ConfigField t = ConfigField
{ fieldValue :: Dynamic t Text
, fieldError :: Dynamic t (Maybe (NonEmpty Text))
, fieldDisabled :: Dynamic t Bool
, fieldType :: Dynamic t OverrideFieldType
, fieldType :: Dynamic t ConfigFieldType
}
deriving stock (Generic)
data OverrideFieldType
= DefaultOverrideFieldType
| EditedOverrideFieldType
data ConfigFieldType
= DefaultConfigFieldType
| CustomConfigFieldType
deriving stock (Show)
overrideFieldTypeClasses :: OverrideFieldType -> Classes
overrideFieldTypeClasses DefaultOverrideFieldType = "input--default"
overrideFieldTypeClasses EditedOverrideFieldType = mempty
overrideField :: MonadWidget t m => Dynamic t [Text] -> OverrideField t -> OverrideField t -> m (Dynamic t Text, Dynamic t Text, Event t ())
overrideField overrideKeyValues keyDyn valueDyn = do
elDiv "overrides__item" $ do
keyInp <-
configField ::
MonadWidget t m =>
Dynamic t [Text] ->
Dynamic t WorkingOverride ->
m (Dynamic t Text, Dynamic t Text, Event t ConfigFieldButtonAction)
configField overrideKeyValues (splitDynPure -> (k, v')) = do
let (splitDynPure -> (keyClass, valueClass)) =
v' <&> \case
DefaultConfigValue _ -> ("key-default-pristine", "value-pristine")
CustomConfigValue (Right (CustomValue _)) -> ("key-default-edited", "value-edited")
CustomConfigValue (Right (DeletedValue Just {})) -> ("key-deleted", "value-deleted")
CustomConfigValue (Right (DeletedValue Nothing)) -> ("key-deleted", "value-unknown")
CustomConfigValue (Left _) -> ("key-custom-edited", "value-edited")
valDyn =
v' <&> \case
DefaultConfigValue v -> v
CustomConfigValue (Right (CustomValue v)) -> v
CustomConfigValue (Right (DeletedValue v)) -> fromMaybe "UNKNOWN" v
CustomConfigValue (Left (CustomKey v)) -> v
deletedDyn' = (is (_Ctor' @"CustomConfigValue" . _Right . _Ctor' @"DeletedValue")) <$> v'
deletedDyn <- holdUniqDyn deletedDyn'
let extraDivClasses =
deletedDyn <&> \case
True -> "input--deleted"
False -> mempty
elDiv "editable-row" $ mdo
(value -> keyTextDyn) <-
octopodTextInputDyn
overrideKeyValues
(keyDyn ^. #fieldDisabled)
( do
t <- keyDyn ^. #fieldType
pure $ "overrides__key" <> overrideFieldTypeClasses t
)
deletedDyn
(mappend "editable-row__key" <$> extraDivClasses)
keyClass
"key"
(keyDyn ^. #fieldValue)
(keyDyn ^. #fieldError)
let keyTextDyn = value keyInp
k
(pure Nothing)
(value -> valTextDyn) <-
octopodTextInputDyn
(pure [])
(valueDyn ^. #fieldDisabled)
( do
t <- valueDyn ^. #fieldType
pure $ "overrides__value" <> overrideFieldTypeClasses t
)
deletedDyn
(mappend "editable-row__value" <$> extraDivClasses)
valueClass
"value"
(valueDyn ^. #fieldValue)
(valueDyn ^. #fieldError)
closeEv <- deleteOverrideButton
valDyn
(valTextDyn <&> \t -> if (T.null . T.strip) t then Just (pure "Value can not be empty") else Nothing)
closeEv <-
networkView >=> switchHold never $
deletedDyn <&> \case
True -> fmap (const ConfigFieldButtonRecover) <$> undoOverrideButton
False -> fmap (const ConfigFieldButtonClear) <$> deleteOverrideButton
pure (keyTextDyn, valTextDyn, closeEv)
data ConfigFieldButtonAction = ConfigFieldButtonClear | ConfigFieldButtonRecover
deriving stock (Show)
deletedOverride :: MonadWidget t m => Text -> Maybe Text -> m (Event t ())
deletedOverride k vM = do
elDiv "overrides__item" $ do
elDiv "overrides__key input input--deleted" $ field k
elDiv "editable-row" $ do
elDiv "editable-row__key input input--deleted" $ field k
case vM of
Nothing -> do
elDiv "overrides__placeholder overrides__value" blank
elDiv "editable-row__placeholder editable-row__value" blank
loadingOverrideSpinner
pure never
Just v -> do
elDiv "overrides__value input input--deleted" $ field $ v
undoOverrideButton
elDiv "editable-row__value input input--deleted" $ field $ v
(($> ()) <$> undoOverrideButton)
where
field t =
void $
@ -152,20 +182,20 @@ deletedOverride k vM = do
& initialAttributes .~ ("type" =: "text" <> "class" =: "input__widget")
loadingOverrideField :: MonadWidget t m => m ()
loadingOverrideField = elDiv "overrides__placeholder" blank
loadingOverrideField = elDiv "editable-row__placeholder" blank
loadingOverrideSpinner :: MonadWidget t m => m ()
loadingOverrideSpinner = elDiv "overrides__delete spot spot--loader" blank
loadingOverride :: MonadWidget t m => m ()
loadingOverride = do
elDiv "overrides__item loader" $ do
elDiv "editable-row loader" $ do
loadingOverrideField
loadingOverrideField
loadingOverrideSpinner
loadingOverrides :: MonadWidget t m => m ()
loadingOverrides = do
loadingOverrides = divClass "padded" $ do
loadingOverride
loadingOverride
loadingOverride
@ -181,6 +211,8 @@ octopodTextInput' ::
(Dynamic t [Text]) ->
-- | Disabled?
Dynamic t Bool ->
-- | Div classes.
Dynamic t Classes ->
-- | Input field classes.
Dynamic t Classes ->
-- | Placeholder for input field.
@ -190,14 +222,14 @@ octopodTextInput' ::
-- | Event carrying the error message.
Event t Text ->
m (InputElement EventResult GhcjsDomSpace t, Dynamic t Bool)
octopodTextInput' valuesDyn disabledDyn clssDyn placeholder inValDyn' errEv = mdo
octopodTextInput' valuesDyn disabledDyn inpClassesDyn clssDyn placeholder inValDyn' errEv = mdo
errDyn <-
holdDyn Nothing $
leftmost
[ Just . pure <$> errEv
, Nothing <$ updated (value inp)
]
inp <- octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn
inp <- octopodTextInputDyn valuesDyn disabledDyn inpClassesDyn clssDyn placeholder inValDyn' errDyn
pure (inp, isNothing <$> errDyn)
-- | The only text input field that is used in project forms. This input
@ -208,6 +240,8 @@ octopodTextInputDyn ::
Dynamic t [Text] ->
-- | Disabled?
Dynamic t Bool ->
-- | Div classes.
Dynamic t Classes ->
-- | Input field classes.
Dynamic t Classes ->
-- | Placeholder for input field.
@ -217,7 +251,7 @@ octopodTextInputDyn ::
-- | Event carrying the error message.
Dynamic t (Maybe (NonEmpty Text)) ->
m (InputElement EventResult GhcjsDomSpace t)
octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn = mdo
octopodTextInputDyn valuesDyn disabledDyn divClasses inpClassesDyn placeholder inValDyn' errDyn = mdo
inValDyn <- holdUniqDyn inValDyn'
let valDyn = value inp
inValEv =
@ -237,18 +271,22 @@ octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn =
let classDyn = do
errClasses <- errorClassesDyn
additionalClasses <- clssDyn
additionalClasses <- divClasses
pure . destructClasses $ "input" <> errClasses <> additionalClasses
inVal <- sample . current $ inValDyn
disabled <- sample . current $ disabledDyn
let actualInputClassesDyn = do
inpClass <- inpClassesDyn
pure $ destructClasses $ "input__widget" <> inpClass
clss <- sample . current $ actualInputClassesDyn
(inp, selectedValue) <- elDynClass "div" classDyn $ do
inp' <-
inputElement $
def
& initialAttributes
.~ ( "type" =: "text"
<> "class" =: "input__widget"
<> "class" =: clss
<> "placeholder" =: placeholder
<> "spellcheck" =: "false"
)
@ -260,9 +298,11 @@ octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn =
<>~ updated
( do
disabled' <- disabledDyn
classes <- actualInputClassesDyn
pure $
M.singleton "disabled" $
if disabled' then Just "disabled" else Nothing
( "disabled" =: (if disabled' then Just "disabled" else Nothing)
<> "class" =: Just classes
)
)
void $
simpleList ((maybeToList >=> NE.toList) <$> errDyn) $ \err ->
@ -288,14 +328,14 @@ octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn =
searchResultEv <&> \case
[] -> pure never
searchResult -> do
elClass "ul" "overrides__search" $ do
elClass "ul" "input__dropdown" $ do
fmap leftmost $
forM searchResult $ \(res, initialText) -> do
(resEl, ()) <- elClass' "li" "overrides__search-item" $
forM_ res $ \case
for searchResult $ \(res, initialText) -> do
(resEl, ()) <- elClass' "li" "input__suggest" $
for_ res $ \case
Matched t -> elAttr "span" ("style" =: "font-weight: bold;") $ text t
NotMatched t -> text t
-- Because 'Click' would fire after the text field loses
-- Because 'Click' would fire after the text field looses
-- focus and the popup disappears.
pure $ domEvent Mousedown resEl $> initialText
@ -307,60 +347,179 @@ popupOverlay :: DomBuilder t m => m ()
popupOverlay =
elAttr "div" ("class" =: "popup__overlay" <> "aria-hidden" =: "true") blank
data NonEditableWorkingOverrideStyle
= RegularNonEditableWorkingOverrideStyle
| LargeNonEditableWorkingOverrideStyle
nonEditableWorkingOverrideStyleClasses :: NonEditableWorkingOverrideStyle -> Classes
nonEditableWorkingOverrideStyleClasses RegularNonEditableWorkingOverrideStyle = mempty
nonEditableWorkingOverrideStyleClasses LargeNonEditableWorkingOverrideStyle = "listing--larger"
-- | Widget that shows overrides list. It does not depend on their type.
showNonEditableWorkingOverride ::
(MonadWidget t m, Renderable te) =>
-- | Loading?
Bool ->
-- | Is it fully loaded?
Bool ->
NonEditableWorkingOverrideStyle ->
-- | Overrides list.
[WorkingOverride' te] ->
showFlatConfig ::
(MonadWidget t m, Renderable tk, Renderable tv) =>
[(tk, OverrideValue' tv)] ->
m ()
showNonEditableWorkingOverride loading loaded style cfg =
divClass
( destructClasses $
"listing" <> "listing--for-text" <> nonEditableWorkingOverrideStyleClasses style
)
$ do
case cfg of
[] ->
divClass "listing__item" $
elClass "span" "listing--info-text" $
text $
if loaded then "no configuration" else "no custom configuration"
_ -> forM_ cfg $ \(WorkingOverrideKey keyType key, val) -> do
let wrapper = case val of
WorkingDeletedValue _ -> divClass "listing__item deleted"
_ -> divClass "listing__item"
wrapper $ do
let keyWrapper = case keyType of
CustomWorkingOverrideKey -> elClass "span" "listing__key"
DefaultWorkingOverrideKey -> elClass "span" "listing__key default"
keyWrapper $ do
rndr key
text ": "
showFlatConfig l =
for_ l $ \(k, v) ->
let keyClasses = if is (_Ctor' @"ValueDeleted") v then "key-deleted" else "key-default-pristine"
in divClass "row" $ do
elClass "span" (destructClasses keyClasses) $ do
rndr k
text ": "
pure ()
case v of
ValueAdded t -> elClass "span" "value-edited" $ rndr t
ValueDeleted -> elClass "div" "listing__placeholder listing__placeholder__value" $ pure ()
pure ()
case val of
WorkingCustomValue txt -> elClass "span" "listing__value" $ rndr txt
WorkingDefaultValue txt -> elClass "span" "listing__value default" $ rndr txt
WorkingDeletedValue (Just txt) -> elClass "span" "listing__value default" $ rndr txt
WorkingDeletedValue Nothing -> do
elClass "div" "listing__placeholder listing__placeholder__value" $ pure ()
when loading $ elClass "div" "listing__spinner" $ pure ()
when loading $
divClass "listing__item" $ do
elClass "div" "listing__placeholder" $ pure ()
elClass "div" "listing__spinner" $ pure ()
showNonEditableWorkingOverrideTree ::
(MonadWidget t m, Renderable tk, Renderable tv, Ord tk, Ord (ConfigValue tv)) =>
Dynamic t (WorkingConfigTree tk tv) ->
m ()
showNonEditableWorkingOverrideTree treeDyn = do
memoRef <- newRef mempty
flip runReaderT memoRef $
untilReady (lift nonEditableLoading) $
networkView $
treeDyn <&> \tree ->
untilReady (lift nonEditableLoading) $
divClass "padded" $
showWorkingOverrideTree'
(is (_Ctor' @"CustomConfigValue"))
(\_ _ -> ())
(\_ k v -> renderRow k v $> pure ())
tree
(pure True)
never
pure ()
nonEditableLoading :: DomBuilder t m => m ()
nonEditableLoading = for_ [1 :: Int .. 3] $ \_ ->
divClass "padded" $
divClass "row" $ do
elClass "div" "listing__placeholder" $ pure ()
elClass "div" "listing__spinner" $ pure ()
showOverrideTree ::
forall m t tv tk x.
( MonadWidget t m
, Renderable tk
, Show x
, Ord tk
, Ord tv
) =>
-- | Value is modified
(tv -> Bool) ->
-- | Prepare the fixpoint data
(tk -> tv -> x) ->
-- | Render row
(x -> tk -> tv -> m (Dynamic t x)) ->
ConfigTree tk tv ->
-- | Should we render anything
m [Dynamic t x]
showOverrideTree isModified prepareData renderValue ct = do
memoRef <- newRef mempty
flip runReaderT memoRef $
showWorkingOverrideTree' isModified prepareData ((fmap . fmap . fmap) lift renderValue) ct (pure True) never
showWorkingOverrideTree' ::
( MonadWidget t m
, Renderable tk
, Show x
, MonadReader (Ref m (Map (ConfigTree tk tv) Bool)) m
, Ord tv
, Ord tk
) =>
-- | Value is modified
(tv -> Bool) ->
-- | Prepare the fixpoint data
(tk -> tv -> x) ->
-- | Render row
(x -> tk -> tv -> m (Dynamic t x)) ->
ConfigTree tk tv ->
-- | Parent is open
Dynamic t Bool ->
-- | Force open/close all subtrees
Event t Bool ->
m [Dynamic t x]
showWorkingOverrideTree' isModified prepareData renderValue (ConfigTree m) shouldRenderDyn' forceSubtreesEv = do
shouldRenderDyn <- holdUniqDyn shouldRenderDyn'
(appEndo . fold -> f) <- for (OM.assocs m) $ \(k, (mv, _)) -> do
xMaybe <- for mv $ \v -> mdo
let initialData = prepareData k v
x <-
networkView $
shouldRenderDyn <&> \render -> do
lastX <- sample $ current x'
if render
then elClass "div" "row" $ renderValue lastX k v
else pure (pure lastX)
x' <- (join <$> (holdDyn (pure initialData) x))
pure $ x'
pure $ Endo $ maybe id (:) xMaybe
fmap (f . concat) $
for (OM.assocs m) $ \case
(_, (_, subtree)) | CT.null subtree -> pure []
(k, (_, subtree)) -> mdo
let wrapperClass = do
open <- openDyn
pure $
destructClasses $
"collapse--project" <> "collapse"
<> if open
then "collapse--expanded"
else mempty
(ovs, openDyn) <- elDynClass "div" wrapperClass $ do
modified <- configTreeHasLeaf isModified subtree
(traceEvent "clickEv" -> clickEv) <-
treeButton
TreeButtonConfig
{ buttonText = TextBuilder $ rndr k
, subtreeHasChanges = modified
, visible = shouldRenderDyn
, forceState = forceSubtreesEv
}
buttonIsOpen <- holdDyn False $ leftmost [forceSubtreesEv, fst <$> clickEv]
let selfForceSubtrees = fst <$> ffilter (isRight . snd) clickEv
ovs' <-
divClass "collapse__inner" $
showWorkingOverrideTree'
isModified
prepareData
renderValue
subtree
((&&) <$> buttonIsOpen <*> shouldRenderDyn)
(leftmost [selfForceSubtrees, forceSubtreesEv])
pure (ovs', buttonIsOpen)
pure ovs
renderRow ::
(DomBuilder t m, Renderable te, Renderable k) =>
k ->
ConfigValue te ->
m ()
renderRow k v = do
case v of
DefaultConfigValue v' -> do
elClass "span" "key-default-pristine" $ do
rndr k
text ": "
pure ()
elClass "span" "value-pristine" $ rndr v'
pure ()
CustomConfigValue (Left (CustomKey v')) -> do
elClass "span" "key-custom-edited" $ do
rndr k
text ": "
pure ()
elClass "span" "value-edited" $ rndr v'
pure ()
CustomConfigValue (Right (CustomValue v')) -> do
elClass "span" "key-default-edited" $ do
rndr k
text ": "
pure ()
elClass "span" "value-edited" $ rndr v'
pure ()
CustomConfigValue (Right (DeletedValue mv)) -> do
elClass "span" "key-deleted" $ do
rndr k
text ": "
pure ()
elClass "span" "value-deleted" $ rndr `traverse_` mv
pure ()
untilReadyEv' ::
(Adjustable t m, PostBuild t m, MonadHold t m) =>

View File

@ -47,7 +47,7 @@ buttonTypeClasses = \case
actionButton ::
(DomBuilder t m, PostBuild t m) =>
ActionButtonConfig t ->
m (Event t ())
m (Event t (Either () ()))
actionButton cfg =
buttonEl
CommonButtonConfig
@ -58,6 +58,6 @@ actionButton cfg =
, enabledClasses = mempty
, disabledClasses = "action--disabled"
, buttonEnabled = cfg ^. #buttonEnabled
, buttonText = pure $ cfg ^. #buttonText
, buttonText = textBuilder $ cfg ^. #buttonText
, buttonBaseTag = cfg ^. #buttonBaseTag
}

View File

@ -2,23 +2,43 @@ module Frontend.UIKit.Button.Common
( CommonButtonConfig (..),
BaseButtonTag (..),
buttonEl,
TextBuilder (..),
textBuilder,
)
where
import Control.Lens
import Control.Lens hiding (element)
import Data.Generics.Labels ()
import Data.Map (Map)
import Data.Proxy
import Data.String
import Data.Text (Text)
import qualified Data.Text as T
import Frontend.Classes
import GHC.Generics (Generic)
import Reflex.Dom
textBuilder :: Text -> TextBuilder t
textBuilder t = TextBuilder $ text t
newtype TextBuilder t = TextBuilder
{ unTextBuilder ::
forall m.
( DomBuilder t m
, PostBuild t m
) =>
m ()
}
instance IsString (TextBuilder t) where
fromString t = TextBuilder (text . T.pack $ t)
data CommonButtonConfig t = CommonButtonConfig
{ constantClasses :: Dynamic t Classes
, enabledClasses :: Classes
, disabledClasses :: Classes
, buttonEnabled :: Dynamic t Bool
, buttonText :: Dynamic t Text
, buttonText :: TextBuilder t
, buttonBaseTag :: BaseButtonTag
}
deriving stock (Generic)
@ -30,9 +50,10 @@ baseTag ButtonTag = ("button", "type" =: "button")
baseTag (ATag url) = ("a", "href" =: url <> "target" =: "_blank")
buttonEl ::
forall m t.
(DomBuilder t m, PostBuild t m) =>
CommonButtonConfig t ->
m (Event t ())
m (Event t (Either () ()))
buttonEl cfg = do
let (t, staticAttrs) = baseTag (cfg ^. #buttonBaseTag)
attrsDyn = do
@ -45,5 +66,32 @@ buttonEl cfg = do
staticAttrs
<> "class" =: destructClasses (enabledClasses <> cs)
<> enabledAttrs
(bEl, _) <- elDynAttr' t attrsDyn $ dynText $ cfg ^. #buttonText
pure $ domEvent Click bEl
modAttrs <- dynamicAttributesToModifyAttributes attrsDyn
let elCfg =
(def @(ElementConfig EventResult t (DomBuilderSpace m)))
& elementConfig_modifyAttributes .~ fmapCheap mapKeysToAttributeName modAttrs
& elementConfig_eventSpec
%~~ addEventSpecFlags
(Proxy @(DomBuilderSpace m))
Contextmenu
( const
EventFlags
{ _eventFlags_propagation = Propagation_StopImmediate
, _eventFlags_preventDefault = True
}
)
& elementConfig_eventSpec
%~~ addEventSpecFlags
(Proxy @(DomBuilderSpace m))
Click
( const
EventFlags
{ _eventFlags_propagation = Propagation_StopImmediate
, _eventFlags_preventDefault = True
}
)
(bEl, _) <- element t elCfg (unTextBuilder (cfg ^. #buttonText))
pure $ leftmost [Left <$> domEvent Click bEl, Right <$> domEvent Contextmenu bEl]
(%~~) :: ASetter' s a -> (a -> a) -> s -> s
(%~~) = (%~)

View File

@ -54,7 +54,7 @@ buttonTypeClasses = \case
dashButton ::
(DomBuilder t m, PostBuild t m) =>
DashButtonConfig t ->
m (Event t ())
m (Event t (Either () ()))
dashButton cfg =
buttonEl
CommonButtonConfig
@ -66,6 +66,6 @@ dashButton cfg =
, enabledClasses = mempty
, disabledClasses = "dash--disabled"
, buttonEnabled = cfg ^. #buttonEnabled
, buttonText = pure $ cfg ^. #buttonText
, buttonText = textBuilder $ cfg ^. #buttonText
, buttonBaseTag = ButtonTag
}

View File

@ -10,6 +10,7 @@ where
import Control.Lens
import Control.Monad.Fix
import Data.Default
import Data.Functor
import Data.Generics.Labels ()
import Data.Text (Text)
import Frontend.Classes
@ -65,7 +66,7 @@ expanderButton ::
ExpanderButtonConfig t ->
m (Dynamic t ExpanderState)
expanderButton cfg = mdo
stateDyn <- foldDyn (\() -> toggleState) (cfg ^. #buttonInitialState) toggleEv
stateDyn <- foldDyn (\() -> toggleState) (cfg ^. #buttonInitialState) (toggleEv $> ())
let constantClasses =
"expander"
<> maybe mempty buttonTypeClasses (cfg ^. #buttonType)
@ -79,7 +80,7 @@ expanderButton cfg = mdo
, enabledClasses = mempty
, disabledClasses = mempty
, buttonEnabled = pure True
, buttonText = cfg ^. #buttonText
, buttonText = TextBuilder $ dynText $ cfg ^. #buttonText
, buttonBaseTag = ButtonTag
}
pure stateDyn

View File

@ -77,7 +77,7 @@ buttonTypeClasses LoadingLargeButtonType = "button--save-loading"
largeButton ::
(DomBuilder t m, PostBuild t m) =>
LargeButtonConfig t ->
m (Event t ())
m (Event t (Either () ()))
largeButton cfg =
buttonEl
CommonButtonConfig
@ -91,6 +91,6 @@ largeButton cfg =
, enabledClasses = mempty
, disabledClasses = "button--disabled"
, buttonEnabled = cfg ^. #buttonEnabled
, buttonText = pure $ cfg ^. #buttonText
, buttonText = textBuilder $ cfg ^. #buttonText
, buttonBaseTag = cfg ^. #buttonBaseTag
}

View File

@ -41,7 +41,7 @@ buttonSortStateClasses = \case
sortButton ::
(DomBuilder t m, PostBuild t m) =>
SortButtonConfig t ->
m (Event t ())
m (Event t (Either () ()))
sortButton cfg =
buttonEl
CommonButtonConfig
@ -53,6 +53,6 @@ sortButton cfg =
, enabledClasses = mempty
, disabledClasses = mempty
, buttonEnabled = cfg ^. #buttonEnabled
, buttonText = pure $ cfg ^. #buttonText
, buttonText = textBuilder $ cfg ^. #buttonText
, buttonBaseTag = ButtonTag
}

View File

@ -4,13 +4,14 @@ module Frontend.UIKit.Button.Static
closePopupButton,
deleteOverrideButton,
undoOverrideButton,
dropButton,
)
where
import Frontend.UIKit.Button.Common
import Reflex.Dom
closeNotificationButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
closeNotificationButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
closeNotificationButton =
buttonEl
CommonButtonConfig
@ -18,11 +19,11 @@ closeNotificationButton =
, enabledClasses = mempty
, disabledClasses = mempty
, buttonEnabled = pure True
, buttonText = pure ""
, buttonText = ""
, buttonBaseTag = ButtonTag
}
closeClassicPopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
closeClassicPopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
closeClassicPopupButton =
buttonEl
CommonButtonConfig
@ -30,11 +31,11 @@ closeClassicPopupButton =
, enabledClasses = mempty
, disabledClasses = mempty
, buttonEnabled = pure True
, buttonText = pure ""
, buttonText = ""
, buttonBaseTag = ButtonTag
}
closePopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
closePopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
closePopupButton =
buttonEl
CommonButtonConfig
@ -42,30 +43,42 @@ closePopupButton =
, enabledClasses = mempty
, disabledClasses = mempty
, buttonEnabled = pure True
, buttonText = pure ""
, buttonText = ""
, buttonBaseTag = ButtonTag
}
deleteOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
deleteOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
deleteOverrideButton =
buttonEl
CommonButtonConfig
{ constantClasses = pure "overrides__delete spot spot--cancel"
{ constantClasses = pure "editable-row__delete spot spot--cancel"
, enabledClasses = mempty
, disabledClasses = mempty
, buttonEnabled = pure True
, buttonText = pure ""
, buttonText = ""
, buttonBaseTag = ButtonTag
}
undoOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
undoOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
undoOverrideButton =
buttonEl
CommonButtonConfig
{ constantClasses = pure "overrides__delete spot spot--undo"
{ constantClasses = pure "editable-row__delete spot spot--undo"
, enabledClasses = mempty
, disabledClasses = mempty
, buttonEnabled = pure True
, buttonText = pure ""
, buttonText = ""
, buttonBaseTag = ButtonTag
}
dropButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
dropButton =
buttonEl
CommonButtonConfig
{ constantClasses = pure "drop__handler"
, enabledClasses = mempty
, disabledClasses = mempty
, buttonEnabled = pure True
, buttonText = "Actions"
, buttonBaseTag = ButtonTag
}

View File

@ -0,0 +1,84 @@
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Use newtype instead of data" #-}
module Frontend.UIKit.Button.Tree
( treeButton,
TreeButtonConfig (..),
TextBuilder (..),
TreeButtonStatus (..),
TreeButtonApply (..),
)
where
import Control.Lens
import Control.Monad
import Control.Monad.Fix
import Data.Default
import Data.Functor
import Data.Generics.Labels ()
import Frontend.UIKit.Button.Common
import GHC.Generics (Generic)
import Reflex.Dom
import Reflex.Network (networkView)
data TreeButtonConfig t = TreeButtonConfig
{ buttonText :: TextBuilder t
, subtreeHasChanges :: Bool
, visible :: Dynamic t Bool
, -- | From outside
forceState :: Event t Bool
}
deriving stock (Generic)
instance Reflex t => Default (TreeButtonConfig t) where
def =
TreeButtonConfig
{ buttonText = ""
, subtreeHasChanges = False
, visible = pure True
, forceState = never
}
treeButton ::
(DomBuilder t m, PostBuild t m, MonadHold t m, MonadFix m) =>
TreeButtonConfig t ->
-- | (<Open?>, <Click>)
m (Event t (Bool, Either () ()))
treeButton cfg = mdo
(traceEvent "treeButton" -> clickedEv) <-
networkView >=> switchHold never $
(cfg ^. #visible) <&> \visible ->
if visible
then
buttonEl
CommonButtonConfig
{ constantClasses = do
open <- openDyn
pure $
"collapse__head"
<> (if cfg ^. #subtreeHasChanges then "collapse__head--has-changes" else mempty)
<> (if open then mempty else "collapse--expanded")
, buttonText = cfg ^. #buttonText
, buttonBaseTag = ButtonTag
, buttonEnabled = pure True
, disabledClasses = mempty
, enabledClasses = mempty
}
else pure never
openDyn <-
foldDyn
( \case
Left () -> not
Right x -> const x
)
False
$ (leftmost [Right <$> cfg ^. #forceState, clickedEv $> Left ()])
pure $ coincidence $ (\click -> (,click) <$> updated openDyn) <$> clickedEv
data TreeButtonStatus
= TreeButtonClosed
| TreeButtonOpen
data TreeButtonApply
= TreeButtonApplyToSelf
| TreeButtonApplyToSubtree

View File

@ -20,60 +20,45 @@ module Frontend.Utils
DeploymentPageNotification (..),
formatPosixToDateTime,
dropdownWidget,
dropdownWidget',
deploymentConfigProgressiveComponents,
deploymentConfigProgressive,
-- deploymentConfigProgressive,
holdClearingWith,
unitEv,
(<&&>),
ProgressiveFullConfig (..),
RequestErrorHandler,
deploymentOverridesWidget,
deploymentOverridesWidgetSearched,
applicationOverridesWidget,
applicationOverridesWidgetSearched,
debounceDyn,
catchReturns,
holdDynMaybe,
constructWorkingOverridesEv,
)
where
import Common.Types as CT
import Control.Arrow
import Control.Lens
import Control.Monad
import Control.Monad.Reader
import Control.Monad.State.Strict
import qualified Data.Bifunctor as Bi
import qualified Data.Foldable as F
import qualified Data.ConfigTree as CT
import Data.Functor
import Data.Functor.Misc
import Data.Generics.Labels ()
import Data.List.NonEmpty (NonEmpty)
import Data.Map (Map)
import Data.Generics.Sum
import qualified Data.Map as M
import Data.Map.Monoidal.Strict (MonoidalMap)
import qualified Data.Map.Monoidal.Strict as MM
import Data.Maybe (fromMaybe, isJust, isNothing)
import Data.Monoid
import Data.Set (Set)
import qualified Data.Set as S
import Data.Maybe (fromMaybe)
import Data.String
import Data.Text as T (Text, null, pack, strip)
import Data.Text.Search
import Data.These
import Data.Time
import Data.UniqMap
import Data.Unique
import Data.Witherable
import Data.WorkingOverrides
import Debug.Trace
import Frontend.API
import Frontend.GHCJS
import Frontend.UIKit
import GHC.Generics (Generic)
import GHCJS.DOM
import GHCJS.DOM.Element as DOM
import GHCJS.DOM.EventM (on, target)
import GHCJS.DOM.GlobalEventHandlers as Events (click)
import GHCJS.DOM.Node as DOM
import Generic.Data (Generically (..))
import Reflex.Dom as R
import Reflex.Dom.Renderable
import Reflex.Network
@ -90,49 +75,30 @@ elementClick = do
doc <- currentDocumentUnchecked
wrapDomEvent doc (`on` Events.click) $ ClickedElement <$> target
-- | Dropdown widget which binds its own @document click@ event.
dropdownWidget ::
MonadWidget t m =>
-- | Button widget which opens the dropdown widget.
m () ->
-- | Widget with the dropdown list.
-- Returns an event carrying the user's selection.
m (Event t a) ->
m (Event t a)
dropdownWidget btn body = mdo
clickedEl <- elementClick
dropdownWidget' clickedEl btn body
-- | Similar to 'dropdownWidget' but uses @document click@ event that may be
-- shared between other widgets.
dropdownWidget' ::
dropdownWidget ::
MonadWidget t m =>
-- | Document click event that may be shared between other widgets.
Event t ClickedElement ->
-- | Button widget which opens the dropdown widget.
m () ->
-- | Widget with the dropdown list.
-- Returns an event carrying the user's selection.
m (Event t a) ->
m (Event t a)
dropdownWidget' clickedEl btn body = mdo
clickInsideEv <- performEvent $
ffor clickedEl $ \(ClickedElement clicked) ->
DOM.contains (_element_raw btnEl) clicked
openedDyn <- foldDyn switchState False $ clickInsideEv
let switchState ev cur = ev && not cur
wrapperClassDyn = ffor openedDyn $ \case
dropdownWidget body = mdo
openedDyn <- toggle False $ btnEl
let wrapperClassDyn = ffor openedDyn $ \case
True -> "class" =: "drop drop--actions drop--expanded"
False -> "class" =: "drop drop--actions"
(btnEl, (_, wEv)) <- elDynAttr'
(btnEl, wEv) <- elDynAttr
"div"
wrapperClassDyn
$ do
btn
elDynAttr'
"div"
(constDyn $ "class" =: "drop__dropdown")
body
clickedEv <- dropButton
aEv <-
elDynAttr
"div"
(constDyn $ "class" =: "drop__dropdown")
body
pure (clickedEv, aEv)
pure wEv
showT :: Show a => a -> Text
@ -214,90 +180,46 @@ octopodTextInput clss lbl placeholder val errEv =
elClass "section" "deployment__section" $ do
elClass "h3" "deployment__sub-heading" $ text lbl
elClass "div" "deployment__widget" $ do
(value -> valDyn, validDyn) <- octopodTextInput' (pure []) (pure False) (pure clss) placeholder (pure . fromMaybe "" $ val) errEv
(value -> valDyn, validDyn) <- octopodTextInput' (pure []) (pure False) (pure mempty) (pure clss) placeholder (pure . fromMaybe "" $ val) errEv
pure (valDyn, validDyn)
-- | Widget that can show and hide overrides if there are more than 3. This
-- widget is used in the deployments table and the deployment action table.
overridesWidget ::
(MonadWidget t m, Renderable te, Ord te) =>
(MonadWidget t m, Renderable te, IsString te, Monoid te) =>
-- | List of overrides.
Overrides' te l ->
(Event t () -> m (Event t (DefaultConfig' te l))) ->
m ()
overridesWidget ovs getDef = divClass "listing listing--for-text" $ mdo
defMDyn <- getDef firstExpand >>= holdDynMaybe
dyn_ $
expandState <&> \case
ExpandedState ->
void $
networkView $
do
defM <- defMDyn
pure $
showNonEditableWorkingOverride (isNothing defM) (isJust defM) RegularNonEditableWorkingOverrideStyle $
elemsUniq $ constructWorkingOverrides defM ovs
ContractedState ->
let ovsList = elemsUniq $ constructWorkingOverrides Nothing ovs
in showNonEditableWorkingOverride False False RegularNonEditableWorkingOverrideStyle $
take 3 ovsList
overridesWidget (Overrides ovs) = mdo
let l = CT.toFlatList ovs
expandable = length l > 3
expandState <-
expanderButton
ExpanderButtonConfig
{ buttonText = do
s <- expandState
pure $ case s of
ExpandedState -> "Hide default configuration"
ContractedState -> "Show full configuration"
, buttonInitialState = ContractedState
, buttonType = Just ListingExpanderButton
, buttonStyle = RegularExpanderButtonStyle
}
firstExpand <- headE $ ($> ()) $ ffilter (== ExpandedState) $ updated expandState
if expandable
then mdo
dyn_ $
expandState <&> \s -> do
let l' = case s of
ExpandedState -> l
ContractedState -> take 3 l
showFlatConfig l'
pure ()
expandState <-
expanderButton
ExpanderButtonConfig
{ buttonText = do
s <- expandState
pure $ case s of
ExpandedState -> "Hide default configuration"
ContractedState -> "Show full configuration"
, buttonInitialState = ContractedState
, buttonType = Just ListingExpanderButton
, buttonStyle = RegularExpanderButtonStyle
}
pure ()
else showFlatConfig l
pure ()
deploymentOverridesWidget ::
(MonadWidget t m) =>
RequestErrorHandler t m ->
Overrides 'DeploymentLevel ->
m ()
deploymentOverridesWidget hReq depOvs =
deploymentOverridesWidgetSearched hReq (wrapResult depOvs)
deploymentOverridesWidgetSearched ::
(MonadWidget t m) =>
RequestErrorHandler t m ->
Overrides' SearchResult 'DeploymentLevel ->
m ()
deploymentOverridesWidgetSearched hReq depOvs =
overridesWidget depOvs $ (fmap . fmap . fmap) wrapResult $ defaultDeploymentOverrides >=> hReq
applicationOverridesWidget ::
MonadWidget t m =>
RequestErrorHandler t m ->
Overrides 'DeploymentLevel ->
Overrides 'ApplicationLevel ->
m ()
applicationOverridesWidget hReq depOvs appOvs =
applicationOverridesWidgetSearched hReq (wrapResult depOvs) (wrapResult appOvs)
applicationOverridesWidgetSearched ::
MonadWidget t m =>
RequestErrorHandler t m ->
Overrides' SearchResult 'DeploymentLevel ->
Overrides' SearchResult 'ApplicationLevel ->
m ()
applicationOverridesWidgetSearched hReq depOvs appOvs =
overridesWidget appOvs $ \fire -> do
depDefEv <- defaultDeploymentOverrides fire >>= hReq
fmap switchDyn $
(fmap . fmap . fmap) wrapResult $
networkHold (pure never) $
depDefEv <&> \depDef -> do
pb <- getPostBuild
defaultApplicationOverrides (pure $ Right $ applyOverrides (deSearch depOvs) depDef) pb >>= hReq
-- | Type of notification at the top of pages.
data DeploymentPageNotification
= -- | Message text for a \"success\" events.
@ -351,9 +273,11 @@ deploymentPopupBody ::
m (Dynamic t (Maybe DeploymentUpdate))
deploymentPopupBody hReq defAppOv defDepOv errEv = mdo
pb <- getPostBuild
(defDepEv, defApp, depCfgEv) <- deploymentConfigProgressiveComponents hReq deploymentOvsDynDebounced
defAppM <- holdClearingWith defApp (unitEv deploymentOvsDyn)
defDep <- holdDynMaybe defDepEv
(defDepEv, defAppEv, depCfgEv) <-
deploymentConfigProgressiveComponents hReq deploymentOvsDynDebounced
defAppEvM <- holdClearingWith defAppEv (unitEv deploymentOvsDyn)
depKeys <- deploymentOverrideKeys pb >>= hReq
let commandResponseEv = fmapMaybe commandResponse errEv
@ -361,46 +285,47 @@ deploymentPopupBody hReq defAppOv defDepOv errEv = mdo
let holdDCfg ::
Dynamic t [Text] ->
Dynamic t (Maybe (DefaultConfig l)) ->
(DefaultConfig l) ->
Overrides l ->
m (Dynamic t (Overrides l), Dynamic t (Overrides l), Dynamic t Bool)
holdDCfg values dCfgDyn ovs = mdo
ovsDyn <- holdDyn ovs ovsEv
ovsDynDebounced <- holdDyn ovs ovsEvDebounced
x <- attachDyn (current ovsDyn) dCfgDyn
(splitE -> (ovsEvEv, isValidDynEv)) <-
(>>= joinEvM) $
runWithReplace' loadingOverrides $
updated x <&> \(ovs', dCfg) -> untilReady' loadingOverrides $ envVarsInput values dCfg ovs'
ovsEv <- joinEvM ovsEvEv
ovsEvDebounced <- debounce 2 ovsEv
isValid <- join <$> holdDyn (pure False) isValidDynEv
pure (ovsDyn, ovsDynDebounced, isValid)
m (Dynamic t (Overrides l), Dynamic t (Overrides l))
holdDCfg (traceDyn "values" -> values) cfgDyn@(DefaultConfig ct) ovs = mdo
let ovs' = constructWorkingOverrides cfgDyn ovs
lookupOverride k = CT.lookup (CT.deconstructConfigKey k) ct
(resCfgDyn :: Dynamic t [Dynamic t WorkingOverride]) <- envVarsInput values lookupOverride ovs'
(resEvDebounced :: Event t [Dynamic t WorkingOverride]) <- debounce 2 $ updated resCfgDyn
let resDyn' = join . fmap destructWorkingOverridesDyn $ resCfgDyn
debouncedResDyn <- fmap join . holdDyn (pure ovs) . fmap destructWorkingOverridesDyn $ resEvDebounced
pure (resDyn', debouncedResDyn)
depKeysDyn <- holdDyn [] depKeys
(deploymentOvsDyn, deploymentOvsDynDebounced, depValidDyn) <-
deploymentSection "Deployment configuration" $ holdDCfg depKeysDyn defDep defDepOv
(join . fmap fst &&& join . fmap snd -> (deploymentOvsDyn, deploymentOvsDynDebounced)) <-
deploymentSection "Deployment configuration" $
networkHold (loadingOverrides $> (pure defDepOv, pure defDepOv)) $
defDepEv <&> \defDep -> holdDCfg depKeysDyn defDep defDepOv
appKeys <- waitForValuePromptly depCfgEv $ \deploymentCfg -> do
pb' <- getPostBuild
applicationOverrideKeys (pure $ Right deploymentCfg) pb' >>= hReq >>= immediateNothing
appKeysDyn <- holdDyn [] $ catMaybes appKeys
(applicationOvsDyn, _, appValidDyn) <- deploymentSection "App configuration" $ holdDCfg appKeysDyn defAppM defAppOv
(join -> applicationOvsMDyn) <-
deploymentSection "App configuration" >=> holdDyn (pure Nothing) . fmap sequenceA $
networkView $
defAppEvM <&> \case
Nothing -> loadingOverrides $> Nothing
Just defApp -> Just . fst <$> holdDCfg appKeysDyn defApp defAppOv
pure $ do
depValid <- depValidDyn
appValid <- appValidDyn
if depValid && appValid
then do
depCfg <- deploymentOvsDyn
appOvs <- applicationOvsDyn
pure $
Just $
DeploymentUpdate
{ appOverrides = appOvs
, deploymentOverrides = depCfg
}
else pure Nothing
depCfg <- deploymentOvsDyn
appOvsM <- applicationOvsMDyn
pure $ do
appOvs <- appOvsM
Just $
DeploymentUpdate
{ appOverrides = appOvs
, deploymentOverrides = depCfg
}
deploymentConfigProgressiveComponents ::
MonadWidget t m =>
@ -427,36 +352,17 @@ deploymentConfigProgressiveComponents hReq depOvsDyn = do
defaultApplicationOverrides (pure $ Right depCfg) pb' >>= hReq
pure (defDepEv, defAppEv, depCfgEv)
deploymentConfigProgressive ::
MonadWidget t m =>
RequestErrorHandler t m ->
Dynamic t (Overrides 'DeploymentLevel) ->
Dynamic t (Overrides 'ApplicationLevel) ->
m (Dynamic t ProgressiveFullConfig)
deploymentConfigProgressive hReq depOvsDyn appOvsDyn = do
(depCfgEv, defAppEv, _) <- deploymentConfigProgressiveComponents hReq depOvsDyn
defAppMDyn <- holdDynMaybe defAppEv
defCfgMDyn <- holdDynMaybe depCfgEv
pure $ do
defAppM <- defAppMDyn
appOvs <- appOvsDyn
defDepM <- defCfgMDyn
depOvs <- depOvsDyn
pure
ProgressiveFullConfig
{ appConfig = constructWorkingOverrides defAppM appOvs
, appConfigLoading = isNothing defAppM
, depConfig = constructWorkingOverrides defDepM depOvs
, depConfigLoading = isNothing defDepM
}
data ProgressiveFullConfig = ProgressiveFullConfig
{ appConfig :: WorkingOverrides
, appConfigLoading :: Bool
, depConfig :: WorkingOverrides
, depConfigLoading :: Bool
}
deriving stock (Generic)
constructWorkingOverridesEv ::
MonadWidget t m => Event t (DefaultConfig l) -> Dynamic t (Overrides l) -> m (Event t WorkingOverrides)
constructWorkingOverridesEv defCfgEv ovsDyn = do
defCfgMDyn <- holdDynMaybe defCfgEv
pure $
catMaybes . updated $ do
defCfgM <- defCfgMDyn
ovs <- ovsDyn
pure $ do
defCfg <- defCfgM
pure $ constructWorkingOverrides defCfg ovs
waitForValuePromptly :: (MonadHold t m, Adjustable t m) => Event t x -> (x -> m (Event t y)) -> m (Event t y)
waitForValuePromptly ev f = fmap switchPromptlyDyn $ networkHold (pure never) $ f <$> ev
@ -492,32 +398,23 @@ errorHeader ::
-- | Message text.
Dynamic t Text ->
m ()
errorHeader appErr = do
divClass "deployment__output notification notification--danger" $ do
errorHeader appErr =
elClass "pre" "deployment__output notification notification--danger" $
dynText appErr
-- | Widget with override fields. This widget supports adding and
-- removing key-value pairs.
envVarsInput ::
forall l t m.
forall t m.
MonadWidget t m =>
Dynamic t [Text] ->
Maybe (DefaultConfig l) ->
(Text -> Maybe Text) ->
-- | Initial deployment overrides.
Overrides l ->
-- | Updated deployment overrides.
m (Event t (Overrides l), Dynamic t Bool)
envVarsInput values dCfg ovs = mdo
let initialOverrides = constructWorkingOverrides dCfg ovs
(envsDyn, addedKeys) <-
mapAccumDyn
( \prevOvs -> \case
Left () ->
Bi.second S.singleton $ insertUniqStart newWorkingOverride prevOvs
Right (Endo f) -> (f prevOvs, S.empty)
)
initialOverrides
$ leftmost [Left <$> clickEv, Right <$> updEv]
WorkingOverrides ->
-- | Updated deployment overrides. [(<full deconstructed key>, <value>)]
m (Dynamic t [Dynamic t WorkingOverride])
envVarsInput (traceDyn "values" -> values) lookupOverride ovs = divClass "padded" $ mdo
let initialConfig = CT.markValues ovs
clickEv <-
dashButton
DashButtonConfig
@ -526,187 +423,93 @@ envVarsInput values dCfg ovs = mdo
, buttonType = Just AddDashButtonType
, buttonStyle = OverridesDashButtonStyle
}
let workingOverridesWithErrors = validateWorkingOverrides . uniqMap <$> envsDyn
erroredOverride = M.filter ((/= mempty) . snd) <$> workingOverridesWithErrors
isValid = M.null <$> erroredOverride
(_, changedValues) <-
mapAccumDyn
( \prev new ->
( fmap (\(v, _) -> (v, mempty)) new
, new <> prev
)
)
mempty
(updated erroredOverride)
let updatedValues = restrictMapEv updatedKeys (updated workingOverridesWithErrors)
valChangedEvSelector = fanMap (clearedOvs <> updatedValues <> changedValues)
(splitE -> (clearedOvs, deletedOvs)) =
alignEventWithMaybe
( \case
These keys m ->
Just $
let cleared = M.restrictKeys m keys
in (cleared, M.fromSet (const Nothing) (keys `S.difference` M.keysSet cleared))
_ -> Nothing
)
removedKeys
(updated workingOverridesWithErrors)
listChange =
deletedOvs <> (fmap . fmap) Just (restrictMapEv addedKeys (updated workingOverridesWithErrors))
let emptyItem = ("", CustomConfigValue $ Left $ CustomKey "")
addedItemsCount <- foldDyn (const (fst . insertUniqStart ())) emptyUniqKeyMap clickEv
addedOvsListDyn <- (fmap . fmap) (fmap snd . M.toList)
<$> list (uniqMap <$> addedItemsCount)
$ \_ ->
envVarInput lookupOverride values emptyItem
treeResCfg <-
showOverrideTree
(isn't (_Ctor' @"DefaultConfigValue") . snd)
(\_ v -> v)
(\a _ _ -> envVarInput lookupOverride values (trace "a" a))
initialConfig
let resCfg :: Dynamic t [Dynamic t (Text, ConfigValue Text)]
resCfg = (<> treeResCfg) <$> addedOvsListDyn
addingIsEnabled = join $ fmap and . sequenceA . (fmap . fmap) (not . T.null . fst) <$> resCfg
pure resCfg
(splitE3 -> (updEv, removedKeys, updatedKeys)) <-
switchDyn . fmap F.fold
<$> listHoldWithKey
(validateWorkingOverrides . uniqMap $ initialOverrides)
listChange
( \i val -> do
valDyn <- holdDyn val $ select valChangedEvSelector $ Const2 i
userActionEv <- envVarInput values valDyn
pure $ performUserOverrideAction (lookupDefaultConfig <$> dCfg) i <$> userActionEv
)
let addingIsEnabled = all (\(WorkingOverrideKey _ x, _) -> not . T.null $ x) . elemsUniq <$> envsDyn
case dCfg of
Just _ -> pure ()
Nothing -> loadingOverrides
pure (updated $ destructWorkingOverrides <$> envsDyn, isValid)
restrictMapEv :: (Reflex t, Ord k) => Event t (Set k) -> Event t (Map k v) -> Event t (Map k v)
restrictMapEv =
alignEventWithMaybe
( \case
These keys _ | S.null keys -> Nothing
These keys m -> Just $ M.restrictKeys m keys
_ -> Nothing
)
validateWorkingOverrides ::
forall f.
Traversable f =>
f WorkingOverride ->
f (WorkingOverride, OverrideErrors)
validateWorkingOverrides overrides =
let (result, keyOccurrences :: MonoidalMap Text (Sum Int)) =
flip runState mempty $ forM overrides \override@(WorkingOverrideKey _ key, value') -> do
case value' of
WorkingDeletedValue _ -> pure ()
_ -> modify (<> MM.singleton key (Sum 1))
pure . (override,) . mconcat $
[ case MM.lookup key keyOccurrences of
Just (Sum n)
| n > 1 ->
overrideKeyErrors "You can not use the same key multiple times."
_ -> mempty
, if T.null key
then overrideKeyErrors "Keys can not be empty."
else mempty
, case value' of
WorkingCustomValue "" -> overrideValueErrors "Values can not be empty."
WorkingCustomValue _ -> mempty
WorkingDefaultValue "" -> overrideValueErrors "Values can not be empty."
WorkingDefaultValue _ -> mempty
WorkingDeletedValue _ -> mempty
]
in result
data OverrideErrors = OverrideErrors
{ keyErrors :: Maybe (NonEmpty Text)
, valueErrors :: Maybe (NonEmpty Text)
}
deriving stock (Generic, Eq, Show)
deriving (Semigroup, Monoid) via Generically OverrideErrors
overrideKeyErrors :: Text -> OverrideErrors
overrideKeyErrors x = mempty {keyErrors = Just $ pure x}
overrideValueErrors :: Text -> OverrideErrors
overrideValueErrors x = mempty {valueErrors = Just $ pure x}
-- validateWorkingOverrides ::
-- forall f.
-- Traversable f =>
-- f WorkingOverride ->
-- f (WorkingOverride, OverrideErrors)
-- validateWorkingOverrides overrides =
-- let (result, keyOccurrences :: MonoidalMap Text (Sum Int)) =
-- flip runState mempty $ for overrides \override@(WorkingOverrideKey _ key, value') -> do
-- case value' of
-- WorkingDeletedValue _ -> pure ()
-- _ -> modify (<> MM.singleton key (Sum 1))
-- pure . (override,) . mconcat $
-- [ case MM.lookup key keyOccurrences of
-- Just (Sum n)
-- | n > 1 ->
-- overrideKeyErrors "You can not use the same key multiple times."
-- _ -> mempty
-- , if T.null key
-- then overrideKeyErrors "Keys can not be empty."
-- else mempty
-- , case value' of
-- WorkingCustomValue "" -> overrideValueErrors "Values can not be empty."
-- WorkingCustomValue _ -> mempty
-- WorkingDefaultValue "" -> overrideValueErrors "Values can not be empty."
-- WorkingDefaultValue _ -> mempty
-- WorkingDeletedValue _ -> mempty
-- ]
-- in result
-- | Widget for entering a key-value pair.
envVarInput ::
(MonadWidget t m) =>
(Text -> Maybe Text) ->
-- | The key values to suggest to the user
Dynamic t [Text] ->
-- | Current variable key and value.
Dynamic t (WorkingOverride, OverrideErrors) ->
m (Event t UserOverrideAction)
envVarInput values val = untilReadyEv' loadingOverride $ do
let kDyn = val <&> fst <&> \(WorkingOverrideKey _ x, _) -> x
-- Either <override present> <override deleted>
d <-
eitherDyn $
val <&> snd . fst <&> \case
WorkingCustomValue v -> Right (v, EditedOverrideFieldType)
WorkingDefaultValue v -> Right (v, DefaultOverrideFieldType)
WorkingDeletedValue v -> Left v
networkView >=> switchHold never $
d <&> \case
Right vtDyn -> do
let (vDyn, vTypeDyn) = splitDynPure vtDyn
(keyTextDyn, valTextDyn, closeEv) <-
overrideField
values
OverrideField
{ fieldValue = kDyn
, fieldError = keyErrors . snd <$> val
, fieldDisabled =
val <&> fst <&> \(WorkingOverrideKey t _, _) -> t == DefaultWorkingOverrideKey
, fieldType =
val <&> fst <&> \(WorkingOverrideKey t _, _) -> case t of
CustomWorkingOverrideKey -> EditedOverrideFieldType
DefaultWorkingOverrideKey -> DefaultOverrideFieldType
}
OverrideField
{ fieldValue = vDyn
, fieldError = valueErrors . snd <$> val
, fieldDisabled = pure False
, fieldType = vTypeDyn
}
WorkingOverride ->
m (Dynamic t WorkingOverride)
envVarInput f keys val = mdo
value' <- holdDyn val (leftmost [buttonValue, updated inputValue])
(keyTextDyn, valTextDyn, buttonPressEv) <- configField keys value'
let inputValue = do
v <- valTextDyn
k <- keyTextDyn
pure $
leftmost
[ UpdateKey <$> updated keyTextDyn
, UpdateValue <$> updated valTextDyn
, closeEv $> DeleteOverride
]
Left vDyn -> do
restoreEv <-
networkView >=> switchHold never $ do
v <- vDyn
k <- kDyn
pure $ deletedOverride k v
pure $
flip push restoreEv $ \() -> do
v <- sample . current $ vDyn
pure $ UpdateValue <$> v
data UserOverrideAction = UpdateKey !Text | UpdateValue !Text | DeleteOverride
performUserOverrideAction ::
Maybe (Text -> Maybe Text) ->
Int ->
UserOverrideAction ->
-- | (endo, removed/cleared keys, updatedKeys)
(Endo WorkingOverrides, Set Int, Set Int)
performUserOverrideAction f i (UpdateValue v) = (,S.empty,S.singleton i) $
Endo $
updateUniq i $ \(k@(WorkingOverrideKey _ kt), _) ->
( k
, case f >>= ($ kt) of
Just v' | v == v' -> WorkingDefaultValue v
_ -> WorkingCustomValue v
)
performUserOverrideAction _ i (UpdateKey k) = (,S.empty,S.singleton i) $
Endo $
updateUniq i $
\(_, v) -> (WorkingOverrideKey CustomWorkingOverrideKey k, v)
performUserOverrideAction f i DeleteOverride = (,S.singleton i,S.empty) $
Endo $ \m ->
case f of
Nothing -> updateUniq i (\(k, _) -> (k, WorkingDeletedValue Nothing)) m
Just _ -> case lookupUniq i m of
Nothing -> m
Just (WorkingOverrideKey DefaultWorkingOverrideKey k, _) ->
updateUniq i (const (WorkingOverrideKey DefaultWorkingOverrideKey k, WorkingDeletedValue (f >>= ($ k)))) m
Just (WorkingOverrideKey CustomWorkingOverrideKey _, _) -> deleteUniq i m
( k
, case f k of
Nothing -> CustomConfigValue $ Left $ CustomKey v
Just x | x == v -> DefaultConfigValue v
Just _ -> CustomConfigValue $ Right $ CustomValue v
)
buttonValue = flip push buttonPressEv $ \x ->
Just <$> do
k <- sample . current $ keyTextDyn
(k,) <$> case x of
ConfigFieldButtonClear -> do
v <- case f k of
Nothing -> sample . current $ valTextDyn
Just v -> pure v
pure $ CustomConfigValue $ Right $ DeletedValue $ Just v
ConfigFieldButtonRecover -> do
case f k of
Nothing -> do
-- the value is not in the default config
v <- sample . current $ valTextDyn
pure $ CustomConfigValue $ Left $ CustomKey v
Just v -> do
-- the value is in the default config
pure $ DefaultConfigValue v
pure $ value'
holdDynMaybe :: (Reflex t, MonadHold t m) => Event t a -> m (Dynamic t (Maybe a))
holdDynMaybe ev = holdDyn Nothing $ fmapCheap Just ev
@ -716,12 +519,6 @@ immediateNothing ev = do
pb <- getPostBuild
pure $ leftmost [pb $> Nothing, fmapCheap Just ev]
attachDyn :: (Reflex t, MonadHold t m) => Behavior t a -> Dynamic t b -> m (Dynamic t (a, b))
attachDyn b d = do
currD <- sample . current $ d
currB <- sample b
holdDyn (currB, currD) (attach b $ updated d)
holdClearingWith :: (Reflex t, MonadHold t m) => Event t a -> Event t () -> m (Dynamic t (Maybe a))
holdClearingWith aEv clear =
holdDyn Nothing $ leftmost [fmapCheap Just aEv, fmapCheap (const Nothing) clear]
@ -751,6 +548,3 @@ catchReturns :: (DomBuilder t m, MonadFix m) => (Event t () -> m a) -> m a
catchReturns f = mdo
(divEl, a) <- el' "div" $ f $ ($> ()) . ffilter (== 13) $ domEvent Keypress divEl
pure a
splitE3 :: Reflex t => Event t (a, b, c) -> (Event t a, Event t b, Event t c)
splitE3 e = ((\(a, _, _) -> a) <$> e, (\(_, b, _) -> b) <$> e, (\(_, _, c) -> c) <$> e)

View File

@ -12,6 +12,7 @@ where
import Reflex.Dom
import Data.Functor
import Data.Generics.Labels ()
import Frontend.UIKit
@ -53,7 +54,7 @@ popupWidget m =
divClass "classic-popup__slot" $ do
(okEv, cancelEv) <- m
closeEv <- closeClassicPopupButton
pure $ (okEv, leftmost [cancelEv, closeEv])
pure $ (okEv, leftmost [cancelEv, closeEv $> ()])
-- | Popup that requires confirmation of deployment deletion.
confirmArchivePopup ::
@ -77,5 +78,5 @@ confirmArchivePopup showEv txt = do
& #buttonStyle .~~ DialogActionLargeButtonStyle
& #buttonPriority .~~ SecondaryLargeButton
& #buttonText .~~ "Cancel"
pure (okEv, cancelEv)
pure (okEv $> (), cancelEv $> ())
classicPopup showEv body

View File

@ -5,31 +5,30 @@
--This module contains the definition of a deployment page.
module Page.Deployment (deploymentPage) where
import Control.Lens
import Control.Monad
import Data.Coerce
import Data.Generics.Product (field)
import Data.Text as T (Text, pack)
import Obelisk.Route.Frontend
import Reflex.Dom as R
import Servant.Reflex
import Common.Types as CT
import Common.Utils
import Control.Lens
import Control.Monad
import Control.Monad.Reader
import Data.Align
import Data.Coerce
import Data.Functor
import Data.Generics.Labels ()
import Data.Generics.Product (field)
import Data.Text as T (Text, pack)
import Data.Time
import Data.UniqMap
import Frontend.API
import Frontend.GHCJS
import Frontend.Route
import Frontend.UIKit
import Frontend.Utils
import Obelisk.Route.Frontend
import Page.ClassicPopup
import Page.Elements.Links
import Page.Popup.EditDeployment
import Reflex.Dom as R
import Reflex.Network
import Servant.Reflex
import Servant.Reflex.Extra
-- | The root widget of a deployment page. It requests the deployment data.
@ -125,7 +124,7 @@ deploymentHead dfiDyn sentEv =
& #buttonStyle .~~ PageActionLargeButtonStyle
& #buttonText .~~ "Recover from archive"
& #buttonEnabled .~~ btnEnabledDyn
void $ restoreEndpoint (Right . coerce <$> dname) btnEv
void $ restoreEndpoint (Right . coerce <$> dname) $ btnEv $> ()
pure (never, never)
else mdo
let btnState = not $ isPending . recordedStatus $ dfi ^. field @"status"
@ -162,7 +161,7 @@ deploymentHead dfiDyn sentEv =
, buttonBaseTag = ATag url
}
)
delEv <- confirmArchivePopup archEv $ do
delEv <- confirmArchivePopup (archEv $> ()) $ do
text "Are you sure you want to archive the"
el "br" blank
dynText dname
@ -192,7 +191,7 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $
wrapRequestErrors $ \hReq -> do
let nameDyn = dfiDyn <^.> dfiName
depDyn = dfiDyn <^.> #deployment
cfgDyn <- deploymentConfigProgressive hReq (depDyn <^.> #deploymentOverrides) (depDyn <^.> #appOverrides)
(defDepCfgEv, defAppCfgEv, _) <- deploymentConfigProgressiveComponents hReq (depDyn <^.> #deploymentOverrides)
divClass "deployment__summary" $ do
divClass "deployment__stat" $ do
elClass "b" "deployment__param" $ text "Status"
@ -213,18 +212,26 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $
divClass "deployment__widget" $
divClass "listing" $
void $ simpleList urlsDyn renderMetadataLink
void $
networkView $
cfgDyn <&> \cfg -> do
let showVars bL l =
divClass "deployment__widget" $
showNonEditableWorkingOverride (cfg ^. bL) (not $ cfg ^. bL) LargeNonEditableWorkingOverrideStyle $
elemsUniq (cfg ^. l)
deploymentSection "Deployment configuration" $ showVars #depConfigLoading #depConfig
deploymentSection "App configuration" $ showVars #appConfigLoading #appConfig
deploymentSection "Deployment configuration" $ showVars defDepCfgEv $ depDyn <^.> #deploymentOverrides
deploymentSection "App configuration" $ showVars defAppCfgEv $ depDyn <^.> #appOverrides
deploymentSection "Actions" $
divClass "table table--actions" $
actionsTable hReq updEv nameDyn
actionsTable updEv nameDyn
showVars ::
MonadWidget t m =>
Event t (DefaultConfig l) ->
Dynamic t (Overrides l) ->
m ()
showVars defCfgEv ovsDyn = do
workingOvsEv <- constructWorkingOverridesEv defCfgEv ovsDyn
_ <-
divClass "deployment__widget" $
networkHold nonEditableLoading $
workingOvsEv <&> \x -> do
showNonEditableWorkingOverrideTree (pure x)
pure ()
pure ()
-- | Widget with a table of actions that can be performed on a deployment.
-- It requests deployment data.
@ -232,12 +239,11 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $
-- otherwise it calls 'actionsTableData', passing the received data.
actionsTable ::
MonadWidget t m =>
RequestErrorHandler t m ->
-- | Event notifying about the need to update data.
Event t () ->
Dynamic t DeploymentName ->
m ()
actionsTable hReq updEv nameDyn = do
actionsTable updEv nameDyn = do
pb <- getPostBuild
respEv <- infoEndpoint (Right <$> nameDyn) pb
let okEv = join . fmap logs <$> fmapMaybe reqSuccess respEv
@ -247,7 +253,7 @@ actionsTable hReq updEv nameDyn = do
widgetHold_ actionsTableLoading $
leftmost
[ actionsTableError <$ errEv
, actionsTableData hReq updEv nameDyn <$> okEv
, actionsTableData updEv nameDyn <$> okEv
]
-- | Header of the actions table.
@ -285,25 +291,24 @@ actionsTableError = do
-- It updates data every time when the supplied event fires.
actionsTableData ::
MonadWidget t m =>
RequestErrorHandler t m ->
-- | Event notifying about the need to update data.
Event t () ->
Dynamic t DeploymentName ->
-- | Initial logs.
[DeploymentLog] ->
m ()
actionsTableData hReq updEv nameDyn initLogs = do
actionsTableData updEv nameDyn initLogs = do
respEv <- infoEndpoint (Right <$> nameDyn) updEv
let okEv = (>>= logs) <$> fmapMaybe reqSuccess respEv
logsDyn <- holdDyn initLogs okEv
el "tbody" $
void $
simpleList logsDyn $ \logDyn -> do
dyn_ $ actinRow hReq <$> logDyn
dyn_ $ actionRow <$> logDyn
-- | Data row of the actions table.
actinRow :: RequestErrorHandler t m -> MonadWidget t m => DeploymentLog -> m ()
actinRow hReq DeploymentLog {..} = do
actionRow :: MonadWidget t m => DeploymentLog -> m ()
actionRow DeploymentLog {..} = do
el "tr" $ do
el "td" $ do
text $ actionToText action
@ -311,8 +316,8 @@ actinRow hReq DeploymentLog {..} = do
"status "
<> if exitCode == 0 then "status--success" else "status--failure"
divClass statusClass blank
el "td" $ deploymentOverridesWidget hReq deploymentDepOverrides
el "td" $ applicationOverridesWidget hReq deploymentDepOverrides deploymentAppOverrides
el "td" $ overridesWidget deploymentDepOverrides
el "td" $ overridesWidget deploymentAppOverrides
el "td" $ text $ showT $ exitCode
el "td" $ text $ formatPosixToDateTime createdAt
el "td" $ text $ formatDuration duration

View File

@ -24,6 +24,7 @@ import Common.Utils
import Control.Applicative
import Control.Monad.Reader
import Data.Align
import Data.Foldable
import Data.Functor
import qualified Data.Semigroup as S
import qualified Data.Text as T
@ -79,20 +80,19 @@ deploymentsWidget ::
[DeploymentFullInfo] ->
m ()
deploymentsWidget updAllEv dfis = do
(showNewDeploymentEv, editEv) <- deploymentsWidgetWrapper $
wrapRequestErrors $ \hReq -> mdo
pageNotification $
leftmost
[ DPMError
"Deployment list update failed, deployment list\
\ may be slightly outdated."
<$ errUpdEv
, DPMClear <$ okUpdEv
]
(showNewDeploymentEv', termDyn') <- deploymentsHeadWidget True okUpdEv
termDyn <- debounceDyn 0.3 termDyn'
(okUpdEv, errUpdEv, editEv) <- deploymentsListWidget hReq updAllEv termDyn dfis
pure (showNewDeploymentEv', deSearch <$> editEv)
(showNewDeploymentEv, editEv) <- deploymentsWidgetWrapper $ mdo
pageNotification $
leftmost
[ DPMError
"Deployment list update failed, deployment list\
\ may be slightly outdated."
<$ errUpdEv
, DPMClear <$ okUpdEv
]
(showNewDeploymentEv', termDyn') <- deploymentsHeadWidget True okUpdEv
termDyn <- debounceDyn 0.3 termDyn'
(okUpdEv, errUpdEv, editEv) <- deploymentsListWidget updAllEv termDyn dfis
pure (showNewDeploymentEv', deSearch <$> editEv)
newDepEv <- newDeploymentPopup showNewDeploymentEv never
setRoute $ newDepEv <&> \newDep -> DashboardRoute :/ Just (newDep ^. #name)
@ -178,13 +178,12 @@ deploymentsListWidget ::
, SetRoute t (R Routes) m
, MonadReader ProjectConfig m
) =>
RequestErrorHandler t m ->
Event t () ->
Dynamic t Text ->
-- | Initial deployment data
[DeploymentFullInfo] ->
m (Event t (), Event t (), Event t SearchedDeploymentInfo)
deploymentsListWidget hReq updAllEv termDyn ds = dataWidgetWrapper $ mdo
deploymentsListWidget updAllEv termDyn ds = dataWidgetWrapper $ mdo
retryEv <- delay 10 errUpdEv
updRespEv <- listEndpoint $ leftmost [updAllEv, () <$ retryEv]
let okUpdEv = fmapMaybe reqSuccess updRespEv
@ -197,9 +196,8 @@ deploymentsListWidget hReq updAllEv termDyn ds = dataWidgetWrapper $ mdo
let (archivedDsDyn, activeDsDyn) =
splitDynPure $ L.partition isDeploymentArchived <$> searchedDyn
searchSorting = termDyn $> Nothing
clickedEv <- elementClick
editEv <- activeDeploymentsWidget hReq searchSorting clickedEv activeDsDyn
archivedDeploymentsWidget hReq searchSorting clickedEv archivedDsDyn
editEv <- activeDeploymentsWidget searchSorting activeDsDyn
archivedDeploymentsWidget searchSorting archivedDsDyn
pure (() <$ okUpdEv, () <$ errUpdEv, editEv)
type SearchedDeploymentInfo = DeploymentFullInfo' SearchResult
@ -210,16 +208,12 @@ activeDeploymentsWidget ::
, SetRoute t (R Routes) m
, MonadReader ProjectConfig m
) =>
RequestErrorHandler t m ->
Dynamic t (Maybe (SortDir DeploymentFullInfo)) ->
-- | Event that carries the clicked DOM element. This event is required by
-- `dropdownWidget'`.
Event t ClickedElement ->
Dynamic t [SearchedDeploymentInfo] ->
-- | Returns an event carrying editable deployment
-- to \"edit deployment\" sidebar.
m (Event t SearchedDeploymentInfo)
activeDeploymentsWidget hReq searchSorting clickedEv dsDyn =
activeDeploymentsWidget searchSorting dsDyn =
divClass "data__primary" $
tableWrapper (updated searchSorting $> SortingChanged) $ \sortDyn -> do
sorting <- holdDyn Nothing (mergeWith (<|>) [updated sortDyn, updated searchSorting])
@ -230,7 +224,7 @@ activeDeploymentsWidget hReq searchSorting clickedEv dsDyn =
dyn $
emptyDyn <&> \case
False -> do
editEvs <- simpleList dsSortedDyn (activeDeploymentWidget hReq clickedEv)
editEvs <- simpleList dsSortedDyn activeDeploymentWidget
pure $ switchDyn $ leftmost <$> editEvs
True -> do
emptyTableBody noDeploymentsWidget
@ -250,15 +244,11 @@ activeDeploymentWidget ::
, SetRoute t (R Routes) m
, MonadReader ProjectConfig m
) =>
RequestErrorHandler t m ->
-- | Event that carries the clicked DOM element. This event is required by
-- `dropdownWidget'`.
Event t ClickedElement ->
Dynamic t SearchedDeploymentInfo ->
-- | Returns event carrying editable deployment
-- that is required by \"edit deployment\" sidebar.
m (Event t SearchedDeploymentInfo)
activeDeploymentWidget hReq clickedEv dDyn' = do
activeDeploymentWidget dDyn' = do
dDyn <- holdUniqDyn dDyn'
editEvEv <- dyn $
ffor dDyn $ \d@DeploymentFullInfo {..} -> do
@ -270,27 +260,17 @@ activeDeploymentWidget hReq clickedEv dDyn' = do
statusWidget $ constDyn status
el "td" $
divClass "listing" $
forM_ (unDeploymentMetadata metadata) (renderMetadataLink . pure)
for_ (unDeploymentMetadata metadata) (renderMetadataLink . pure)
el "td" $
deploymentOverridesWidgetSearched hReq (deployment ^. field @"deploymentOverrides" . coerced)
overridesWidget (deployment ^. field @"deploymentOverrides" . coerced)
el "td" $
applicationOverridesWidgetSearched
hReq
(deployment ^. field @"deploymentOverrides" . coerced)
(deployment ^. field @"appOverrides" . coerced)
overridesWidget (deployment ^. field @"appOverrides" . coerced)
el "td" $
text $ formatPosixToDate createdAt
el "td" $
text $ formatPosixToDate updatedAt
el "td" $ do
let enabled = not . isPending . recordedStatus $ status
btn =
elAttr
"button"
( "class" =: "drop__handler"
<> "type" =: "button"
)
$ text "Actions"
body = do
btnEditEv <-
actionButton $
@ -321,7 +301,7 @@ activeDeploymentWidget hReq clickedEv dDyn' = do
pure $
leftmost
[ArchiveDeployment <$ btnArcEv, EditDeployment <$ btnEditEv]
dropdownWidget' clickedEv btn body
dropdownWidget body
let archEv = () <$ ffilter (is (_Ctor @"ArchiveDeployment")) dropdownEv
editEv = d <$ ffilter (is (_Ctor @"EditDeployment")) dropdownEv
delEv <- confirmArchivePopup archEv $ do
@ -330,7 +310,7 @@ activeDeploymentWidget hReq clickedEv dDyn' = do
text $ unDeploymentName dName <> " deployment?"
void $ archiveEndpoint (constDyn . Right $ dName) delEv
let route = DashboardRoute :/ Just dName
setRoute $ route <$ domEvent Dblclick linkEl
setRoute $ route <$ domEvent Click linkEl
pure editEv
switchHold never editEvEv
@ -340,14 +320,10 @@ archivedDeploymentsWidget ::
( MonadWidget t m
, SetRoute t (R Routes) m
) =>
RequestErrorHandler t m ->
Dynamic t (Maybe (SortDir DeploymentFullInfo)) ->
-- | Event that carries the clicked DOM element. This event is required by
-- `dropdownWidget'`.
Event t ClickedElement ->
Dynamic t [SearchedDeploymentInfo] ->
m ()
archivedDeploymentsWidget hReq searchSorting clickedEv dsDyn = do
archivedDeploymentsWidget searchSorting dsDyn = do
showDyn <- toggleButton
let classDyn = ffor showDyn $ \case
True -> "data__archive data__archive--open"
@ -364,7 +340,7 @@ archivedDeploymentsWidget hReq searchSorting clickedEv dsDyn = do
void $
simpleList
dsSortedDyn
(archivedDeploymentWidget hReq clickedEv)
archivedDeploymentWidget
True -> emptyTableBody noDeploymentsWidget
-- | Row with archived deployment.
@ -372,11 +348,9 @@ archivedDeploymentWidget ::
( MonadWidget t m
, SetRoute t (R Routes) m
) =>
RequestErrorHandler t m ->
Event t ClickedElement ->
Dynamic t SearchedDeploymentInfo ->
m ()
archivedDeploymentWidget hReq clickedEv dDyn' = do
archivedDeploymentWidget dDyn' = do
dDyn <- holdUniqDyn dDyn'
dyn_ $
ffor dDyn $ \d@DeploymentFullInfo {..} -> do
@ -388,32 +362,22 @@ archivedDeploymentWidget hReq clickedEv dDyn' = do
statusWidget (pure status)
el "td" $ text "..."
el "td" $
deploymentOverridesWidgetSearched hReq (deployment ^. field @"deploymentOverrides" . coerced)
overridesWidget (deployment ^. field @"deploymentOverrides" . coerced)
el "td" $
applicationOverridesWidgetSearched
hReq
(deployment ^. field @"deploymentOverrides" . coerced)
(deployment ^. field @"appOverrides" . coerced)
overridesWidget (deployment ^. field @"appOverrides" . coerced)
el "td" $
text $ formatPosixToDate createdAt
el "td" $
text $ formatPosixToDate updatedAt
el "td" $ do
let btn =
elAttr
"button"
( "class" =: "drop__handler"
<> "type" =: "button"
)
$ text "Actions"
body =
let body =
actionButton
def
{ buttonText = "Restore from archive"
, buttonType = Just ArchiveActionButtonType
}
btnEv <- dropdownWidget' clickedEv btn body
void $ restoreEndpoint (constDyn $ Right $ dName) btnEv
btnEv <- dropdownWidget body
void $ restoreEndpoint (constDyn $ Right $ dName) (btnEv $> ())
let route = DashboardRoute :/ Just dName
setRoute $ route <$ domEvent Dblclick linkEl
@ -561,7 +525,7 @@ tableWrapper ::
(Dynamic t (Maybe (SortDir DeploymentFullInfo)) -> m a) ->
m a
tableWrapper sChanged ma =
divClass "table table--deployments table--clickable table--double-click" $
divClass "table table--deployments table--clickable" $
el "table" $ mdo
((), sDyn') <- runSortableTableGroup sChanged tableHeader
sDyn <- holdDyn Nothing $ Just <$> sDyn'

View File

@ -88,7 +88,7 @@ editDeploymentPopupHeader dname validDyn loadingDyn =
)
divClass "popup__menu drop drop--actions" blank
pure (closeEv, saveEv)
pure (closeEv $> (), saveEv $> ())
-- | The body of the sidebar containing the edit form. Contains a tag field and
-- an override field.

View File

@ -83,7 +83,7 @@ newDeploymentPopupHeader enabledDyn loadingDyn =
True -> Just LoadingLargeButtonType
)
divClass "popup__menu drop drop--actions" blank
pure (closeEv, saveEv)
pure (closeEv $> (), saveEv $> ())
-- | The body of the sidebar contains the creation form. It contains: a name field,
-- a tag field and overrides fields. The name field is validated with the regexp:

View File

@ -1,4 +1,4 @@
resolver: lts-18.2
resolver: lts-18.28
packages:
- octo-cli
@ -10,3 +10,5 @@ extra-deps:
- servant-websockets-2.0.0
- table-layout-0.9.1.0
- data-default-instances-base-0.1.0.1
- git: https://github.com/typeable/ordered-containers.git
commit: 3eb05fb2f44fe482e9092aff447e9c68fbd6a7f6