mirror of
https://github.com/typeable/octopod.git
synced 2024-10-03 18:27:13 +03:00
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:
parent
602ae2314c
commit
83d275df7f
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@ -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
5
.nova/Configuration.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"env-generator" : "nix-shell --run env",
|
||||
"haskell.checkProject" : false,
|
||||
"haskell.formattingProvider" : "fourmolu"
|
||||
}
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,4 @@
|
||||
{- ORMOLU_DISABLE -}
|
||||
{-# LANGUAGE CPP #-}
|
||||
|
||||
module Octopod.API.WebSocket
|
||||
|
@ -113,7 +113,6 @@ library
|
||||
, time
|
||||
, hasql
|
||||
, hasql-transaction
|
||||
, ordered-containers
|
||||
, unordered-containers
|
||||
, vector
|
||||
, process
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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" $
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
165
octopod-common/src/Data/ConfigTree.hs
Normal file
165
octopod-common/src/Data/ConfigTree.hs
Normal 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 #-}
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -61,7 +61,7 @@
|
||||
|
||||
}
|
||||
.table--clickable tbody tr {
|
||||
cursor: default;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table--clickable tbody tr:hover {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) =>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
(%~~) = (%~)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
84
octopod-frontend/src/Frontend/UIKit/Button/Tree.hs
Normal file
84
octopod-frontend/src/Frontend/UIKit/Button/Tree.hs
Normal 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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user