Refactored queries to not be strings (#68)

* Rel8ed backend

* Removed rel8 dependency from common

* Format frontend

* Made frontend compile

* Migrations

* Fixed Actions encoding

* Fixed timeout

* Fixed overrides

* Fixed deployment duration

* Fixed inserting deployment logs

* Removed postgresql-simple

* Fixed octo CLI
This commit is contained in:
iko 2021-08-26 18:14:29 +03:00 committed by GitHub
parent 90fa7b8108
commit 0336964bab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1555 additions and 1522 deletions

View File

@ -35,7 +35,7 @@ let
} }
]; ];
index-state = "2021-07-02T00:00:00Z"; index-state = "2021-08-04T00:00:00Z";
compiler-nix-name = "ghc8105"; compiler-nix-name = "ghc8105";
}; };
in in

View File

@ -0,0 +1,62 @@
-- Deploy octopod:migrate_2.0 to pg
BEGIN;
-- XXX Add DDLs here.
ALTER TABLE deployments ADD COLUMN app_overrides jsonb;
UPDATE deployments AS d SET app_overrides =
( SELECT COALESCE(jsonb_agg(jsonb_build_array("do".key, jsonb_build_object('tag', 'ValueAdded', 'contents', "do".value))), jsonb_build_array())
FROM deployment_overrides as "do"
WHERE d.id = "do".deployment_id AND scope = 'ApplicationScope'
);
ALTER TABLE deployments ALTER COLUMN app_overrides SET NOT NULL;
ALTER TABLE deployments ADD COLUMN deployment_overrides jsonb;
UPDATE deployments AS d SET deployment_overrides =
( SELECT COALESCE(jsonb_agg(jsonb_build_array("do".key, jsonb_build_object('tag', 'ValueAdded', 'contents', "do".value))), jsonb_build_array())
FROM deployment_overrides as "do"
WHERE d.id = "do".deployment_id AND scope = 'DeploymentScope'
);
ALTER TABLE deployments ALTER COLUMN deployment_overrides SET NOT NULL;
ALTER TABLE deployments ADD COLUMN links jsonb;
UPDATE deployments AS d SET links =
( SELECT COALESCE(jsonb_agg(jsonb_build_object('name', "dm".key, 'link', "dm".value)), jsonb_build_array())
FROM deployment_metadata as "dm"
WHERE d.id = "dm".deployment_id
);
ALTER TABLE deployments ALTER COLUMN links SET NOT NULL;
ALTER TABLE deployments ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE deployments ALTER COLUMN "status" TYPE text;
DROP TABLE deployment_overrides;
DROP TABLE deployment_metadata;
ALTER TABLE deployment_logs ALTER COLUMN "action" TYPE text;
ALTER TABLE deployment_logs ALTER COLUMN "duration" DROP DEFAULT;
ALTER TABLE deployment_logs ALTER COLUMN "duration" TYPE interval USING (duration || ' seconds') :: interval;
ALTER TABLE deployment_logs ADD COLUMN deployment_overrides jsonb;
UPDATE deployment_logs AS d SET deployment_overrides =
( SELECT COALESCE(jsonb_agg(jsonb_build_array("do".key, jsonb_build_object('tag', 'ValueAdded', 'contents', "do".value))), jsonb_build_array())
FROM deployment_log_overrides as "do"
WHERE d.id = "do".deployment_log_id AND scope = 'DeploymentScope'
);
ALTER TABLE deployment_logs ALTER COLUMN deployment_overrides SET NOT NULL;
ALTER TABLE deployment_logs ADD COLUMN app_overrides jsonb;
UPDATE deployment_logs AS d SET app_overrides =
( SELECT COALESCE(jsonb_agg(jsonb_build_array("do".key, jsonb_build_object('tag', 'ValueAdded', 'contents', "do".value))), jsonb_build_array())
FROM deployment_log_overrides as "do"
WHERE d.id = "do".deployment_log_id AND scope = 'ApplicationScope'
);
ALTER TABLE deployment_logs ALTER COLUMN app_overrides SET NOT NULL;
DROP TABLE deployment_log_overrides;
COMMIT;

View File

@ -0,0 +1,12 @@
-- Revert octopod:migrate_2.0 from pg
BEGIN;
DO LANGUAGE 'plpgsql'
$$
BEGIN
RAISE EXCEPTION 'Revert not supported for this migration';
END
$$;
COMMIT;

View File

@ -15,3 +15,4 @@ rename-delete-to-archive 2020-11-23T10:53:10Z Ilya <octopod@typeable.io> # Renam
rename_delete_to_archive_2 2020-11-26T08:28:58Z Typeable LLC <octopod@typeable.io> # Renamed delete to archive rename_delete_to_archive_2 2020-11-26T08:28:58Z Typeable LLC <octopod@typeable.io> # Renamed delete to archive
add_detailed_failures 2021-02-04T11:01:15Z Typeable LLC <octopod@typeable.io> # Added more failure states add_detailed_failures 2021-02-04T11:01:15Z Typeable LLC <octopod@typeable.io> # Added more failure states
remove_archived_column 2021-01-28T18:44:54Z Typeable LLC <octopod@typeable.io> # Removed 'archived' column remove_archived_column 2021-01-28T18:44:54Z Typeable LLC <octopod@typeable.io> # Removed 'archived' column
migrate_2.0 2021-08-11T17:09:37Z Typeable LLC <octopod@typeable.io> # Migrate to 2.0-style tables. (Less tables)

View File

@ -0,0 +1,37 @@
-- Verify octopod:migrate_2.0 on pg
BEGIN;
SELECT
id,
name,
tag,
app_overrides,
deployment_overrides,
created_at,
updated_at,
archived_at,
status,
status_updated_at,
checked_at,
links
FROM deployments
WHERE false;
SELECT
id,
deployment_id,
action,
tag,
exit_code,
created_at,
archived,
duration,
stdout,
stderr,
app_overrides,
deployment_overrides
FROM deployment_logs
WHERE false;
ROLLBACK;

View File

@ -44,7 +44,7 @@ executable octo
aeson, aeson,
base, base,
bytestring, bytestring,
chronos, time,
data-default-class, data-default-class,
generic-lens, generic-lens,
http-client-tls, http-client-tls,
@ -59,6 +59,7 @@ executable octo
transformers, transformers,
table-layout ^>= 0.9.0.0, table-layout ^>= 0.9.0.0,
servant-auth, servant-auth,
ordered-containers,
hs-source-dirs: src hs-source-dirs: src
default-language: Haskell2010 default-language: Haskell2010
ghc-options: ghc-options:

View File

@ -2,9 +2,9 @@
module Octopod.CLI (runOcto) where module Octopod.CLI (runOcto) where
import Chronos
import Common.Types import Common.Types
import Common.Utils (dfiName) import Common.Utils (dfiName)
import Control.Exception
import Control.Lens hiding (List) import Control.Lens hiding (List)
import Control.Monad import Control.Monad
import Control.Monad.IO.Class import Control.Monad.IO.Class
@ -14,11 +14,14 @@ import qualified Data.ByteString.Lazy.Char8 as LBSC
import Data.Coerce import Data.Coerce
import Data.Generics.Labels () import Data.Generics.Labels ()
import Data.Generics.Product import Data.Generics.Product
import qualified Data.Map.Ordered.Strict as OM
import Data.Maybe
import Data.Proxy import Data.Proxy
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.IO as T import qualified Data.Text.IO as T
import Data.Text.Lens import Data.Text.Lens
import Data.Time
import GHC.IO.Encoding import GHC.IO.Encoding
import Network.HTTP.Client.TLS import Network.HTTP.Client.TLS
import Octopod.CLI.Args import Octopod.CLI.Args
@ -45,6 +48,9 @@ import Text.Layout.Table
import Text.Layout.Table.Extras () import Text.Layout.Table.Extras ()
import Prelude as P import Prelude as P
dieT :: MonadIO m => Text -> m a
dieT = liftIO . die . T.unpack
-- | Runs the octo CLI. -- | Runs the octo CLI.
runOcto :: IO () runOcto :: IO ()
runOcto = do runOcto = do
@ -58,27 +64,17 @@ runOcto = do
let clientEnv = mkClientEnv manager env let clientEnv = mkClientEnv manager env
flip runReaderT clientEnv $ flip runReaderT clientEnv $
case args of case args of
Create tName tTag tSetAp tSetDep tSetPAp tSetPDep -> do Create tName tTag tSetAp tSetDep -> do
setApp <- liftIO $ parseSetApplicationOverrides Public tSetAp appOvs <- either dieT pure $ parseSetOverrides tSetAp
setDep <- liftIO $ parseSetDeploymentOverrides Public tSetDep depOvs <- either dieT pure $ parseSetOverrides tSetDep
setPApp <- liftIO $ parseSetApplicationOverrides Private tSetPAp
setPDep <- liftIO $ parseSetDeploymentOverrides Private tSetPDep
let appOvs = setApp ++ setPApp
depOvs = setDep ++ setPDep
handleCreate auth $ Deployment (coerce tName) (coerce tTag) appOvs depOvs handleCreate auth $ Deployment (coerce tName) (coerce tTag) appOvs depOvs
List -> handleList auth List -> handleList auth
Archive tName -> handleArchive auth . coerce $ tName Archive tName -> handleArchive auth . coerce $ tName
Update tName tTag tSetAp tUnsAp tSetD tUnsD tSetPAp tSetPD -> do Update tName tTag tSetAp unsetApp tSetD unsetDep -> do
setApp <- liftIO $ parseSetApplicationOverrides Public tSetAp appOvs <- either dieT pure $ parseSetOverrides tSetAp
setDep <- liftIO $ parseSetDeploymentOverrides Public tSetD depOvs <- either dieT pure $ parseSetOverrides tSetD
unsetApp <- liftIO $ parseUnsetApplicationOverrides Public tUnsAp let tName' = coerce tName
unsetDep <- liftIO $ parseUnsetDeploymentOverrides Public tUnsD tTag' = coerce <$> tTag
setPApp <- liftIO $ parseSetApplicationOverrides Private tSetPAp
setPDep <- liftIO $ parseSetDeploymentOverrides Private tSetPD
let appOvs = setApp ++ setPApp
depOvs = setDep ++ setPDep
tName' = coerce tName
tTag' = coerce tTag
handleUpdate auth tName' tTag' appOvs unsetApp depOvs unsetDep handleUpdate auth tName' tTag' appOvs unsetApp depOvs unsetDep
Info tName -> Info tName ->
handleInfo auth . coerce $ tName handleInfo auth . coerce $ tName
@ -130,26 +126,46 @@ handleArchive auth dName = do
handleUpdate :: handleUpdate ::
AuthContext AuthHeaderAuth -> AuthContext AuthHeaderAuth ->
DeploymentName -> DeploymentName ->
DeploymentTag -> Maybe DeploymentTag ->
ApplicationOverrides -> Overrides 'ApplicationLevel ->
ApplicationOverrides -> [Text] ->
DeploymentOverrides -> Overrides 'DeploymentLevel ->
DeploymentOverrides -> [Text] ->
ReaderT ClientEnv IO () ReaderT ClientEnv IO ()
handleUpdate auth dName dTag dNewAppOvs dOldAppOvs dNewDepOvs dOldDepOvs = do handleUpdate auth dName dTag dNewAppOvs removedAppOvs dNewDepOvs removedDepOvs = do
clientEnv <- ask 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
liftIO $ do liftIO $ do
let dUpdate = let dUpdate =
DeploymentUpdate DeploymentUpdate
{ newTag = dTag { newTag = fromMaybe (dep ^. #deployment . #tag) dTag
, newAppOverrides = dNewAppOvs , appOverrides = appOverrides'
, oldAppOverrides = dOldAppOvs , deploymentOverrides = deploymentOverrides'
, newDeploymentOverrides = dNewDepOvs
, oldDeploymentOverrides = dOldDepOvs
} }
response <- runClientM (updateH auth dName dUpdate) clientEnv response <- runClientM (updateH auth dName dUpdate) clientEnv
handleResponse (const $ pure ()) response handleResponse (const $ pure ()) response
runClientM' :: MonadIO m => ClientM a -> ClientEnv -> m a
runClientM' req env = liftIO $ runClientM req env >>= either (die . displayException) pure
-- | Handles the 'info' subcommand. -- | Handles the 'info' subcommand.
handleInfo :: AuthContext AuthHeaderAuth -> DeploymentName -> ReaderT ClientEnv IO () handleInfo :: AuthContext AuthHeaderAuth -> DeploymentName -> ReaderT ClientEnv IO ()
handleInfo auth dName = do handleInfo auth dName = do
@ -192,13 +208,13 @@ handleGetActionInfo auth aId l = do
runClientM (getActionInfoH auth aId) clientEnv >>= \case runClientM (getActionInfoH auth aId) clientEnv >>= \case
Left err -> print err Left err -> print err
Right x -> case l of Right x -> case l of
Out -> T.putStrLn $ x ^. #stdout Out -> T.putStrLn . unStdout $ x ^. #stdout
Err -> T.putStrLn $ x ^. #stderr Err -> T.putStrLn . unStderr $ x ^. #stderr
ErrOut -> do ErrOut -> do
T.putStrLn "\t\tstdout:\n" T.putStrLn "\t\tstdout:\n"
T.putStrLn $ x ^. #stdout T.putStrLn . unStdout $ x ^. #stdout
T.putStrLn "\t\tstderr:\n" T.putStrLn "\t\tstderr:\n"
T.putStrLn $ x ^. #stderr T.putStrLn . unStderr $ x ^. #stderr
listH :: AuthContext AuthHeaderAuth -> ClientM [DeploymentFullInfo] listH :: AuthContext AuthHeaderAuth -> ClientM [DeploymentFullInfo]
createH :: AuthContext AuthHeaderAuth -> Deployment -> ClientM CommandResponse createH :: AuthContext AuthHeaderAuth -> Deployment -> ClientM CommandResponse
@ -260,19 +276,19 @@ decodeError body =
-- | Pretty-prints the 'info' subcommand result. -- | Pretty-prints the 'info' subcommand result.
printInfo :: DeploymentInfo -> IO () printInfo :: DeploymentInfo -> IO ()
printInfo (DeploymentInfo (Deployment _ dTag dAppOvs dStOvs) dMeta dLogs) = do printInfo (DeploymentInfo (Deployment _ dTag dAppOvs dStOvs) (DeploymentMetadata dMeta) dLogs) = do
T.putStrLn "Current settings:" T.putStrLn "Current settings:"
T.putStrLn $ "tag: " <> coerce dTag T.putStrLn $ "tag: " <> coerce dTag
T.putStrLn $ T.putStrLn $
"application overrides: " "application overrides: "
<> (formatOverrides $ coerce <$> dAppOvs) <> formatOverrides dAppOvs
T.putStrLn $ T.putStrLn $
"deployment overrides: " "deployment overrides: "
<> (formatOverrides $ coerce <$> dStOvs) <> formatOverrides dStOvs
T.putStrLn $ "metadata: " T.putStrLn $ "metadata: "
forM_ dMeta $ \m -> forM_ dMeta $ \m ->
T.putStrLn $ T.putStrLn $
" " <> deploymentMetadataKey m <> ": " <> deploymentMetadataValue m " " <> m ^. #name <> ": " <> m ^. #link
T.putStrLn "" T.putStrLn ""
T.putStrLn "Last logs:" T.putStrLn "Last logs:"
ppDeploymentLogs dLogs ppDeploymentLogs dLogs
@ -308,21 +324,13 @@ ppDeploymentLogRow dLog =
colsAllG colsAllG
top top
[ [
[ encode_YmdHMS [ T.pack . formatTime defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S")) $
SubsecondPrecisionAuto dLog ^. field @"createdAt"
w3c
( timeToDatetime . Time . fromIntegral $
dLog ^. field @"createdAt" * 10 ^ (9 :: Int)
)
] ]
, [dLog ^. field @"actionId" . to unActionId . re _Show . packed] , [dLog ^. field @"actionId" . to unActionId . re _Show . packed]
, [dLog ^. field @"action" . coerced] , [dLog ^. field @"action" . to actionToText]
, [dLog ^. field @"deploymentTag" . coerced] , [dLog ^. field @"deploymentTag" . coerced]
, dLog , dLog ^. field @"deploymentAppOverrides" . to formatOverrides'
^. field @"deploymentAppOverrides" , dLog ^. field @"deploymentDepOverrides" . to formatOverrides'
. to (fmap $ formatOverride . coerce)
, dLog
^. field @"deploymentDepOverrides"
. to (fmap $ formatOverride . coerce)
, [dLog ^. field @"exitCode" . re _Show . packed] , [dLog ^. field @"exitCode" . re _Show . packed]
] ]

View File

@ -20,10 +20,6 @@ data Args
setAppOverrides :: [Text] setAppOverrides :: [Text]
, -- | deployment-level overrides to set , -- | deployment-level overrides to set
setDeploymentOverrides :: [Text] setDeploymentOverrides :: [Text]
, -- | application-level private overrides to set
setAppPrivateOverrides :: [Text]
, -- | deployment-level private overrides to set
setDeploymentPrivateOverrides :: [Text]
} }
| List | List
| Archive | Archive
@ -34,7 +30,7 @@ data Args
{ -- | deployment name { -- | deployment name
name :: Text name :: Text
, -- | deployment tag , -- | deployment tag
tag :: Text newTag :: Maybe Text
, -- | application-level overrides to set , -- | application-level overrides to set
setAppOverrides :: [Text] setAppOverrides :: [Text]
, -- | application-level overrides to unset , -- | application-level overrides to unset
@ -43,10 +39,6 @@ data Args
setDeploymentOverrides :: [Text] setDeploymentOverrides :: [Text]
, -- | deployment-level overrides to unset , -- | deployment-level overrides to unset
unsetDeploymentOverrides :: [Text] unsetDeploymentOverrides :: [Text]
, -- | application-level private overrides to set
setAppPrivateOverrides :: [Text]
, -- | deployment-level private overrides to set
setDeploymentPrivateOverrides :: [Text]
} }
| Info | Info
{ -- | deployment name { -- | deployment name
@ -119,22 +111,6 @@ createArgs =
<> help "set deployment level override" <> help "set deployment level override"
) )
) )
<*> many
( strOption
( long "set-app-env-private-override"
<> short 'a'
<> help "set application level private override"
<> internal
)
)
<*> many
( strOption
( long "set-deployment-private-override"
<> short 's'
<> help "set deployment level private override"
<> internal
)
)
-- | Parses arguments of 'list' subcommand. -- | Parses arguments of 'list' subcommand.
listArgs :: Parser Args listArgs :: Parser Args
@ -152,7 +128,7 @@ updateArgs :: Parser Args
updateArgs = updateArgs =
Update Update
<$> strOption (long "name" <> short 'n' <> help "deployment name") <$> strOption (long "name" <> short 'n' <> help "deployment name")
<*> strOption (long "tag" <> short 't' <> help "deployment tag") <*> optional (strOption (long "tag" <> short 't' <> help "deployment tag"))
<*> many <*> many
( strOption ( strOption
( long "set-app-env-override" ( long "set-app-env-override"
@ -181,22 +157,6 @@ updateArgs =
<> help "unset a deployment level override" <> help "unset a deployment level override"
) )
) )
<*> many
( strOption
( long "set-app-env-private-override"
<> short 'a'
<> help "set application level private override"
<> internal
)
)
<*> many
( strOption
( long "set-deployment-private-override"
<> short 's'
<> help "set deployment level private override"
<> internal
)
)
-- | Parses arguments of 'info' subcommand. -- | Parses arguments of 'info' subcommand.
infoArgs :: Parser Args infoArgs :: Parser Args

View File

@ -63,9 +63,9 @@ library
Octopod.Server.Posix Octopod.Server.Posix
Orphans Orphans
Types Types
Database.PostgreSQL.Simple.Instances
Control.Octopod.DeploymentLock Control.Octopod.DeploymentLock
Octopod.PowerAPI.Auth.Server Octopod.PowerAPI.Auth.Server
Octopod.Schema
hs-source-dirs: hs-source-dirs:
src src
build-depends: build-depends:
@ -73,7 +73,6 @@ library
, async , async
, base >=4.7 && <5 , base >=4.7 && <5
, bytestring , bytestring
, chronos
, conduit , conduit
, deriving-aeson , deriving-aeson
, octopod-common , octopod-common
@ -87,7 +86,6 @@ library
, optparse-applicative , optparse-applicative
, optparse-generic , optparse-generic
, postgresql-error-codes , postgresql-error-codes
, postgresql-simple
, resource-pool , resource-pool
, servant , servant
, servant-server , servant-server
@ -107,6 +105,11 @@ library
, servant-auth-server , servant-auth-server
, wai , wai
, jose , jose
, rel8
, time
, hasql
, hasql-transaction
, ordered-containers
default-language: Haskell2010 default-language: Haskell2010
executable octopod-exe executable octopod-exe

View File

@ -1,33 +0,0 @@
{-# OPTIONS_GHC -Wno-orphans #-}
module Database.PostgreSQL.Simple.Instances
(
) where
import Common.Types
import Control.Applicative
import Control.Arrow
import qualified Data.Map as M
import Data.Text (Text)
import Database.PostgreSQL.Simple.FromField
import Database.PostgreSQL.Simple.ToField
import qualified Data.Text.Encoding as T
instance ToField DeploymentStatus where
toField = toField @Text . deploymentStatusText
instance FromField DeploymentStatus where
fromField _ (Just b) =
(either (const empty) pure . T.decodeUtf8' $ b) >>= maybe empty return . flip M.lookup m
where
m = M.fromList . fmap (deploymentStatusText &&& id) $
[ Running
, Failure GenericFailure
, Failure TagMismatch
, Failure PartialAvailability
, CreatePending
, UpdatePending
, ArchivePending
, Archived
]
fromField _ Nothing = empty

View File

@ -0,0 +1,101 @@
module Octopod.Schema
( DeploymentSchema (..),
deploymentSchema,
extractDeployment,
DeploymentLogSchema (..),
deploymentLogSchema,
extractDeploymentLog,
)
where
import Data.Generics.Product
import Data.Int
import Data.Time
import GHC.Generics (Generic)
import Orphans ()
import Rel8
import Types
deploymentSchema :: TableSchema (DeploymentSchema Name)
deploymentSchema =
TableSchema
{ name = "deployments"
, schema = Nothing
, columns =
DeploymentSchema
{ id_ = "id"
, name = "name"
, tag = "tag"
, appOverrides = "app_overrides"
, deploymentOverrides = "deployment_overrides"
, createdAt = "created_at"
, updatedAt = "updated_at"
, archivedAt = "archived_at"
, status = "status"
, statusUpdatedAt = "status_updated_at"
, checkedAt = "checked_at"
, metadata = "links"
}
}
data DeploymentSchema f = DeploymentSchema
{ id_ :: Column f DeploymentId
, name :: Column f DeploymentName
, tag :: Column f DeploymentTag
, appOverrides :: Column f (Overrides 'ApplicationLevel)
, deploymentOverrides :: Column f (Overrides 'DeploymentLevel)
, createdAt :: Column f UTCTime
, updatedAt :: Column f UTCTime
, archivedAt :: Column f (Maybe UTCTime)
, status :: Column f DeploymentStatus
, statusUpdatedAt :: Column f UTCTime
, checkedAt :: Column f UTCTime
, metadata :: Column f DeploymentMetadata
}
deriving stock (Generic)
deriving anyclass (Rel8able)
extractDeployment :: DeploymentSchema Result -> Deployment
extractDeployment = upcast
data DeploymentLogSchema f = DeploymentLogSchema
{ actionId :: Column f ActionId
, deploymentId :: Column f DeploymentId
, action :: Column f Action
, deploymentTag :: Column f DeploymentTag
, exitCode :: Column f Int64
, createdAt :: Column f UTCTime
, archived :: Column f Bool
, duration :: Column f Duration
, stdout :: Column f Stdout
, stderr :: Column f Stderr
, deploymentAppOverrides :: Column f (Overrides 'ApplicationLevel)
, deploymentDepOverrides :: Column f (Overrides 'DeploymentLevel)
}
deriving stock (Generic)
deriving anyclass (Rel8able)
deploymentLogSchema :: TableSchema (DeploymentLogSchema Name)
deploymentLogSchema =
TableSchema
{ name = "deployment_logs"
, schema = Nothing
, columns =
DeploymentLogSchema
{ actionId = "id"
, deploymentId = "deployment_id"
, action = "action"
, deploymentTag = "tag"
, exitCode = "exit_code"
, createdAt = "created_at"
, archived = "archived"
, duration = "duration"
, stdout = "stdout"
, stderr = "stderr"
, deploymentAppOverrides = "app_overrides"
, deploymentDepOverrides = "deployment_overrides"
}
}
extractDeploymentLog :: DeploymentLogSchema Result -> DeploymentLog
extractDeploymentLog = upcast

File diff suppressed because it is too large Load Diff

View File

@ -15,19 +15,18 @@ module Octopod.Server.ControlScriptUtils
tagCheckCommandArgs, tagCheckCommandArgs,
-- * Helpers -- * Helpers
applicationOverrideToArg, fullConfigArgs,
applicationOverridesToArgs, overridesArgs,
deploymentOverrideToArg,
deploymentOverridesToArgs,
) )
where where
import Control.Lens
import Control.Monad.Base import Control.Monad.Base
import Control.Monad.Reader import Control.Monad.Reader
import qualified Data.ByteString.Lazy as TL import qualified Data.ByteString.Lazy as TL
import Data.Coerce import Data.Coerce
import Data.Generics.Product.Typed import Data.Generics.Product.Typed
import Data.Text (Text) import qualified Data.Map.Ordered.Strict as MO
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
import Octopod.Server.Logger import Octopod.Server.Logger
@ -43,9 +42,10 @@ infoCommandArgs ::
, HasType ProjectName r , HasType ProjectName r
, HasType Domain r , HasType Domain r
) => ) =>
FullDefaultConfig ->
Deployment -> Deployment ->
m ControlScriptArgs m ControlScriptArgs
infoCommandArgs dep = do infoCommandArgs dCfg dep = do
(Namespace namespace) <- asks getTyped (Namespace namespace) <- asks getTyped
(ProjectName projectName) <- asks getTyped (ProjectName projectName) <- asks getTyped
(Domain domain) <- asks getTyped (Domain domain) <- asks getTyped
@ -58,12 +58,11 @@ infoCommandArgs dep = do
, "--namespace" , "--namespace"
, T.unpack . coerce $ namespace , T.unpack . coerce $ namespace
, "--name" , "--name"
, T.unpack . coerce $ name dep , T.unpack . coerce $ dep ^. #name
, "--tag" , "--tag"
, T.unpack . coerce $ tag dep , T.unpack . coerce $ tag dep
] ]
<> getApplicationOverrideArgs dep <> fullConfigArgs dCfg dep
<> getDeploymentOverrideArgs dep
notificationCommandArgs :: notificationCommandArgs ::
( MonadReader r m ( MonadReader r m
@ -95,9 +94,9 @@ notificationCommandArgs dName dTag old new = do
, "--tag" , "--tag"
, T.unpack . coerce $ dTag , T.unpack . coerce $ dTag
, "--old-status" , "--old-status"
, T.unpack $ deploymentStatusText old , T.unpack $ deploymentStatusToText old
, "--new-status" , "--new-status"
, T.unpack $ deploymentStatusText new , T.unpack $ deploymentStatusToText new
] ]
checkCommandArgs :: checkCommandArgs ::
@ -106,9 +105,10 @@ checkCommandArgs ::
, HasType ProjectName r , HasType ProjectName r
, HasType Domain r , HasType Domain r
) => ) =>
FullDefaultConfig ->
Deployment -> Deployment ->
m ControlScriptArgs m ControlScriptArgs
checkCommandArgs dep = do checkCommandArgs dCfg dep = do
(Namespace namespace) <- asks getTyped (Namespace namespace) <- asks getTyped
(ProjectName projectName) <- asks getTyped (ProjectName projectName) <- asks getTyped
(Domain domain) <- asks getTyped (Domain domain) <- asks getTyped
@ -121,12 +121,11 @@ checkCommandArgs dep = do
, "--namespace" , "--namespace"
, T.unpack . coerce $ namespace , T.unpack . coerce $ namespace
, "--name" , "--name"
, T.unpack . coerce $ name dep , T.unpack . coerce $ dep ^. #name
, "--tag" , "--tag"
, T.unpack . coerce $ tag dep , T.unpack . coerce $ tag dep
] ]
<> getApplicationOverrideArgs dep <> fullConfigArgs dCfg dep
<> getDeploymentOverrideArgs dep
tagCheckCommandArgs :: tagCheckCommandArgs ::
( MonadReader r m ( MonadReader r m
@ -134,9 +133,10 @@ tagCheckCommandArgs ::
, HasType ProjectName r , HasType ProjectName r
, HasType Domain r , HasType Domain r
) => ) =>
FullDefaultConfig ->
Deployment -> Deployment ->
m ControlScriptArgs m ControlScriptArgs
tagCheckCommandArgs dep = do tagCheckCommandArgs dCfg dep = do
(Namespace namespace) <- asks getTyped (Namespace namespace) <- asks getTyped
(ProjectName projectName) <- asks getTyped (ProjectName projectName) <- asks getTyped
(Domain domain) <- asks getTyped (Domain domain) <- asks getTyped
@ -149,12 +149,11 @@ tagCheckCommandArgs dep = do
, "--namespace" , "--namespace"
, T.unpack . coerce $ namespace , T.unpack . coerce $ namespace
, "--name" , "--name"
, T.unpack . coerce $ name dep , T.unpack . coerce $ dep ^. #name
, "--tag" , "--tag"
, T.unpack . coerce $ tag dep , T.unpack . coerce $ tag dep
] ]
<> getApplicationOverrideArgs dep <> fullConfigArgs dCfg dep
<> getDeploymentOverrideArgs dep
archiveCheckArgs :: archiveCheckArgs ::
( MonadReader r m ( MonadReader r m
@ -219,27 +218,18 @@ runCommandWithoutPipes :: FilePath -> [String] -> IO ExitCode
runCommandWithoutPipes cmd args = runCommandWithoutPipes cmd args =
withProcessWait (proc cmd args) waitExitCode withProcessWait (proc cmd args) waitExitCode
-- | Converts an application-level override list to command arguments. fullConfigArgs :: FullDefaultConfig -> Deployment -> ControlScriptArgs
applicationOverrideToArg :: ApplicationOverride -> [Text] fullConfigArgs defCfg dep =
applicationOverrideToArg o = ["--app-env-override", overrideToArg . coerce $ o] overridesArgs (applyOverrides (dep ^. #appOverrides) (appDefaultConfig defCfg))
<> overridesArgs (applyOverrides (dep ^. #deploymentOverrides) (depDefaultConfig defCfg))
-- | Helper to convert an application-level override to command arguments. overridesArgs :: forall l. KnownOverrideLevel l => Config l -> ControlScriptArgs
applicationOverridesToArgs :: ApplicationOverrides -> [Text] overridesArgs (Config cc) =
applicationOverridesToArgs ovs = concat [applicationOverrideToArg o | o <- ovs] ControlScriptArgs
. concatMap (\(T.unpack -> k, T.unpack -> v) -> [argumentName, k <> "=" <> v])
getApplicationOverrideArgs :: Deployment -> ControlScriptArgs . MO.assocs
getApplicationOverrideArgs = $ cc
ControlScriptArgs . map T.unpack . applicationOverridesToArgs . appOverrides where
argumentName = case knownOverrideLevel @l of
-- | Converts a deployment-level override list to command arguments. ApplicationLevel -> "--app-env-override"
deploymentOverrideToArg :: DeploymentOverride -> [Text] DeploymentLevel -> "--deployment-override"
deploymentOverrideToArg o =
["--deployment-override", overrideToArg . coerce $ o]
-- | Helper to convert a deployment-level override to command arguments.
deploymentOverridesToArgs :: DeploymentOverrides -> [Text]
deploymentOverridesToArgs ovs = concat [deploymentOverrideToArg o | o <- ovs]
getDeploymentOverrideArgs :: Deployment -> ControlScriptArgs
getDeploymentOverrideArgs =
ControlScriptArgs . map T.unpack . deploymentOverridesToArgs . deploymentOverrides

View File

@ -3,17 +3,48 @@
module Orphans () where module Orphans () where
import Common.Types import Common.Types
import Database.PostgreSQL.Simple.FromField (FromField) import Data.Maybe
import Database.PostgreSQL.Simple.ToField (ToField) import Rel8
deriving newtype instance FromField Action parseTypeInformationFromMapping :: (Eq a, Eq b, DBType b, Show b, Show a) => [(a, b)] -> TypeInformation a
parseTypeInformationFromMapping m =
parseTypeInformation
(\v -> maybe (Left $ "unknown value: " <> show v) Right . flip lookup reversedM $ v)
(\v -> fromMaybe (error $ "forgot case: " <> show v) . flip lookup m $ v)
typeInformation
where
reversedM = (\(x, y) -> (y, x)) <$> m
deriving newtype instance ToField Action deriving via JSONBEncoded (Overrides l) instance (DBType (Overrides l))
deriving newtype instance FromField DeploymentTag deriving newtype instance DBType DeploymentId
deriving newtype instance DBEq DeploymentId
deriving newtype instance ToField DeploymentTag deriving newtype instance DBType DeploymentName
deriving newtype instance DBEq DeploymentName
deriving newtype instance FromField DeploymentName deriving newtype instance DBType DeploymentTag
deriving newtype instance DBEq DeploymentTag
deriving newtype instance ToField DeploymentName instance DBType Action where
typeInformation = parseTypeInformationFromMapping actionText
deriving newtype instance DBType ArchivedFlag
deriving newtype instance DBType Duration
deriving newtype instance DBType Timestamp
deriving newtype instance DBType ProjectName
deriving anyclass instance DBEq DeploymentStatus
instance DBType DeploymentStatus where
typeInformation = parseTypeInformationFromMapping deploymentStatusText
deriving via JSONBEncoded DeploymentMetadata instance DBType DeploymentMetadata
deriving newtype instance DBType Stdout
deriving newtype instance DBType Stderr
deriving newtype instance DBType ActionId
deriving newtype instance DBEq ActionId

View File

@ -25,15 +25,16 @@ import Data.Text as T
import Data.Traversable import Data.Traversable
import Common.Types import Common.Types
import Data.Time
-- | Parses deployment metadata. -- | Parses deployment metadata.
parseDeploymentMetadata :: [Text] -> IO [DeploymentMetadata] parseDeploymentMetadata :: [Text] -> IO DeploymentMetadata
parseDeploymentMetadata texts = parseDeploymentMetadata texts = fmap DeploymentMetadata $
for texts $ \t -> for texts $ \t ->
case T.findIndex (== ',') t of case T.findIndex (== ',') t of
Just i -> do Just i -> do
let (key, value) = bimap strip (T.tail . strip) $ T.splitAt i t let (key, value) = bimap strip (T.tail . strip) $ T.splitAt i t
pure $ DeploymentMetadata key value pure $ DeploymentMetadatum key value
Nothing -> Nothing ->
error $ error $
"Malformed metadata key-value pair " <> T.unpack t "Malformed metadata key-value pair " <> T.unpack t
@ -60,11 +61,11 @@ newtype Namespace = Namespace {unNamespace :: Text}
deriving stock (Show) deriving stock (Show)
-- | Archive retention. -- | Archive retention.
newtype ArchiveRetention = ArchiveRetention {unArchiveRetention :: Int} newtype ArchiveRetention = ArchiveRetention {unArchiveRetention :: NominalDiffTime}
deriving stock (Show) deriving stock (Show)
-- | Timeout. -- | Timeout.
newtype Timeout = Timeout {unTimeout :: Int} newtype Timeout = Timeout {unTimeout :: CalendarDiffTime}
deriving stock (Show) deriving stock (Show)
-- | Path to a deployment control script. -- | Path to a deployment control script.

View File

@ -18,6 +18,7 @@ library
exposed-modules: Common.Types exposed-modules: Common.Types
, Common.Utils , Common.Utils
, Common.Validation , Common.Validation
, Data.Map.Ordered.Strict.Extra
-- other-modules: -- other-modules:
-- other-extensions: -- other-extensions:
default-extensions: BlockArguments default-extensions: BlockArguments
@ -35,6 +36,16 @@ library
, TypeApplications , TypeApplications
, TypeOperators , TypeOperators
, ViewPatterns , ViewPatterns
, DeriveAnyClass
, OverloadedLabels
, StandaloneDeriving
, TypeSynonymInstances
, FlexibleInstances
, DataKinds
, KindSignatures
, EmptyDataDeriving
, TupleSections
, AllowAmbiguousTypes
build-depends: base build-depends: base
, aeson , aeson
, bytestring , bytestring
@ -43,6 +54,9 @@ library
, http-api-data , http-api-data
, lens , lens
, text , text
, containers
, time
, ordered-containers
hs-source-dirs: src hs-source-dirs: src
default-language: Haskell2010 default-language: Haskell2010
ghc-options: ghc-options:
@ -56,3 +70,4 @@ library
-Wno-missing-local-signatures -Wno-missing-local-signatures
-Wno-partial-fields -Wno-partial-fields
-Wno-unsafe -Wno-unsafe
-Wno-missed-specialisations

View File

@ -7,55 +7,87 @@
-- This module contains common types between the backend and the frontend. -- This module contains common types between the backend and the frontend.
module Common.Types where module Common.Types where
import Data.Bifunctor import Control.Lens
import Data.Coerce import Data.Aeson hiding (Result)
import Data.String import Data.Generics.Labels ()
import Data.Text as T hiding (filter) 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.Maybe
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time
import Data.Traversable import Data.Traversable
import Deriving.Aeson
import Deriving.Aeson.Stock import Deriving.Aeson.Stock
import Web.HttpApiData import Web.HttpApiData
-- | Deployment override. data OverrideLevel = ApplicationLevel | DeploymentLevel
data Override = Override
{ overrideKey :: Text class KnownOverrideLevel (l :: OverrideLevel) where
, overrideValue :: Text knownOverrideLevel :: OverrideLevel
, overrideVisibility :: OverrideVisibility
instance KnownOverrideLevel 'ApplicationLevel where
knownOverrideLevel = ApplicationLevel
instance KnownOverrideLevel 'DeploymentLevel where
knownOverrideLevel = DeploymentLevel
data OverrideValue = ValueAdded Text | ValueDeleted
deriving (ToJSON, FromJSON) via Snake OverrideValue
deriving stock (Eq, Ord, Show, Generic)
newtype DefaultConfig (l :: OverrideLevel) = DefaultConfig (OMap Text Text)
deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
newtype Config (l :: OverrideLevel) = Config {unConfig :: OMap Text Text}
deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
data FullDefaultConfig = FullDefaultConfig
{ appDefaultConfig :: DefaultConfig 'ApplicationLevel
, depDefaultConfig :: DefaultConfig 'DeploymentLevel
} }
deriving stock (Generic, Show, Eq) deriving stock (Show, Ord, Eq, Generic)
deriving (FromJSON, ToJSON) via Snake Override deriving (ToJSON, FromJSON) via Snake FullDefaultConfig
type Overrides = [Override] data FullConfig = FullConfig
{ appConfig :: Config 'ApplicationLevel
, depConfig :: Config 'DeploymentLevel
}
deriving stock (Show, Ord, Eq, Generic)
deriving (ToJSON, FromJSON) via Snake FullConfig
-- | Deployment override scope. applyOverrides :: Overrides l -> DefaultConfig l -> Config l
data OverrideScope applyOverrides (Overrides oo) (DefaultConfig dd) =
= ApplicationScope Config . extract $ oo <>| (ValueAdded <$> dd)
| DeploymentScope where
deriving stock (Generic, Show, Read, Eq) extract :: OMap Text OverrideValue -> OMap Text Text
deriving (FromJSON, ToJSON) via Snake OverrideScope extract =
fmap
( \case
ValueAdded v -> v
ValueDeleted -> error "invariant"
)
. OM.filter
( \_ -> \case
ValueAdded _ -> True
ValueDeleted -> False
)
-- | Deployment override visibility. newtype Overrides (l :: OverrideLevel) = Overrides {unOverrides :: OMap Text OverrideValue}
data OverrideVisibility deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
= Private
| Public
deriving stock (Generic, Show, Read, Eq)
deriving (FromJSON, ToJSON) via Snake OverrideVisibility
-- | Deployment application-level override. ov :: Text -> OverrideValue -> Overrides l
newtype ApplicationOverride = ApplicationOverride {unApplicationOverride :: Override} ov k v = Overrides $ OM.singleton (k, v)
deriving newtype (Show, Eq, FromJSON, ToJSON)
-- | Deployment application-level overrides. instance Semigroup (Overrides l) where
type ApplicationOverrides = [ApplicationOverride] (Overrides lhs) <> (Overrides rhs) = Overrides $ rhs <>| lhs
-- | Deployment-level override. instance Monoid (Overrides l) where
newtype DeploymentOverride = DeploymentOverride mempty = Overrides OM.empty
{unDeploymentOverride :: Override}
deriving newtype (Show, Eq, FromJSON, ToJSON)
-- | Deployment-level overrides. newtype DeploymentId = DeploymentId {unDeploymentId :: Int64}
type DeploymentOverrides = [DeploymentOverride]
newtype DeploymentId = DeploymentId {unDeploymentId :: Int}
deriving stock (Show) deriving stock (Show)
newtype DeploymentName = DeploymentName {unDeploymentName :: Text} newtype DeploymentName = DeploymentName {unDeploymentName :: Text}
@ -66,30 +98,47 @@ newtype DeploymentTag = DeploymentTag {unDeploymentTag :: Text}
deriving newtype deriving newtype
(Show, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq) (Show, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq)
newtype Action = Action {unAction :: Text} data Action = RestoreAction | ArchiveAction | UpdateAction | CreateAction
deriving newtype (Show, FromJSON, ToJSON, IsString) deriving stock (Show, Read, Eq, Ord, Generic)
deriving (FromJSON, ToJSON) via Snake Action
actionText :: [(Action, Text)]
actionText =
[ (RestoreAction, "restore")
, (ArchiveAction, "archive")
, (UpdateAction, "update")
, (CreateAction, "create")
]
actionToText :: Action -> Text
actionToText k = fromMaybe (error $ "forgot case: " <> show k) . Prelude.lookup k $ actionText
newtype ArchivedFlag = ArchivedFlag {unArchivedFlag :: Bool} newtype ArchivedFlag = ArchivedFlag {unArchivedFlag :: Bool}
deriving newtype (Show, FromJSON, ToJSON) deriving newtype (Show, FromJSON, ToJSON)
newtype Duration = Duration {unDuration :: Int} newtype Duration = Duration {unDuration :: CalendarDiffTime}
deriving newtype (Show, FromJSON, ToJSON) deriving newtype (Show, Eq, FromJSON, ToJSON, FormatTime)
newtype Timestamp = Timestamp {unTimestamp :: Int} newtype Timestamp = Timestamp {unTimestamp :: CalendarDiffTime}
deriving newtype (Show, Eq, Ord, FromJSON, ToJSON) deriving newtype (Show, Eq, FromJSON, ToJSON)
newtype ProjectName = ProjectName {uProjectName :: Text} newtype ProjectName = ProjectName {uProjectName :: Text}
deriving newtype (Show, FromJSON, ToJSON) deriving newtype (Show, FromJSON, ToJSON)
deploymentStatusText :: DeploymentStatus -> Text deploymentStatusText :: [(DeploymentStatus, Text)]
deploymentStatusText Running = "Running" deploymentStatusText =
deploymentStatusText (Failure GenericFailure) = "GenericFailure" [ (Running, "Running")
deploymentStatusText (Failure TagMismatch) = "TagMismatch" , (Failure GenericFailure, "GenericFailure")
deploymentStatusText (Failure PartialAvailability) = "PartialAvailability" , (Failure TagMismatch, "TagMismatch")
deploymentStatusText CreatePending = "CreatePending" , (Failure PartialAvailability, "PartialAvailability")
deploymentStatusText UpdatePending = "UpdatePending" , (CreatePending, "CreatePending")
deploymentStatusText ArchivePending = "ArchivePending" , (UpdatePending, "UpdatePending")
deploymentStatusText Archived = "Archived" , (ArchivePending, "ArchivePending")
, (Archived, "Archived")
]
deploymentStatusToText :: DeploymentStatus -> Text
deploymentStatusToText k = fromMaybe (error $ "forgot case: " <> show k) . Prelude.lookup k $ deploymentStatusText
data DeploymentStatus data DeploymentStatus
= Running = Running
@ -124,8 +173,8 @@ isArchivedStatus = (`elem` archivedStatuses)
data Deployment = Deployment data Deployment = Deployment
{ name :: DeploymentName { name :: DeploymentName
, tag :: DeploymentTag , tag :: DeploymentTag
, appOverrides :: ApplicationOverrides , appOverrides :: Overrides 'ApplicationLevel
, deploymentOverrides :: DeploymentOverrides , deploymentOverrides :: Overrides 'DeploymentLevel
} }
deriving stock (Generic, Show, Eq) deriving stock (Generic, Show, Eq)
deriving (FromJSON, ToJSON) via Snake Deployment deriving (FromJSON, ToJSON) via Snake Deployment
@ -134,27 +183,30 @@ data DeploymentLog = DeploymentLog
{ actionId :: ActionId { actionId :: ActionId
, action :: Action , action :: Action
, deploymentTag :: DeploymentTag , deploymentTag :: DeploymentTag
, deploymentAppOverrides :: ApplicationOverrides , deploymentAppOverrides :: Overrides 'ApplicationLevel
, deploymentDepOverrides :: DeploymentOverrides , deploymentDepOverrides :: Overrides 'DeploymentLevel
, exitCode :: Int , exitCode :: Int64
, duration :: Duration , duration :: Duration
, createdAt :: Int , createdAt :: UTCTime
}
deriving stock (Generic, Show)
deriving (FromJSON, ToJSON) via Snake DeploymentLog
data DeploymentMetadata = DeploymentMetadata
{ -- | The name of the link
deploymentMetadataKey :: Text
, -- | The URL
deploymentMetadataValue :: Text
} }
deriving stock (Generic, Show, Eq) deriving stock (Generic, Show, Eq)
deriving (FromJSON, ToJSON) via Snake DeploymentMetadata deriving (ToJSON, FromJSON) via Snake DeploymentLog
newtype DeploymentMetadata = DeploymentMetadata {unDeploymentMetadata :: [DeploymentMetadatum]}
deriving newtype (Eq, Show, Ord, FromJSON, ToJSON)
data DeploymentMetadatum = DeploymentMetadatum
{ -- | The name of the link
name :: Text
, -- | The URL
link :: Text
}
deriving stock (Generic, Show, Eq, Ord)
deriving (FromJSON, ToJSON) via Snake DeploymentMetadatum
data DeploymentInfo = DeploymentInfo data DeploymentInfo = DeploymentInfo
{ deployment :: Deployment { deployment :: Deployment
, metadata :: [DeploymentMetadata] , metadata :: DeploymentMetadata
, logs :: [DeploymentLog] , logs :: [DeploymentLog]
} }
deriving stock (Generic, Show) deriving stock (Generic, Show)
@ -163,13 +215,21 @@ data DeploymentInfo = DeploymentInfo
data DeploymentFullInfo = DeploymentFullInfo data DeploymentFullInfo = DeploymentFullInfo
{ deployment :: Deployment { deployment :: Deployment
, status :: PreciseDeploymentStatus , status :: PreciseDeploymentStatus
, metadata :: [DeploymentMetadata] , metadata :: DeploymentMetadata
, createdAt :: Int , createdAt :: UTCTime
, updatedAt :: Int , updatedAt :: UTCTime
, deploymentDefaultConfig :: FullDefaultConfig
} }
deriving stock (Generic, Show, Eq) deriving stock (Generic, Show, Eq)
deriving (FromJSON, ToJSON) via Snake DeploymentFullInfo deriving (FromJSON, ToJSON) via Snake DeploymentFullInfo
getDeploymentConfig :: DeploymentFullInfo -> FullConfig
getDeploymentConfig d =
FullConfig
{ appConfig = applyOverrides (d ^. #deployment . #appOverrides) (d ^. #deploymentDefaultConfig . #appDefaultConfig)
, depConfig = applyOverrides (d ^. #deployment . #deploymentOverrides) (d ^. #deploymentDefaultConfig . #depDefaultConfig)
}
isDeploymentArchived :: DeploymentFullInfo -> Bool isDeploymentArchived :: DeploymentFullInfo -> Bool
isDeploymentArchived DeploymentFullInfo {status = s} = case s of isDeploymentArchived DeploymentFullInfo {status = s} = case s of
DeploymentNotPending s' -> isArchivedStatus s' DeploymentNotPending s' -> isArchivedStatus s'
@ -179,27 +239,12 @@ isDeploymentArchived DeploymentFullInfo {status = s} = case s of
data DeploymentUpdate = DeploymentUpdate data DeploymentUpdate = DeploymentUpdate
{ newTag :: DeploymentTag { newTag :: DeploymentTag
, newAppOverrides :: ApplicationOverrides , appOverrides :: Overrides 'ApplicationLevel
, oldAppOverrides :: ApplicationOverrides , deploymentOverrides :: Overrides 'DeploymentLevel
, newDeploymentOverrides :: DeploymentOverrides
, oldDeploymentOverrides :: DeploymentOverrides
} }
deriving stock (Generic, Show) deriving stock (Generic, Show)
deriving (FromJSON, ToJSON) via Snake DeploymentUpdate deriving (FromJSON, ToJSON) via Snake DeploymentUpdate
applyDeploymentUpdate :: DeploymentUpdate -> Deployment -> Deployment
applyDeploymentUpdate du d =
Deployment
{ name = name d
, tag = newTag du
, appOverrides =
filter (`notElem` oldAppOverrides du) (appOverrides d)
<> newAppOverrides du
, deploymentOverrides =
filter (`notElem` oldDeploymentOverrides du) (deploymentOverrides d)
<> newDeploymentOverrides du
}
data CurrentStatus data CurrentStatus
= Ok = Ok
| Error | Error
@ -225,7 +270,7 @@ data WSEvent = FrontendPleaseUpdateEverything
deriving stock (Generic, Show) deriving stock (Generic, Show)
deriving (FromJSON, ToJSON) via Snake WSEvent deriving (FromJSON, ToJSON) via Snake WSEvent
newtype ActionId = ActionId {unActionId :: Int} newtype ActionId = ActionId {unActionId :: Int64}
deriving newtype deriving newtype
(Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq) (Show, Read, FromJSON, ToJSON, ToHttpApiData, FromHttpApiData, Eq)
@ -238,74 +283,36 @@ newtype Stderr = Stderr {unStderr :: Text}
deriving (FromJSON, ToJSON) via Snake Stderr deriving (FromJSON, ToJSON) via Snake Stderr
data ActionInfo = ActionInfo data ActionInfo = ActionInfo
{ stdout :: Text { stdout :: Stdout
, stderr :: Text , stderr :: Stderr
} }
deriving stock (Generic, Show) deriving stock (Generic, Show)
deriving (FromJSON, ToJSON) via Snake ActionInfo deriving (FromJSON, ToJSON) via Snake ActionInfo
-- | Parses setting application-level overrides.
parseSetApplicationOverrides ::
OverrideVisibility ->
[Text] ->
IO [ApplicationOverride]
parseSetApplicationOverrides visibility texts =
coerce <$> parseSetOverrides visibility texts
-- | Parses setting deployment-level overrides.
parseSetDeploymentOverrides ::
OverrideVisibility ->
[Text] ->
IO [DeploymentOverride]
parseSetDeploymentOverrides visibility texts =
coerce <$> parseSetOverrides visibility texts
-- | Parses setting overrides. -- | Parses setting overrides.
parseSetOverrides :: OverrideVisibility -> [Text] -> IO [Override] parseSetOverrides :: [Text] -> Either Text (Overrides l)
parseSetOverrides visibility texts = parseSetOverrides texts = do
for texts $ \t -> pairs' <- for texts $ \text -> case parseSingleOverride text of
case T.findIndex (== '=') t of Just x -> Right x
Just i -> do
let (key, value) = bimap strip (T.tail . strip) $ T.splitAt i t
pure $ Override key value visibility
Nothing -> Nothing ->
error $ Left $ "Malformed override key-value pair " <> text <> ", should be similar to FOO=bar"
"Malformed override key-value pair " <> T.unpack t return . Overrides $ OM.fromList pairs'
<> ", should be similar to FOO=bar" where
parseSingleOverride :: Text -> Maybe (Text, OverrideValue)
parseSingleOverride t
| Just i <- T.findIndex (== '=') t =
let (key, value) = bimap T.strip (T.tail . T.strip) $ T.splitAt i t
in Just (key, ValueAdded value)
parseSingleOverride _ = Nothing
-- | Parses unsetting application-level overrides. parseUnsetOverrides :: [Text] -> Overrides l
parseUnsetApplicationOverrides :: parseUnsetOverrides = Overrides . OM.fromList . fmap (,ValueDeleted)
OverrideVisibility ->
[Text] ->
IO [ApplicationOverride]
parseUnsetApplicationOverrides visibility texts =
coerce <$> parseUnsetOverrides visibility texts
-- | Parses unsetting deployment-level overrides. formatOverrides :: Overrides l -> Text
parseUnsetDeploymentOverrides :: formatOverrides = T.unlines . formatOverrides'
OverrideVisibility ->
[Text] ->
IO [DeploymentOverride]
parseUnsetDeploymentOverrides visibility texts =
coerce <$> parseUnsetOverrides visibility texts
-- | Parses unsetting overrides. formatOverrides' :: Overrides l -> [Text]
parseUnsetOverrides :: OverrideVisibility -> [Text] -> IO [Override] formatOverrides' (Overrides m) = fmap (\(k, v) -> k <> "=" <> showValue v) . OM.assocs $ m
parseUnsetOverrides visibility texts = where
for texts $ \key -> showValue (ValueAdded v) = v
pure $ Override key "" visibility showValue ValueDeleted = "<removed>"
-- | Creates pretty-printed text from override.
formatOverride :: Override -> Text
formatOverride o@(Override _ _ vis) =
overrideToArg o <> case vis of
Private -> " (" <> pack (show vis) <> ")"
Public -> mempty
-- | Creates pretty-printed texts from overrides.
formatOverrides :: Overrides -> Text
formatOverrides = T.unlines . fmap formatOverride
-- | Creates a CLI argument from an override.
overrideToArg :: Override -> Text
overrideToArg (Override k v _) = k <> "=" <> v

View File

@ -36,7 +36,7 @@ dfiName ::
(DeploymentName -> f DeploymentName) -> (DeploymentName -> f DeploymentName) ->
DeploymentFullInfo -> DeploymentFullInfo ->
f DeploymentFullInfo f DeploymentFullInfo
dfiName = field @"deployment" . field @"name" dfiName = field @"deployment" . field' @"name"
-- | Checks that deployment status is pending. -- | Checks that deployment status is pending.
isPending :: DeploymentStatus -> Bool isPending :: DeploymentStatus -> Bool

View File

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

View File

@ -83,6 +83,7 @@ executable frontend
, FunctionalDependencies , FunctionalDependencies
, AllowAmbiguousTypes , AllowAmbiguousTypes
, OverloadedLabels , OverloadedLabels
, ViewPatterns
build-depends: aeson build-depends: aeson
, base , base
, bytestring , bytestring
@ -104,5 +105,6 @@ executable frontend
, mtl , mtl
, semialign , semialign
, these , these
, ordered-containers
hs-source-dirs: src hs-source-dirs: src
default-language: Haskell2010 default-language: Haskell2010

View File

@ -8,15 +8,23 @@
--frontend modules. --frontend modules.
module Frontend.Utils where module Frontend.Utils where
import Common.Types as CT
import Control.Lens import Control.Lens
import Control.Monad import Control.Monad
import Control.Monad.Reader
import Data.Functor
import Data.Generics.Labels () import Data.Generics.Labels ()
import qualified Data.List as L
import Data.Map (Map) import Data.Map (Map)
import qualified Data.Map as M
import qualified Data.Map.Ordered.Strict as OM
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Monoid
import Data.Proxy (Proxy (..)) import Data.Proxy (Proxy (..))
import Data.Text as T (Text, pack) import Data.Text as T (Text, null, pack)
import Data.Time import Data.Time
import Data.Time.Clock.POSIX import Data.Time.Clock.POSIX
import Frontend.GHCJS
import GHCJS.DOM import GHCJS.DOM
import GHCJS.DOM.Element as DOM import GHCJS.DOM.Element as DOM
import GHCJS.DOM.EventM (on, target) import GHCJS.DOM.EventM (on, target)
@ -24,10 +32,6 @@ import GHCJS.DOM.GlobalEventHandlers as Events (click)
import GHCJS.DOM.Node as DOM import GHCJS.DOM.Node as DOM
import Reflex.Dom as R import Reflex.Dom as R
import Common.Types as CT
import Control.Monad.Reader
import Frontend.GHCJS
-- | Wrapper for @Maybe DOM.Element@. It's used by 'elementClick'. -- | Wrapper for @Maybe DOM.Element@. It's used by 'elementClick'.
newtype ClickedElement = ClickedElement {unClickedElement :: Maybe DOM.Element} newtype ClickedElement = ClickedElement {unClickedElement :: Maybe DOM.Element}
@ -374,18 +378,13 @@ elDynAttrWithModifyConfig' f elementTag attrs child = do
pure result pure result
-- | Formats posix seconds to date in iso8601. -- | Formats posix seconds to date in iso8601.
formatPosixToDate :: Int -> Text formatPosixToDate :: FormatTime t => t -> Text
formatPosixToDate = formatPosixToDate = pack . formatTime defaultTimeLocale (iso8601DateFormat Nothing)
pack
. formatTime defaultTimeLocale (iso8601DateFormat Nothing)
. intToUTCTime
-- | Formats posix seconds to date in iso8601 with time. -- | Formats posix seconds to date in iso8601 with time.
formatPosixToDateTime :: Int -> Text formatPosixToDateTime :: FormatTime t => t -> Text
formatPosixToDateTime = formatPosixToDateTime =
pack pack . formatTime defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S"))
. formatTime defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S"))
. intToUTCTime
-- | Widget displaying the current deployment status. -- | Widget displaying the current deployment status.
statusWidget :: MonadWidget t m => Dynamic t PreciseDeploymentStatus -> m () statusWidget :: MonadWidget t m => Dynamic t PreciseDeploymentStatus -> m ()
@ -487,9 +486,9 @@ errorCommonWidget =
overridesWidget :: overridesWidget ::
MonadWidget t m => MonadWidget t m =>
-- | List of overrides. -- | List of overrides.
Overrides -> Overrides l ->
m () m ()
overridesWidget envs = divClass "listing listing--for-text" $ do overridesWidget (Overrides (OM.assocs -> envs)) = divClass "listing listing--for-text" $ do
let visible = take 3 envs let visible = take 3 envs
envLength = length envs envLength = length envs
listing visible listing visible
@ -512,10 +511,12 @@ overridesWidget envs = divClass "listing listing--for-text" $ do
blank blank
where where
listing envs' = do listing envs' = do
forM_ envs' $ \(Override var val _) -> forM_ envs' $ \(var, val) ->
divClass "listing__item" $ do divClass "listing__item" $ do
el "b" $ text $ var <> ": " el "b" $ text $ var <> ": "
text val case val of
ValueAdded v -> text v
ValueDeleted -> el "i" $ text "<deleted>"
-- | @if-then-else@ helper for cases when bool value is wrapped in 'Dynamic'. -- | @if-then-else@ helper for cases when bool value is wrapped in 'Dynamic'.
ifThenElseDyn :: ifThenElseDyn ::
@ -572,3 +573,87 @@ kubeDashboardUrl deploymentInfo = do
template <- asks kubernetesDashboardUrlTemplate template <- asks kubernetesDashboardUrlTemplate
let name = unDeploymentName . view (#deployment . #name) <$> deploymentInfo let name = unDeploymentName . view (#deployment . #name) <$> deploymentInfo
return $ name <&> (\n -> (<> n) <$> template) return $ name <&> (\n -> (<> n) <$> template)
-- | Widget with override fields. This widget supports adding and
-- removing key-value pairs.
envVarsInput ::
forall l t m.
MonadWidget t m =>
-- | Overrides header.
Text ->
-- | Current deployment overrides.
Overrides l ->
-- | Updated deployment overrides.
m (Dynamic t (Overrides l))
envVarsInput overridesHeader (Overrides evs) = do
elClass "section" "deployment__section" $ do
elClass "h3" "deployment__sub-heading" $ text overridesHeader
elClass "div" "deployment__widget" $
elClass "div" "overrides" $ mdo
let initEnvs =
L.foldl'
( \m -> \case
(k, ValueAdded v) -> fst $ insertUniq (k, v) m
(_, ValueDeleted) -> m
)
emptyUniqKeyMap
. OM.assocs
$ evs
toOverrides :: [Override] -> Overrides l
toOverrides = Overrides . OM.fromList . (fmap . fmap) ValueAdded
emptyVar = ("", "")
addEv = clickEv $> Endo (fst . insertUniq emptyVar)
envsDyn <- foldDyn appEndo initEnvs $ leftmost [addEv, updEv]
(_, updEv) <- runEventWriterT $ listWithKey (uniqMap <$> envsDyn) envVarInput
let addingIsEnabled = all ((not . T.null) . fst) . elemsUniq <$> envsDyn
clickEv <-
buttonClassEnabled'
"overrides__add dash dash--add"
"Add an override"
addingIsEnabled
"dash--disabled"
pure $ toOverrides . elemsUniq <$> envsDyn
-- | Widget for entering a key-value pair. The updated overrides list is
-- written to the 'EventWriter'.
envVarInput ::
(EventWriter t (Endo (UniqKeyMap Override)) m, MonadWidget t m) =>
-- | Index of variable in overrides list.
Int ->
-- | Current variable key and value.
Dynamic t Override ->
m ()
envVarInput i epDyn = do
ep <- sample $ current epDyn
divClass "overrides__item" $ do
(keyDyn, _) <-
octopodTextInput' "overrides__key" "key" (Just $ fst ep) never
(valDyn, _) <-
octopodTextInput' "overrides__value" "value" (Just $ snd ep) never
closeEv <- buttonClass "overrides__delete spot spot--cancel" "Delete"
let envEv = updated $ zipDynWith (,) keyDyn valDyn
deleteEv = Endo (deleteUniq i) <$ closeEv
updEv = Endo . updateUniq i . const <$> envEv
tellEvent $ leftmost [deleteEv, updEv]
data UniqKeyMap v = UniqKeyMap (Map Int v) Int
uniqMap :: UniqKeyMap v -> Map Int v
uniqMap (UniqKeyMap m _) = m
insertUniq :: v -> UniqKeyMap v -> (UniqKeyMap v, Int)
insertUniq v (UniqKeyMap m x) = (UniqKeyMap (M.insert x v m) (x + 1), x)
deleteUniq :: Int -> UniqKeyMap v -> UniqKeyMap v
deleteUniq k (UniqKeyMap m x) = UniqKeyMap (M.delete k m) x
updateUniq :: Int -> (v -> v) -> UniqKeyMap v -> UniqKeyMap v
updateUniq k f (UniqKeyMap m x) = UniqKeyMap (M.adjust f k m) x
elemsUniq :: UniqKeyMap v -> [v]
elemsUniq (UniqKeyMap m _) = M.elems m
emptyUniqKeyMap :: UniqKeyMap v
emptyUniqKeyMap = UniqKeyMap mempty 0
type Override = (Text, Text)

View File

@ -22,6 +22,7 @@ import Frontend.Utils (errorCommonWidget, loadingCommonWidget)
import Page.Deployment import Page.Deployment
import Page.Deployments import Page.Deployments
main :: IO () main :: IO ()
main = mdo main = mdo
mainWidgetWithHead' mainWidgetWithHead'

View File

@ -1,17 +1,15 @@
{-| -- |
Module : Page.Deployment --Module : Page.Deployment
Description : Deployment page. --Description : Deployment page.
--
This module contains the definition of a deployment page. --This module contains the definition of a deployment page.
-}
module Page.Deployment (deploymentPage) where module Page.Deployment (deploymentPage) where
import Control.Lens import Control.Lens
import Control.Monad import Control.Monad
import Data.Coerce import Data.Coerce
import Data.Generics.Product (field) import Data.Generics.Product (field)
import Data.Text as T (Text) import Data.Text as T (Text, pack)
import Obelisk.Route.Frontend import Obelisk.Route.Frontend
import Reflex.Dom as R import Reflex.Dom as R
import Servant.Reflex import Servant.Reflex
@ -21,6 +19,8 @@ import Common.Utils
import Control.Monad.Reader import Control.Monad.Reader
import Data.Align import Data.Align
import Data.Generics.Labels () import Data.Generics.Labels ()
import qualified Data.Map.Ordered.Strict as OM
import Data.Time
import Frontend.API import Frontend.API
import Frontend.GHCJS import Frontend.GHCJS
import Frontend.Route import Frontend.Route
@ -33,50 +33,57 @@ import Servant.Reflex.Extra
-- | The root widget of a deployment page. It requests the deployment data. -- | The root widget of a deployment page. It requests the deployment data.
-- If the request fails it shows an error, -- If the request fails it shows an error,
-- otherwise it calls 'deploymentWidget', passing the received data. -- otherwise it calls 'deploymentWidget', passing the received data.
deploymentPage deploymentPage ::
::
( MonadWidget t m ( MonadWidget t m
, RouteToUrl (R Routes) m , RouteToUrl (R Routes) m
, SetRoute t (R Routes) m , SetRoute t (R Routes) m
, Prerender js t m , Prerender js t m
, MonadReader ProjectConfig m , MonadReader ProjectConfig m
) ) =>
=> Event t () -- ^ Event notifying about the need to update data. -- | Event notifying about the need to update data.
-> DeploymentName -- ^ Name of current deployment. Event t () ->
-> m () -- | Name of current deployment.
DeploymentName ->
m ()
deploymentPage updAllEv dname = do deploymentPage updAllEv dname = do
pb <- getPostBuild pb <- getPostBuild
respEv <- fullInfoEndpoint (constDyn $ Right dname) pb respEv <- fullInfoEndpoint (constDyn $ Right dname) pb
let (okEv, errEv) = processResp respEv let (okEv, errEv) = processResp respEv
widgetHold_ (loadingWidget dname) $ leftmost widgetHold_ (loadingWidget dname) $
leftmost
[ errorWidget dname <$ errEv [ errorWidget dname <$ errEv
, deploymentWidget updAllEv <$> okEv ] , deploymentWidget updAllEv <$> okEv
]
-- | Deployment page widget that takes the initial deployment data. -- | Deployment page widget that takes the initial deployment data.
-- It updates this data every time when the passed event fires. -- It updates this data every time when the passed event fires.
-- If an update fails, a notification widget appears at the top of the page. -- If an update fails, a notification widget appears at the top of the page.
deploymentWidget deploymentWidget ::
::
( MonadWidget t m ( MonadWidget t m
, RouteToUrl (R Routes) m , RouteToUrl (R Routes) m
, SetRoute t (R Routes) m , SetRoute t (R Routes) m
, Prerender js t m , Prerender js t m
, MonadReader ProjectConfig m , MonadReader ProjectConfig m
) ) =>
=> Event t () -- ^ Event notifying about the need to update data. -- | Event notifying about the need to update data.
-> DeploymentFullInfo -- ^ Initial deployment data. Event t () ->
-> m () -- | Initial deployment data.
DeploymentFullInfo ->
m ()
deploymentWidget updEv dfi = mdo deploymentWidget updEv dfi = mdo
editEv <- pageWrapper $ mdo editEv <- pageWrapper $ mdo
retryEv <- delay 10 errEv retryEv <- delay 10 errEv
respEv <- fullInfoEndpoint (constDyn $ Right $ dfi ^. dfiName) respEv <-
$ leftmost [ updEv, retryEv ] fullInfoEndpoint (constDyn $ Right $ dfi ^. dfiName) $
leftmost [updEv, retryEv]
let (okEv, errEv) = processResp respEv let (okEv, errEv) = processResp respEv
dfiDyn <- holdDyn dfi okEv dfiDyn <- holdDyn dfi okEv
editEv' <- deploymentHead dfiDyn sentEv editEv' <- deploymentHead dfiDyn sentEv
pageNotification $ leftmost pageNotification $
leftmost
[ DPMError "Couldn't update status of deployment" <$ errEv [ DPMError "Couldn't update status of deployment" <$ errEv
, DPMClear <$ okEv ] , DPMClear <$ okEv
]
deploymentBody updEv dfiDyn deploymentBody updEv dfiDyn
pure editEv' pure editEv'
sentEv <- editDeploymentPopup editEv never sentEv <- editDeploymentPopup editEv never
@ -89,23 +96,27 @@ deploymentWidget updEv dfi = mdo
-- and \"edit deployment\" buttons. -- and \"edit deployment\" buttons.
-- If the status is pending (\"Creating\", \"Updating\", etc) -- If the status is pending (\"Creating\", \"Updating\", etc)
-- then all buttons are inactive. -- then all buttons are inactive.
deploymentHead deploymentHead ::
:: (MonadWidget t m, MonadReader ProjectConfig m) (MonadWidget t m, MonadReader ProjectConfig m) =>
=> Dynamic t DeploymentFullInfo -- | Deployment data.
-- ^ Deployment data. Dynamic t DeploymentFullInfo ->
-> Event t Bool -- | Event with a flag showing the current state of the request.
-- ^ Event with a flag showing the current state of the request. Event t Bool ->
-> m (Event t DeploymentFullInfo) -- | \"Edit\" event.
-- ^ \"Edit\" event. m (Event t DeploymentFullInfo)
deploymentHead dfiDyn sentEv = deploymentHead dfiDyn sentEv =
divClass "page__head" $ do divClass "page__head" $ do
let dname = dfiDyn <^.> dfiName . coerced let dname = dfiDyn <^.> dfiName . coerced
elClass "h1" "page__heading title" $ dynText dname elClass "h1" "page__heading title" $ dynText dname
(editEv, archEv) <- hold2 . dyn $ dfiDyn <&> \dfi -> if isDeploymentArchived dfi (editEv, archEv) <-
hold2 . dyn $
dfiDyn <&> \dfi ->
if isDeploymentArchived dfi
then mdo then mdo
let btnState = not $ isPending . recordedStatus $ dfi ^. field @"status" let btnState = not $ isPending . recordedStatus $ dfi ^. field @"status"
btnEnabledDyn <- holdDyn btnState $ leftmost [ False <$ btnEv, sentEv ] btnEnabledDyn <- holdDyn btnState $ leftmost [False <$ btnEv, sentEv]
btnEv <- aButtonClassEnabled btnEv <-
aButtonClassEnabled
"page__action button button--secondary button--restore \ "page__action button button--secondary button--restore \
\classic-popup-handler" \classic-popup-handler"
"Recover from archive" "Recover from archive"
@ -115,12 +126,14 @@ deploymentHead dfiDyn sentEv =
else mdo else mdo
let btnState = not $ isPending . recordedStatus $ dfi ^. field @"status" let btnState = not $ isPending . recordedStatus $ dfi ^. field @"status"
btnEnabledDyn <- holdDyn btnState $ not <$> sentEv btnEnabledDyn <- holdDyn btnState $ not <$> sentEv
editEv <- buttonClassEnabled' editEv <-
buttonClassEnabled'
"page__action button button--edit popup-handler" "page__action button button--edit popup-handler"
"Edit deployment" "Edit deployment"
btnEnabledDyn btnEnabledDyn
"button--disabled" "button--disabled"
archEv <- buttonClassEnabled' archEv <-
buttonClassEnabled'
"page__action button button--secondary button--archive \ "page__action button button--secondary button--archive \
\classic-popup-handler" \classic-popup-handler"
"Move to archive" "Move to archive"
@ -128,8 +141,13 @@ deploymentHead dfiDyn sentEv =
"button--disabled" "button--disabled"
pure (R.tag (current dfiDyn) editEv, archEv) pure (R.tag (current dfiDyn) editEv, archEv)
url' <- kubeDashboardUrl dfiDyn url' <- kubeDashboardUrl dfiDyn
void . dyn $ url' <&> maybe blank (\url -> void . dyn $
void $ aButtonDynClass' url'
<&> maybe
blank
( \url ->
void $
aButtonDynClass'
"page__action button button--secondary button--logs" "page__action button button--secondary button--logs"
"Details" "Details"
(pure $ "href" =: url <> "target" =: "_blank") (pure $ "href" =: url <> "target" =: "_blank")
@ -142,10 +160,10 @@ deploymentHead dfiDyn sentEv =
void $ archiveEndpoint (Right . coerce <$> dname) delEv void $ archiveEndpoint (Right . coerce <$> dname) delEv
return editEv return editEv
hold2 hold2 ::
:: (MonadHold t m, Reflex t) (MonadHold t m, Reflex t) =>
=> m (Event t (Event t a, Event t b)) m (Event t (Event t a, Event t b)) ->
-> m (Event t a, Event t b) m (Event t a, Event t b)
hold2 = (>>= fmap fanThese . switchHold never . fmap (uncurry align)) hold2 = (>>= fmap fanThese . switchHold never . fmap (uncurry align))
-- | Div wrappers. -- | Div wrappers.
@ -153,15 +171,16 @@ deploymentBodyWrapper :: MonadWidget t m => m a -> m a
deploymentBodyWrapper m = divClass "page__body" $ divClass "deployment" $ m deploymentBodyWrapper m = divClass "page__body" $ divClass "deployment" $ m
-- | Body of a deployment page. -- | Body of a deployment page.
deploymentBody deploymentBody ::
:: MonadWidget t m MonadWidget t m =>
=> Event t () -- | Event notifying about the need to update data.
-- ^ Event notifying about the need to update data. Event t () ->
-> Dynamic t DeploymentFullInfo -- | Deployment data.
-- ^ Deployment data. Dynamic t DeploymentFullInfo ->
-> m () m ()
deploymentBody updEv dfiDyn = deploymentBodyWrapper $ do deploymentBody updEv dfiDyn = deploymentBodyWrapper $ do
let nameDyn = dfiDyn <^.> dfiName let nameDyn = dfiDyn <^.> dfiName
cfg = dfiDyn <&> getDeploymentConfig
divClass "deployment__summary" $ do divClass "deployment__summary" $ do
divClass "deployment__stat" $ do divClass "deployment__stat" $ do
elClass "b" "deployment__param" $ text "Status" elClass "b" "deployment__param" $ text "Status"
@ -182,20 +201,16 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $ do
elClass "h3" "deployment__sub-heading" $ text "Tag" elClass "h3" "deployment__sub-heading" $ text "Tag"
divClass "deployment__widget" $ dynText tagDyn divClass "deployment__widget" $ dynText tagDyn
elClass "section" "deployment__section" $ do elClass "section" "deployment__section" $ do
let urlsDyn = dfiDyn <^.> field @"metadata" let urlsDyn = dfiDyn <^.> field @"metadata" . to unDeploymentMetadata
elClass "h3" "deployment__sub-heading" $ text "Links" elClass "h3" "deployment__sub-heading" $ text "Links"
divClass "deployment__widget" $ divClass "deployment__widget" $
divClass "listing" $ divClass "listing" $
void $ simpleList urlsDyn renderMetadataLink void $ simpleList urlsDyn renderMetadataLink
elClass "section" "deployment__section" $ do elClass "section" "deployment__section" $ do
let let envsDyn = cfg <^.> #appConfig
envsDyn = dfiDyn <^.> field @"deployment"
. field @"appOverrides" . coerced
allEnvsWidget "App overrides" envsDyn allEnvsWidget "App overrides" envsDyn
elClass "section" "deployment__section" $ do elClass "section" "deployment__section" $ do
let let envsDyn = cfg <^.> #depConfig
envsDyn = dfiDyn <^.> field @"deployment"
. field @"deploymentOverrides" . coerced
allEnvsWidget "Deployment overrides" envsDyn allEnvsWidget "Deployment overrides" envsDyn
elClass "section" "deployment__section" $ do elClass "section" "deployment__section" $ do
elClass "h3" "deployment__sub-heading" $ text "Actions" elClass "h3" "deployment__sub-heading" $ text "Actions"
@ -204,46 +219,49 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $ do
actionsTable updEv nameDyn actionsTable updEv nameDyn
-- | Widget that shows overrides list. It does not depend on their type. -- | Widget that shows overrides list. It does not depend on their type.
allEnvsWidget allEnvsWidget ::
:: MonadWidget t m MonadWidget t m =>
=> Text -- ^ Widget header. -- | Widget header.
-> Dynamic t Overrides -- ^ Overrides list. Text ->
-> m () -- | Overrides list.
Dynamic t (Config l) ->
m ()
allEnvsWidget headerText envsDyn = do allEnvsWidget headerText envsDyn = do
elClass "h3" "deployment__sub-heading" $ text headerText elClass "h3" "deployment__sub-heading" $ text headerText
divClass "deployment__widget" $ divClass "deployment__widget" $
divClass "listing listing--for-text listing--larger" $ divClass "listing listing--for-text listing--larger" $
void $ simpleList envsDyn $ \envDyn -> do void $
let simpleList (OM.assocs . unConfig <$> envsDyn) $ \envDyn -> do
varDyn = overrideKey <$> envDyn let varDyn = fst <$> envDyn
valDyn = overrideValue <$> envDyn valDyn = snd <$> envDyn
divClass "listing__item" $ do divClass "listing__item" $ do
el "b" $ do el "b" $ do
dynText varDyn dynText varDyn
text ": " text ": "
dynText valDyn dynText valDyn
-- ^ Widget with a table of actions that can be performed on a deployment. -- ^ Widget with a table of actions that can be performed on a deployment.
-- It requests deployment data. -- It requests deployment data.
-- If a request fails it shows an error message, -- If a request fails it shows an error message,
-- otherwise it calls 'actionsTableData', passing the received data. -- otherwise it calls 'actionsTableData', passing the received data.
actionsTable
:: MonadWidget t m actionsTable ::
=> Event t () MonadWidget t m =>
-- ^ Event notifying about the need to update data. -- | Event notifying about the need to update data.
-> Dynamic t DeploymentName Event t () ->
-> m () Dynamic t DeploymentName ->
m ()
actionsTable updEv nameDyn = do actionsTable updEv nameDyn = do
pb <- getPostBuild pb <- getPostBuild
respEv <- infoEndpoint (Right <$> nameDyn) pb respEv <- infoEndpoint (Right <$> nameDyn) pb
let let okEv = join . fmap logs <$> fmapMaybe reqSuccess respEv
okEv = join . fmap logs <$> fmapMaybe reqSuccess respEv
errEv = fmapMaybe reqErrorBody respEv errEv = fmapMaybe reqErrorBody respEv
el "table" $ do el "table" $ do
actionsTableHead actionsTableHead
widgetHold_ actionsTableLoading $ leftmost widgetHold_ actionsTableLoading $
leftmost
[ actionsTableError <$ errEv [ actionsTableError <$ errEv
, actionsTableData updEv nameDyn <$> okEv ] , actionsTableData updEv nameDyn <$> okEv
]
-- | Header of the actions table. -- | Header of the actions table.
actionsTableHead :: MonadWidget t m => m () actionsTableHead :: MonadWidget t m => m ()
@ -268,7 +286,7 @@ actionsTableLoading = do
text "Loading..." text "Loading..."
-- | Widget with an error message for the actions table. -- | Widget with an error message for the actions table.
actionsTableError:: MonadWidget t m => m () actionsTableError :: MonadWidget t m => m ()
actionsTableError = do actionsTableError = do
el "tbody" $ el "tbody" $
elClass "tr" "no-table" $ elClass "tr" "no-table" $
@ -279,73 +297,68 @@ actionsTableError = do
-- | Actions table body. -- | Actions table body.
-- It updates data every time when the supplied event fires. -- It updates data every time when the supplied event fires.
actionsTableData actionsTableData ::
:: MonadWidget t m MonadWidget t m =>
=> Event t () -- | Event notifying about the need to update data.
-- ^ Event notifying about the need to update data. Event t () ->
-> Dynamic t DeploymentName Dynamic t DeploymentName ->
-> [DeploymentLog] -- | Initial logs.
-- ^ Initial logs. [DeploymentLog] ->
-> m () m ()
actionsTableData updEv nameDyn initLogs = do actionsTableData updEv nameDyn initLogs = do
respEv <- infoEndpoint (Right <$> nameDyn) updEv respEv <- infoEndpoint (Right <$> nameDyn) updEv
let let okEv = join . fmap logs <$> fmapMaybe reqSuccess respEv
okEv = join . fmap logs <$> fmapMaybe reqSuccess respEv
logsDyn <- holdDyn initLogs okEv logsDyn <- holdDyn initLogs okEv
el "tbody" $ el "tbody" $
void $ simpleList logsDyn $ \logDyn -> do void $
simpleList logsDyn $ \logDyn -> do
dyn_ $ actinRow <$> logDyn dyn_ $ actinRow <$> logDyn
-- | Data row of the actions table. -- | Data row of the actions table.
actinRow :: MonadWidget t m => DeploymentLog -> m () actinRow :: MonadWidget t m => DeploymentLog -> m ()
actinRow DeploymentLog{..} = do actinRow DeploymentLog {..} = do
el "tr" $ do el "tr" $ do
el "td" $ do el "td" $ do
text $ coerce action text $ actionToText action
let let statusClass =
statusClass = "status " <> "status "
if exitCode == 0 then "status--success" else "status--failure" <> if exitCode == 0 then "status--success" else "status--failure"
divClass statusClass blank divClass statusClass blank
el "td" $ text $ coerce deploymentTag el "td" $ text $ coerce deploymentTag
el "td" $ overridesWidget $ coerce $ deploymentAppOverrides el "td" $ overridesWidget $ deploymentAppOverrides
el "td" $ overridesWidget $ coerce $ deploymentDepOverrides el "td" $ overridesWidget $ deploymentDepOverrides
el "td" $ text $ showT $ exitCode el "td" $ text $ showT $ exitCode
el "td" $ text $ formatPosixToDateTime createdAt el "td" $ text $ formatPosixToDateTime createdAt
el "td" $ text $ formatDuration duration el "td" $ text $ formatDuration duration
-- | Formats posix seconds to date in iso8601 with time.
formatDuration :: FormatTime t => t -> Text
formatDuration = pack . formatTime defaultTimeLocale "%mm %Ss"
-- | Convert the duration of an action from milliseconds -- | Convert the duration of an action from milliseconds
-- to a human readable format. -- to a human readable format.
formatDuration
:: Duration -- ^ Duration in milliseconds.
-> Text
formatDuration (Duration d) = m <> "m " <> s <> "s"
where
m = showT $ d `div` (1000 * 60)
s = showT $ d `div` (1000)
-- | Widget with a button that returns to deployments list page. -- | Widget with a button that returns to deployments list page.
backButton backButton ::
::
( MonadWidget t m ( MonadWidget t m
, RouteToUrl (R Routes) m , RouteToUrl (R Routes) m
, SetRoute t (R Routes) m , SetRoute t (R Routes) m
, Prerender js t m ) , Prerender js t m
=> m () ) =>
m ()
backButton = do backButton = do
let let backRoute = constDyn $ DashboardRoute :/ Nothing
backRoute = constDyn $ DashboardRoute :/ Nothing
attrs = constDyn $ "class" =: "page__back dash dash--back dash--smaller" attrs = constDyn $ "class" =: "page__back dash dash--back dash--smaller"
routeLinkDynAttr attrs backRoute $ text "All deployments" routeLinkDynAttr attrs backRoute $ text "All deployments"
-- | Widget with a loading spinner. -- | Widget with a loading spinner.
loadingWidget loadingWidget ::
::
( MonadWidget t m ( MonadWidget t m
, RouteToUrl (R Routes) m , RouteToUrl (R Routes) m
, SetRoute t (R Routes) m , SetRoute t (R Routes) m
, Prerender js t m) , Prerender js t m
=> DeploymentName ) =>
-> m () DeploymentName ->
m ()
loadingWidget dname = pageWrapper $ do loadingWidget dname = pageWrapper $ do
divClass "page__head" $ divClass "page__head" $
elClass "h1" "page__heading title" $ text $ coerce dname elClass "h1" "page__heading title" $ text $ coerce dname
@ -354,14 +367,14 @@ loadingWidget dname = pageWrapper $ do
loadingCommonWidget loadingCommonWidget
-- | Widget with an error placeholder. -- | Widget with an error placeholder.
errorWidget errorWidget ::
::
( MonadWidget t m ( MonadWidget t m
, RouteToUrl (R Routes) m , RouteToUrl (R Routes) m
, SetRoute t (R Routes) m , SetRoute t (R Routes) m
, Prerender js t m) , Prerender js t m
=> DeploymentName ) =>
-> m () DeploymentName ->
m ()
errorWidget dname = pageWrapper $ do errorWidget dname = pageWrapper $ do
divClass "page__head" $ divClass "page__head" $
elClass "h1" "page__heading title" $ text $ coerce dname elClass "h1" "page__heading title" $ text $ coerce dname
@ -370,14 +383,15 @@ errorWidget dname = pageWrapper $ do
errorCommonWidget errorCommonWidget
-- | Div wrappers. -- | Div wrappers.
pageWrapper pageWrapper ::
::
( MonadWidget t m ( MonadWidget t m
, RouteToUrl (R Routes) m , RouteToUrl (R Routes) m
, SetRoute t (R Routes) m , SetRoute t (R Routes) m
, Prerender js t m) , Prerender js t m
=> m a ) =>
-> m a m a ->
pageWrapper m = divClass "page" $ divClass "page__wrap container" $ do m a
pageWrapper m = divClass "page" $
divClass "page__wrap container" $ do
backButton backButton
m m

View File

@ -292,7 +292,9 @@ activeDeploymentWidget clickedEv dDyn' = do
el "td" $ do el "td" $ do
name name
statusWidget $ constDyn status statusWidget $ constDyn status
el "td" $ divClass "listing" $ forM_ metadata (renderMetadataLink . pure) el "td" $
divClass "listing" $
forM_ (unDeploymentMetadata metadata) (renderMetadataLink . pure)
el "td" tag' el "td" tag'
el "td" $ el "td" $
overridesWidget $ deployment ^. field @"appOverrides" . coerced overridesWidget $ deployment ^. field @"appOverrides" . coerced

View File

@ -1,23 +1,26 @@
module Page.Elements.Links module Page.Elements.Links
( renderMetadataLink ( renderMetadataLink,
) where )
where
import Common.Types import Common.Types
import Data.Functor import Control.Lens
import qualified Data.Text as T import qualified Data.Text as T
import Reflex.Dom import Reflex.Dom
renderMetadataLink renderMetadataLink ::
:: (DomBuilder t m, PostBuild t m) (DomBuilder t m, PostBuild t m) =>
=> Dynamic t DeploymentMetadata -> m () Dynamic t DeploymentMetadatum ->
m ()
renderMetadataLink metadataD = do renderMetadataLink metadataD = do
let let attrDyn =
attrDyn = metadataD <&> \metadata -> metadataD <&> \metadata ->
"class" =: "listing__item external bar bar--larger" "class" =: "listing__item external bar bar--larger"
<> "href" =: deploymentMetadataValue metadata <> "href" =: metadata ^. #link
<> "target" =: "_blank" <> "target" =: "_blank"
elDynAttr "a" attrDyn . dynText $ metadataD <&> \case elDynAttr "a" attrDyn . dynText $
metadataD <&> \case
-- If the name is empty, then use the url -- If the name is empty, then use the url
DeploymentMetadata {deploymentMetadataKey = name} DeploymentMetadatum {name = name}
| (not . T.null . T.strip) name -> name | (not . T.null . T.strip) name -> name
DeploymentMetadata {deploymentMetadataValue = url} -> url DeploymentMetadatum {link = url} -> url

View File

@ -1,10 +1,8 @@
{-| -- |
Module : Page.Popup.EditDeployment --Module : Page.Popup.EditDeployment
Description : Edit deployment sidebar. --Description : Edit deployment sidebar.
--
This module contains the definition of the "edit deployment" sidebar. --This module contains the definition of the "edit deployment" sidebar.
-}
module Page.Popup.EditDeployment (editDeploymentPopup) where module Page.Popup.EditDeployment (editDeploymentPopup) where
import Control.Lens (coerced, preview, to, (^.), _2) import Control.Lens (coerced, preview, to, (^.), _2)
@ -13,13 +11,10 @@ import Data.Coerce
import Data.Functor import Data.Functor
import Data.Generics.Product import Data.Generics.Product
import Data.Generics.Sum import Data.Generics.Sum
import Data.List (deleteFirstsBy)
import qualified Data.List as L
import Data.Map as M
import Data.Monoid import Data.Monoid
import qualified Data.Text as T import qualified Data.Text as T
import Reflex.Dom as R hiding (mapMaybe)
import Prelude as P import Prelude as P
import Reflex.Dom as R
import Common.Types import Common.Types
import Common.Utils import Common.Utils
@ -30,61 +25,69 @@ import Servant.Reflex
import Servant.Reflex.Extra import Servant.Reflex.Extra
-- | The root function for \"edit deployment\" sidebar. -- | The root function for \"edit deployment\" sidebar.
editDeploymentPopup editDeploymentPopup ::
:: MonadWidget t m MonadWidget t m =>
=> Event t DeploymentFullInfo -- | \"Show\" event carrying an editable sidebar.
-- ^ \"Show\" event carrying an editable sidebar. Event t DeploymentFullInfo ->
-> Event t () -- | \"Close\" event.
-- ^ \"Close\" event. Event t () ->
-> m (Event t Bool) -- | Event with a flag showing the current state of the request.
-- ^ Event with a flag showing the current state of the request. m (Event t Bool)
editDeploymentPopup showEv hideEv = sidebar showEv hideEv $ \dfi -> mdo editDeploymentPopup showEv hideEv = sidebar showEv hideEv $ \dfi -> mdo
divClass "popup__body" $ mdo divClass "popup__body" $ mdo
let dname = dfi ^. dfiName let dname = dfi ^. dfiName
(closeEv', saveEv) <- editDeploymentPopupHeader dname enabledDyn (closeEv', saveEv) <- editDeploymentPopupHeader dname enabledDyn
(deploymentDyn, validDyn) <- editDeploymentPopupBody dfi respEv (deploymentDyn, validDyn) <- editDeploymentPopupBody dfi respEv
respEv <- updateEndpoint (constDyn $ Right dname) respEv <-
(Right <$> deploymentDyn) saveEv updateEndpoint
sentDyn <- holdDyn False $ leftmost (constDyn $ Right dname)
(Right <$> deploymentDyn)
saveEv
sentDyn <-
holdDyn False $
leftmost
[ True <$ saveEv [ True <$ saveEv
, False <$ respEv ] , False <$ respEv
let ]
successEv = let successEv =
fmapMaybe (preview (_Ctor @"Success") <=< commandResponse) respEv fmapMaybe (preview (_Ctor @"Success") <=< commandResponse) respEv
closeEv = leftmost [ closeEv', successEv ] closeEv = leftmost [closeEv', successEv]
enabledDyn = zipDynWith (&&) (not <$> sentDyn) validDyn enabledDyn = zipDynWith (&&) (not <$> sentDyn) validDyn
pure (updated sentDyn, closeEv) pure (updated sentDyn, closeEv)
-- | The header of the sidebar contains the deployment name and control buttons: -- | The header of the sidebar contains the deployment name and control buttons:
-- \"Save\" and \"Close\". -- \"Save\" and \"Close\".
editDeploymentPopupHeader editDeploymentPopupHeader ::
:: MonadWidget t m MonadWidget t m =>
=> DeploymentName -- ^ Name of the deployment. -- | Name of the deployment.
-> Dynamic t Bool -- ^ Form validation state. DeploymentName ->
-> m (Event t (), Event t ()) -- ^ \"Close\" event and \"Save\" click event. -- | Form validation state.
Dynamic t Bool ->
-- | \"Close\" event and \"Save\" click event.
m (Event t (), Event t ())
editDeploymentPopupHeader dname validDyn = editDeploymentPopupHeader dname validDyn =
divClass "popup__head" $ do divClass "popup__head" $ do
closeEv <- buttonClass "popup__close" "Close popup" closeEv <- buttonClass "popup__close" "Close popup"
elClass "h2" "popup__project" $ text $ "Edit " <> coerce dname elClass "h2" "popup__project" $ text $ "Edit " <> coerce dname
saveEv <- divClass "popup__operations" $ saveEv <-
divClass "popup__operations" $
buttonClassEnabled "popup__action button button--save" "Save" validDyn buttonClassEnabled "popup__action button button--save" "Save" validDyn
divClass "popup__menu drop drop--actions" blank 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 -- | The body of the sidebar containing the edit form. Contains a tag field and
-- an override field. -- an override field.
editDeploymentPopupBody editDeploymentPopupBody ::
:: MonadWidget t m MonadWidget t m =>
=> DeploymentFullInfo -- | Full deployment data.
-- ^ Full deployment data. DeploymentFullInfo ->
-> Event t (ReqResult tag CommandResponse) -- | \"Edit request\" failure event.
-- ^ \"Edit request\" failure event. Event t (ReqResult tag CommandResponse) ->
-> m (Dynamic t DeploymentUpdate, Dynamic t Bool) -- | Returns deployment update and validation state.
-- ^ Returns deployment update and validation state. m (Dynamic t DeploymentUpdate, Dynamic t Bool)
editDeploymentPopupBody dfi errEv = divClass "popup__content" $ editDeploymentPopupBody dfi errEv = divClass "popup__content" $
divClass "deployment" $ mdo divClass "deployment" $ mdo
let let commandResponseEv = fmapMaybe commandResponse errEv
commandResponseEv = fmapMaybe commandResponse errEv
appErrEv = R.difference (fmapMaybe reqErrorBody errEv) commandResponseEv appErrEv = R.difference (fmapMaybe reqErrorBody errEv) commandResponseEv
dfiTag = dfi ^. field @"deployment" . field @"tag" . coerced . to Just dfiTag = dfi ^. field @"deployment" . field @"tag" . coerced . to Just
dfiAppVars = dfi ^. field @"deployment" . field @"appOverrides" . coerced dfiAppVars = dfi ^. field @"deployment" . field @"appOverrides" . coerced
@ -95,101 +98,31 @@ editDeploymentPopupBody dfi errEv = divClass "popup__content" $
(tagDyn, tOkEv) <- octopodTextInput "tag" "Tag" "Tag" dfiTag tagErrEv (tagDyn, tOkEv) <- octopodTextInput "tag" "Tag" "Tag" dfiTag tagErrEv
appVarsDyn <- envVarsInput "App overrides" dfiAppVars appVarsDyn <- envVarsInput "App overrides" dfiAppVars
deploymentVarsDyn <- envVarsInput "Deployment overrides" dfiDeploymentVars deploymentVarsDyn <- envVarsInput "Deployment overrides" dfiDeploymentVars
let
oldAppVarDyn = coerce <$> getOldVars dfiAppVars <$> appVarsDyn
newAppVarDyn = coerce <$> getNewVars dfiAppVars <$> appVarsDyn
oldDeploymentVarDyn = coerce <$> getOldVars dfiDeploymentVars <$> deploymentVarsDyn
newDeploymentVarDyn = coerce <$> getNewVars dfiDeploymentVars <$> deploymentVarsDyn
validDyn <- holdDyn True $ updated tOkEv validDyn <- holdDyn True $ updated tOkEv
pure $ (DeploymentUpdate pure
( DeploymentUpdate
<$> (DeploymentTag <$> tagDyn) <$> (DeploymentTag <$> tagDyn)
<*> newAppVarDyn <*> appVarsDyn
<*> oldAppVarDyn <*> deploymentVarsDyn
<*> newDeploymentVarDyn , validDyn
<*> oldDeploymentVarDyn, validDyn) )
where where
getTagError crEv tagDyn = let getTagError crEv tagDyn =
tagErrEv' = fmapMaybe (preview (_Ctor @"ValidationError" . _2 )) crEv let tagErrEv' = fmapMaybe (preview (_Ctor @"ValidationError" . _2)) crEv
tagErrEv = ffilter (/= "") $ T.intercalate ". " <$> tagErrEv' tagErrEv = ffilter (/= "") $ T.intercalate ". " <$> tagErrEv'
badTagText = "Tag should not be empty" badTagText = "Tag should not be empty"
badNameEv = badTagText <$ (ffilter (== "") $ updated tagDyn) badNameEv = badTagText <$ ffilter (== "") (updated tagDyn)
in leftmost [tagErrEv, badNameEv] in leftmost [tagErrEv, badNameEv]
getOldVars i u = deleteFirstsBy cmpKey i u
getNewVars i u = deleteFirstsBy (==) u i
cmpKey (Override k1 _ v1) (Override k2 _ v2) = k1 == k2 && v1 == v2
-- | The widget used to display errors. -- | The widget used to display errors.
errorHeader errorHeader ::
:: MonadWidget t m MonadWidget t m =>
=> Event t Text -- ^ Message text. -- | Message text.
-> m () Event t Text ->
m ()
errorHeader appErrEv = do errorHeader appErrEv = do
widgetHold_ blank $ appErrEv <&> \appErr -> do widgetHold_ blank $
appErrEv <&> \appErr -> do
divClass "deployment__output notification notification--danger" $ do divClass "deployment__output notification notification--danger" $ do
el "b" $ text "App error: " el "b" $ text "App error: "
text appErr text appErr
-- | Widget with override fields. This widget supports adding and
-- removing key-value pairs.
envVarsInput
:: MonadWidget t m
=> Text -- ^ Overrides header.
-> Overrides -- ^ Current deployment overrides.
-> m (Dynamic t Overrides) -- ^ Updated deployment overrides.
envVarsInput overridesHeader evs = do
elClass "section" "deployment__section" $ do
elClass "h3" "deployment__sub-heading" $ text overridesHeader
elClass "div" "deployment__widget" $
elClass "div" "overrides" $ mdo
let
initEnvs = L.foldl' (\m v -> fst $ insertUniq v m) emptyUniqKeyMap evs
emptyVar = Override "" "" Public
addEv = clickEv $> Endo (fst . insertUniq emptyVar)
envsDyn <- foldDyn appEndo initEnvs $ leftmost [ addEv, updEv ]
(_, updEv) <- runEventWriterT $ listWithKey (uniqMap <$> envsDyn) envVarInput
let addingIsEnabled = all ( (not . T.null) . overrideKey ) . elemsUniq <$> envsDyn
clickEv <- buttonClassEnabled'
"overrides__add dash dash--add" "Add an override" addingIsEnabled
"dash--disabled"
pure $ elemsUniq <$> envsDyn
-- | Widget for entering a key-value pair. The updated overrides list is
-- written to the 'EventWriter'.
envVarInput
:: (EventWriter t (Endo (UniqKeyMap Override)) m, MonadWidget t m)
=> Int -- ^ Index of variable in overrides list.
-> Dynamic t Override -- ^ Current variable key and value.
-> m ()
envVarInput ix epDyn = do
ep <- sample $ current epDyn
divClass "overrides__item" $ do
(keyDyn, _) <-
octopodTextInput' "overrides__key" "key" (Just $ overrideKey ep) never
(valDyn, _) <-
octopodTextInput' "overrides__value" "value" (Just $ overrideValue ep) never
closeEv <- buttonClass "overrides__delete spot spot--cancel" "Delete"
let
envEv = updated $ zipDynWith (\k v -> Override k v Public) keyDyn valDyn
deleteEv = Endo (deleteUniq ix) <$ closeEv
updEv = Endo . updateUniq ix . const <$> envEv
tellEvent $ leftmost [deleteEv, updEv]
data UniqKeyMap v = UniqKeyMap (Map Int v) (Int)
uniqMap :: UniqKeyMap v -> Map Int v
uniqMap (UniqKeyMap m _) = m
insertUniq :: v -> UniqKeyMap v -> (UniqKeyMap v, Int)
insertUniq v (UniqKeyMap m x) = (UniqKeyMap (M.insert x v m) (x + 1), x)
deleteUniq :: Int -> UniqKeyMap v -> UniqKeyMap v
deleteUniq k (UniqKeyMap m x) = UniqKeyMap (M.delete k m) x
updateUniq :: Int -> (v -> v) -> UniqKeyMap v -> UniqKeyMap v
updateUniq k f (UniqKeyMap m x) = UniqKeyMap (M.adjust f k m) x
elemsUniq :: UniqKeyMap v -> [v]
elemsUniq (UniqKeyMap m _) = M.elems m
emptyUniqKeyMap :: UniqKeyMap v
emptyUniqKeyMap = UniqKeyMap mempty 0

View File

@ -1,23 +1,18 @@
{-| -- |
Module : Page.Popup.NewDeployment --Module : Page.Popup.NewDeployment
Description : New deployment sidebar. --Description : New deployment sidebar.
--
This module contains the definition of \"new deployment\" sidebar. --This module contains the definition of \"new deployment\" sidebar.
-}
module Page.Popup.NewDeployment (newDeploymentPopup) where module Page.Popup.NewDeployment (newDeploymentPopup) where
import Control.Lens (preview, _1, _2) import Control.Lens (preview, _1, _2)
import Control.Monad import Control.Monad
import Data.Coerce
import Data.Functor import Data.Functor
import Data.Generics.Sum import Data.Generics.Sum
import Data.Map as M
import Data.Monoid import Data.Monoid
import Data.Text as T (Text, intercalate) import Data.Text as T (Text, intercalate)
import Prelude as P
import Reflex.Dom as R import Reflex.Dom as R
import Prelude as P
import Common.Types import Common.Types
import Common.Validation (isNameValid) import Common.Validation (isNameValid)
@ -26,38 +21,44 @@ import Frontend.Utils
import Servant.Reflex import Servant.Reflex
import Servant.Reflex.Extra import Servant.Reflex.Extra
-- | The root function for \"new deployment\" sidebar. -- | The root function for \"new deployment\" sidebar.
newDeploymentPopup newDeploymentPopup ::
:: MonadWidget t m MonadWidget t m =>
=> Event t () -- ^ \"Show\" event. -- | \"Show\" event.
-> Event t () -- ^ \"Close\" event. Event t () ->
-> m () -- | \"Close\" event.
newDeploymentPopup showEv hideEv = void $ sidebar showEv hideEv $ const $ mdo Event t () ->
m ()
newDeploymentPopup showEv hideEv = void $
sidebar showEv hideEv $
const $ mdo
divClass "popup__body" $ mdo divClass "popup__body" $ mdo
(closeEv', saveEv) <- newDeploymentPopupHeader enabledDyn (closeEv', saveEv) <- newDeploymentPopupHeader enabledDyn
(deploymentDyn, validDyn) <- newDeploymentPopupBody respEv (deploymentDyn, validDyn) <- newDeploymentPopupBody respEv
respEv <- createEndpoint (Right <$> deploymentDyn) saveEv respEv <- createEndpoint (Right <$> deploymentDyn) saveEv
sentDyn <- holdDyn False $ leftmost sentDyn <-
holdDyn False $
leftmost
[ True <$ saveEv [ True <$ saveEv
, False <$ respEv ] , False <$ respEv
let ]
successEv = let successEv =
fmapMaybe (preview (_Ctor @"Success") <=< commandResponse) respEv fmapMaybe (preview (_Ctor @"Success") <=< commandResponse) respEv
closeEv = leftmost [ closeEv', successEv ] closeEv = leftmost [closeEv', successEv]
enabledDyn = zipDynWith (&&) (not <$> sentDyn) validDyn enabledDyn = zipDynWith (&&) (not <$> sentDyn) validDyn
pure (never, closeEv) pure (never, closeEv)
-- | The header of sidebar contains control buttons: \"Save\" and \"Close\". -- | The header of sidebar contains control buttons: \"Save\" and \"Close\".
newDeploymentPopupHeader newDeploymentPopupHeader ::
:: MonadWidget t m MonadWidget t m =>
=> Dynamic t Bool Dynamic t Bool ->
-> m (Event t (), Event t ()) m (Event t (), Event t ())
newDeploymentPopupHeader enabledDyn = newDeploymentPopupHeader enabledDyn =
divClass "popup__head" $ do divClass "popup__head" $ do
closeEv <- buttonClass "popup__close" "Close popup" closeEv <- buttonClass "popup__close" "Close popup"
elClass "h2" "popup__project" $ text "Create new deployment" elClass "h2" "popup__project" $ text "Create new deployment"
saveEv <- divClass "popup__operations" $ saveEv <-
divClass "popup__operations" $
buttonClassEnabled "popup__action button button--save" "Save" enabledDyn buttonClassEnabled "popup__action button button--save" "Save" enabledDyn
divClass "popup__menu drop drop--actions" blank divClass "popup__menu drop drop--actions" blank
pure (closeEv, saveEv) pure (closeEv, saveEv)
@ -65,93 +66,58 @@ newDeploymentPopupHeader enabledDyn =
-- | The body of the sidebar contains the creation form. It contains: a name field, -- | 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: -- a tag field and overrides fields. The name field is validated with the regexp:
-- @^[a-z][a-z0-9\\-]{1,16}$@. -- @^[a-z][a-z0-9\\-]{1,16}$@.
newDeploymentPopupBody newDeploymentPopupBody ::
:: MonadWidget t m MonadWidget t m =>
=> Event t (ReqResult tag CommandResponse) -- | Request failure event.
-- ^ Request failure event. Event t (ReqResult tag CommandResponse) ->
-> m (Dynamic t Deployment, Dynamic t Bool) -- | Returns new deployment and validation states.
-- ^ Returns new deployment and validation states. m (Dynamic t Deployment, Dynamic t Bool)
newDeploymentPopupBody errEv = divClass "popup__content" $ newDeploymentPopupBody errEv = divClass "popup__content" $
divClass "deployment" $ mdo divClass "deployment" $ mdo
let let commandResponseEv = fmapMaybe commandResponse errEv
commandResponseEv = fmapMaybe commandResponse errEv
appErrEv = R.difference (fmapMaybe reqErrorBody errEv) commandResponseEv appErrEv = R.difference (fmapMaybe reqErrorBody errEv) commandResponseEv
nameErrEv = getNameError commandResponseEv nameDyn nameErrEv = getNameError commandResponseEv nameDyn
tagErrEv = getTagError commandResponseEv tagDyn tagErrEv = getTagError commandResponseEv tagDyn
errorHeader appErrEv errorHeader appErrEv
(nameDyn, nOkDyn) <- octopodTextInput "tag" "Name" "Name" Nothing nameErrEv (nameDyn, nOkDyn) <- octopodTextInput "tag" "Name" "Name" Nothing nameErrEv
(tagDyn, tOkDyn) <- octopodTextInput "tag" "Tag" "Tag" Nothing tagErrEv (tagDyn, tOkDyn) <- octopodTextInput "tag" "Tag" "Tag" Nothing tagErrEv
appVarsDyn <- envVarsInput "App overrides" appVarsDyn <- envVarsInput "App overrides" mempty
deploymentVarsDyn <- envVarsInput "Deployment overrides" deploymentVarsDyn <- envVarsInput "Deployment overrides" mempty
validDyn <- holdDyn False $ updated $ zipDynWith (&&) nOkDyn tOkDyn validDyn <- holdDyn False $ updated $ zipDynWith (&&) nOkDyn tOkDyn
pure $ (Deployment pure
( Deployment
<$> (DeploymentName <$> nameDyn) <$> (DeploymentName <$> nameDyn)
<*> (DeploymentTag <$> tagDyn) <*> (DeploymentTag <$> tagDyn)
<*> (coerce <$> appVarsDyn) <*> appVarsDyn
<*> (coerce <$> deploymentVarsDyn), validDyn) <*> deploymentVarsDyn
, validDyn
)
where where
getNameError crEv nameDyn = let getNameError crEv nameDyn =
nameErrEv' = fmapMaybe (preview (_Ctor @"ValidationError" . _1 )) crEv let nameErrEv' = fmapMaybe (preview (_Ctor @"ValidationError" . _1)) crEv
isNameValidDyn = isNameValid . DeploymentName <$> nameDyn isNameValidDyn = isNameValid . DeploymentName <$> nameDyn
badNameText = "Deployment name length should be longer than 2 characters \ badNameText =
"Deployment name length should be longer than 2 characters \
\and under 17 characters and begin with a letter." \and under 17 characters and begin with a letter."
badNameEv = badNameText <$ (ffilter not $ updated isNameValidDyn) badNameEv = badNameText <$ (ffilter not $ updated isNameValidDyn)
nameErrEv = ffilter (/= "") $ T.intercalate ". " <$> nameErrEv' nameErrEv = ffilter (/= "") $ T.intercalate ". " <$> nameErrEv'
in leftmost [nameErrEv, badNameEv] in leftmost [nameErrEv, badNameEv]
getTagError crEv tagDyn = let getTagError crEv tagDyn =
tagErrEv' = fmapMaybe (preview (_Ctor @"ValidationError" . _2 )) crEv let tagErrEv' = fmapMaybe (preview (_Ctor @"ValidationError" . _2)) crEv
tagErrEv = ffilter (/= "") $ T.intercalate ". " <$> tagErrEv' tagErrEv = ffilter (/= "") $ T.intercalate ". " <$> tagErrEv'
badTagText = "Tag should not be empty" badTagText = "Tag should not be empty"
badNameEv = badTagText <$ (ffilter (== "") $ updated tagDyn) badNameEv = badTagText <$ (ffilter (== "") $ updated tagDyn)
in leftmost [tagErrEv, badNameEv] in leftmost [tagErrEv, badNameEv]
-- | The widget used to display errors. -- | The widget used to display errors.
errorHeader errorHeader ::
:: MonadWidget t m MonadWidget t m =>
=> Event t Text -- ^ Message text. -- | Message text.
-> m () Event t Text ->
m ()
errorHeader appErrEv = do errorHeader appErrEv = do
widgetHold_ blank $ appErrEv <&> \appErr -> do widgetHold_ blank $
appErrEv <&> \appErr -> do
divClass "deployment__output notification notification--danger" $ do divClass "deployment__output notification notification--danger" $ do
el "b" $ text "App error: " el "b" $ text "App error: "
text appErr text appErr
-- | Widget with override fields. This widget supports adding and
-- a removing key-value pairs.
envVarsInput
:: MonadWidget t m
=> Text -- ^ Widget header.
-> m (Dynamic t [Override])
envVarsInput headerText = do
elClass "section" "deployment__section" $ do
elClass "h3" "deployment__sub-heading" $ text headerText
elClass "div" "deployment__widget" $
elClass "div" "overrides" $ mdo
let
emptyVar = Override "" "" Public
addEv = clickEv $> Endo (\envs -> P.length envs =: emptyVar <> envs)
envsDyn <- foldDyn appEndo mempty $ leftmost [ addEv, updEv ]
(_, updEv) <- runEventWriterT $ listWithKey envsDyn envVarInput
let addDisabledDyn = all ( (/= "") . overrideKey ) . elems <$> envsDyn
clickEv <- buttonClassEnabled'
"overrides__add dash dash--add" "Add an override" addDisabledDyn
"dash--disabled"
pure $ elems <$> envsDyn
-- | Widget for a key-value pair. It returns an event carrying an update
-- of overrides list via 'EventWriter'.
envVarInput
:: (EventWriter t (Endo (Map Int Override)) m, MonadWidget t m)
=> Int -- ^ Index of variable in overrides list.
-> Dynamic t Override -- ^ Current variable key and value.
-> m ()
envVarInput ix _ = do
divClass "overrides__item" $ do
(keyDyn, _) <- octopodTextInput' "overrides__key" "key" Nothing never
(valDyn, _) <- octopodTextInput' "overrides__value" "value" Nothing never
closeEv <- buttonClass "overrides__delete spot spot--cancel" "Delete"
let
envEv = updated $ zipDynWith (\k v -> Override k v Public) keyDyn valDyn
deleteEv = Endo (M.delete ix) <$ closeEv
updEv = Endo . flip update ix . const . Just <$> envEv
tellEvent $ leftmost [deleteEv, updEv]