diff --git a/cardano-tracer/cardano-tracer.cabal b/cardano-tracer/cardano-tracer.cabal
index 4c4edf21247..13ec3398247 100644
--- a/cardano-tracer/cardano-tracer.cabal
+++ b/cardano-tracer/cardano-tracer.cabal
@@ -61,7 +61,6 @@ library
Cardano.Tracer.Handlers.RTView.State.Displayed
Cardano.Tracer.Handlers.RTView.State.EraSettings
- Cardano.Tracer.Handlers.RTView.State.Errors
Cardano.Tracer.Handlers.RTView.State.Historical
Cardano.Tracer.Handlers.RTView.State.Last
Cardano.Tracer.Handlers.RTView.State.Peers
@@ -73,10 +72,10 @@ library
Cardano.Tracer.Handlers.RTView.UI.CSS.Own
Cardano.Tracer.Handlers.RTView.UI.HTML.Node.Column
Cardano.Tracer.Handlers.RTView.UI.HTML.Node.EKG
- Cardano.Tracer.Handlers.RTView.UI.HTML.Node.Errors
Cardano.Tracer.Handlers.RTView.UI.HTML.Node.Peers
Cardano.Tracer.Handlers.RTView.UI.HTML.About
Cardano.Tracer.Handlers.RTView.UI.HTML.Body
+ Cardano.Tracer.Handlers.RTView.UI.HTML.Logs
Cardano.Tracer.Handlers.RTView.UI.HTML.Main
Cardano.Tracer.Handlers.RTView.UI.HTML.NoNodes
Cardano.Tracer.Handlers.RTView.UI.HTML.Notifications
@@ -93,10 +92,10 @@ library
Cardano.Tracer.Handlers.RTView.Update.Chain
Cardano.Tracer.Handlers.RTView.Update.EKG
Cardano.Tracer.Handlers.RTView.Update.EraSettings
- Cardano.Tracer.Handlers.RTView.Update.Errors
Cardano.Tracer.Handlers.RTView.Update.Historical
Cardano.Tracer.Handlers.RTView.Update.KES
Cardano.Tracer.Handlers.RTView.Update.Leadership
+ Cardano.Tracer.Handlers.RTView.Update.Logs
Cardano.Tracer.Handlers.RTView.Update.NodeInfo
Cardano.Tracer.Handlers.RTView.Update.NodeState
Cardano.Tracer.Handlers.RTView.Update.Nodes
@@ -118,7 +117,7 @@ library
other-modules: Paths_cardano_tracer
build-depends: aeson
- , aeson-pretty
+ -- , aeson-pretty
, async
, async-extras
, bimap
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Run.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Run.hs
index 50b68e8785c..759a0ab861d 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Run.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Run.hs
@@ -21,12 +21,10 @@ import Cardano.Tracer.Handlers.RTView.Notifications.Utils
import Cardano.Tracer.Handlers.RTView.SSL.Certs
import Cardano.Tracer.Handlers.RTView.State.Displayed
import Cardano.Tracer.Handlers.RTView.State.EraSettings
-import Cardano.Tracer.Handlers.RTView.State.Errors
import Cardano.Tracer.Handlers.RTView.State.Last
import Cardano.Tracer.Handlers.RTView.State.TraceObjects
import Cardano.Tracer.Handlers.RTView.UI.HTML.Main
import Cardano.Tracer.Handlers.RTView.Update.EraSettings
-import Cardano.Tracer.Handlers.RTView.Update.Errors
import Cardano.Tracer.Handlers.RTView.Update.Historical
-- | RTView is a part of 'cardano-tracer' that provides an ability
@@ -54,7 +52,6 @@ runRTView tracerEnv =
-- period when RTView web-page wasn't opened.
lastResources <- initLastResources
eraSettings <- initErasSettings
- errors <- initErrors
void . sequenceConcurrently $
[ UI.startGUI (config host port certFile keyFile) $
@@ -65,11 +62,9 @@ runRTView tracerEnv =
reloadFlag
logging
network
- errors
, runHistoricalUpdater tracerEnv lastResources
, runHistoricalBackup tracerEnv
, runEraSettingsUpdater tracerEnv eraSettings
- , runErrorsUpdater tracerEnv errors
]
where
TracerConfig{network, logging, hasRTView} = teConfig tracerEnv
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/State/Errors.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/State/Errors.hs
deleted file mode 100644
index 9dffb0de6d5..00000000000
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/State/Errors.hs
+++ /dev/null
@@ -1,153 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE StandaloneDeriving #-}
-
-module Cardano.Tracer.Handlers.RTView.State.Errors
- ( ErrorIx
- , ErrorInfo
- , Errors
- , addError
- , getError
- , getErrors
- , getErrorsFilteredBySeverity
- , getErrorsFilteredByText
- , getErrorsSortedBy
- , timeAsc
- , timeDesc
- , severityAsc
- , severityDesc
- , initErrors
- , deleteAllErrors
- , errorsToJSON
- ) where
-
-import Control.Concurrent.STM (atomically)
-import Control.Concurrent.STM.TVar (TVar, modifyTVar', newTVarIO, readTVarIO)
-import Data.Aeson (ToJSON)
-import Data.Aeson.Encode.Pretty (encodePretty)
-import qualified Data.ByteString.Lazy as BSL
-import Data.List (find, sortBy)
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as M
-import Data.Text (Text, isInfixOf)
-import qualified Data.Text as T
-import Data.Text.Encoding (decodeUtf8)
-
-import Cardano.Tracer.Handlers.RTView.State.TraceObjects
-import Cardano.Tracer.Types (NodeId)
-
-import Cardano.Logging (SeverityS)
-
-type ErrorIx = Int
-type ErrorInfo = (ErrorIx, TraceObjectInfo)
-type Errors = TVar (Map NodeId [ErrorInfo])
-
--- | We need it to export errors list to JSON-file.
-deriving instance ToJSON SeverityS
-
-initErrors :: IO Errors
-initErrors = newTVarIO M.empty
-
-addError
- :: Errors
- -> NodeId
- -> TraceObjectInfo
- -> IO ()
-addError errors nodeId trObInfo = atomically $
- modifyTVar' errors $ \currentErrors ->
- case M.lookup nodeId currentErrors of
- Nothing -> do
- let errorIx = 0
- M.insert nodeId [(errorIx, trObInfo)] currentErrors
- Just errorsFromNode -> do
- -- All errors here should be unique, so check it first.
- case find (\(_, trObInfo') -> trObInfo == trObInfo') errorsFromNode of
- Nothing -> do
- -- No such error, add it.
- let errorIx = length errorsFromNode
- M.adjust (const $ errorsFromNode ++ [(errorIx, trObInfo)]) nodeId currentErrors
- Just _ -> currentErrors
-
-deleteAllErrors
- :: Errors
- -> NodeId
- -> IO ()
-deleteAllErrors errors nodeId = atomically $
- modifyTVar' errors $ \currentErrors ->
- case M.lookup nodeId currentErrors of
- Nothing -> currentErrors
- Just _ -> M.adjust (const []) nodeId currentErrors
-
-getError
- :: ErrorIx
- -> Errors
- -> NodeId
- -> IO (Maybe ErrorInfo)
-getError errorIx errors nodeId =
- getErrors errors nodeId >>= \case
- [] -> return Nothing
- allErrors -> return $ find (\(ix, _) -> ix == errorIx) allErrors
-
-getErrors
- :: Errors
- -> NodeId
- -> IO [ErrorInfo]
-getErrors errors nodeId =
- getErrorsHandled errors nodeId id
-
-getErrorsSortedBy
- :: (ErrorInfo -> ErrorInfo -> Ordering)
- -> Errors
- -> NodeId
- -> IO [ErrorInfo]
-getErrorsSortedBy ordering errors nodeId =
- getErrorsHandled errors nodeId $ sortBy ordering
-
-timeAsc
- , timeDesc
- , severityAsc
- , severityDesc :: ErrorInfo -> ErrorInfo -> Ordering
-timeAsc (_, (_, _, ts1)) (_, (_, _, ts2)) = ts1 `compare` ts2
-timeDesc (_, (_, _, ts1)) (_, (_, _, ts2)) = ts2 `compare` ts1
-severityAsc (_, (_, s1, _)) (_, (_, s2, _)) = s1 `compare` s2
-severityDesc (_, (_, s1, _)) (_, (_, s2, _)) = s2 `compare` s1
-
-getErrorsFilteredBySeverity
- :: SeverityS
- -> Errors
- -> NodeId
- -> IO [ErrorInfo]
-getErrorsFilteredBySeverity severity errors nodeId =
- getErrorsHandled errors nodeId $ filter (\(_, (_, sev, _)) -> sev == severity)
-
-getErrorsFilteredByText
- :: Text
- -> Errors
- -> NodeId
- -> IO [ErrorInfo]
-getErrorsFilteredByText textToSearch errors nodeId =
- if T.null textToSearch
- then return []
- else getErrorsHandled errors nodeId $ filter (\(_, (msg, _, _)) -> textToSearch `isInfixOf` msg)
-
-getErrorsHandled
- :: Errors
- -> NodeId
- -> ([ErrorInfo] -> [ErrorInfo])
- -> IO [ErrorInfo]
-getErrorsHandled errors nodeId handler = do
- errors' <- readTVarIO errors
- case M.lookup nodeId errors' of
- Nothing -> return []
- Just errorsFromNode -> return $ handler errorsFromNode
-
-errorsToJSON
- :: Errors
- -> NodeId
- -> IO (Maybe Text)
-errorsToJSON errors nodeId =
- getErrors errors nodeId >>= \case
- [] -> return Nothing
- errorsFromNode -> do
- let errorsList = [eI | (_ix, eI) <- errorsFromNode]
- return . Just . decodeUtf8 . BSL.toStrict $ encodePretty errorsList
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/State/TraceObjects.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/State/TraceObjects.hs
index c2fc6775ff4..81de2a7aba0 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/State/TraceObjects.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/State/TraceObjects.hs
@@ -2,16 +2,22 @@
{-# LANGUAGE OverloadedStrings #-}
module Cardano.Tracer.Handlers.RTView.State.TraceObjects
- ( Namespace
+ ( LogsLiveViewCounters
+ , Namespace
, SavedTraceObjects
, TraceObjectInfo
+ , getLogsLiveViewCounter
+ , getTraceObjects
+ , incLogsLiveViewCounter
+ , initLogsLiveViewCounters
, initSavedTraceObjects
, saveTraceObjects
) where
import Control.Concurrent.STM (atomically)
-import Control.Concurrent.STM.TVar (TVar, modifyTVar', newTVarIO)
-import Control.Monad (unless)
+import Control.Concurrent.STM.TQueue
+import Control.Concurrent.STM.TVar (TVar, modifyTVar', newTVarIO, readTVar, readTVarIO)
+import Control.Monad (forM_, unless)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (mapMaybe)
@@ -25,24 +31,32 @@ import Cardano.Tracer.Types (NodeId)
type Namespace = Text
type TraceObjectInfo = (Text, SeverityS, UTCTime)
--- | We have to store 'TraceObject's received from the node,
--- to be able to update corresponding elements (on the web page)
--- using the values extracted from these 'TraceObject's.
-type SavedForNode = Map Namespace TraceObjectInfo
+type SavedForNode = TQueue (Namespace, TraceObjectInfo)
type SavedTraceObjects = TVar (Map NodeId SavedForNode)
initSavedTraceObjects :: IO SavedTraceObjects
initSavedTraceObjects = newTVarIO M.empty
-saveTraceObjects :: SavedTraceObjects -> NodeId -> [TraceObject] -> IO ()
+saveTraceObjects
+ :: SavedTraceObjects
+ -> NodeId
+ -> [TraceObject]
+ -> IO ()
saveTraceObjects savedTraceObjects nodeId traceObjects =
- unless (null itemsToSave) $
- atomically $ modifyTVar' savedTraceObjects $ \savedTO ->
- case M.lookup nodeId savedTO of
- Nothing ->
- M.insert nodeId (M.fromList itemsToSave) savedTO
- Just savedTOForThisNode ->
- M.adjust (const $! savedTOForThisNode `updateSavedBy` itemsToSave) nodeId savedTO
+ unless (null itemsToSave) $ atomically $ do
+ savedTO' <- readTVar savedTraceObjects
+ case M.lookup nodeId savedTO' of
+ Nothing -> do
+ -- There is no queue for this node yet, so create it, fill it and save it.
+ newQ <- newTQueue
+ pushItemsToQueue newQ
+ modifyTVar' savedTraceObjects $ \savedTO ->
+ case M.lookup nodeId savedTO of
+ Nothing -> M.insert nodeId newQ savedTO
+ Just _ -> savedTO
+ Just qForThisNode ->
+ -- There is a queue for this node already, so fill it.
+ pushItemsToQueue qForThisNode
where
itemsToSave = mapMaybe getTOValue traceObjects
@@ -56,8 +70,34 @@ saveTraceObjects savedTraceObjects nodeId traceObjects =
mkName = intercalate "."
- -- Update saved 'TraceObject's by new ones: existing value will be replaced.
- updateSavedBy = go
- where
- go saved [] = saved
- go saved ((ns, toI):others) = M.insert ns toI saved `go` others
+ pushItemsToQueue = forM_ itemsToSave . writeTQueue
+
+getTraceObjects
+ :: SavedTraceObjects
+ -> NodeId
+ -> IO [(Namespace, TraceObjectInfo)]
+getTraceObjects savedTraceObjects nodeId = atomically $ do
+ qForThisNode <- M.lookup nodeId <$> readTVar savedTraceObjects
+ maybe (return []) flushTQueue qForThisNode
+
+-- | Counters for displayed logs item in "live view window".
+type LogsLiveViewCounters = TVar (Map NodeId Int)
+
+initLogsLiveViewCounters :: IO LogsLiveViewCounters
+initLogsLiveViewCounters = newTVarIO M.empty
+
+incLogsLiveViewCounter
+ :: LogsLiveViewCounters
+ -> NodeId
+ -> IO ()
+incLogsLiveViewCounter llvCounters nodeId = atomically $
+ modifyTVar' llvCounters $ \currentCounters ->
+ case M.lookup nodeId currentCounters of
+ Nothing -> M.insert nodeId 1 currentCounters
+ Just counterForNode -> M.adjust (const $! counterForNode + 1) nodeId currentCounters
+
+getLogsLiveViewCounter
+ :: LogsLiveViewCounters
+ -> NodeId
+ -> IO (Maybe Int)
+getLogsLiveViewCounter llvCounters nodeId = M.lookup nodeId <$> readTVarIO llvCounters
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/CSS/Bulma.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/CSS/Bulma.hs
index c2ee553f2c7..db2f1d29d44 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/CSS/Bulma.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/CSS/Bulma.hs
@@ -2,10 +2,11 @@
module Cardano.Tracer.Handlers.RTView.UI.CSS.Bulma
( bulmaCSS
- , bulmaTooltipCSS
+ , bulmaCheckboxCSS
+ , bulmaDividerCSS
, bulmaPageloaderCSS
, bulmaSwitchCSS
- , bulmaDividerCSS
+ , bulmaTooltipCSS
) where
import Data.String.QQ
@@ -38,3 +39,8 @@ bulmaDividerCSS = [s|
/*! @creativebulma/bulma-divider v1.1.0 | (c) 2020 Gaetan | MIT License | https://github.com/CreativeBulma/bulma-divider */
.divider{position:relative;display:flex;align-items:center;text-transform:uppercase;color:#7a7a7a;font-size:.75rem;font-weight:600;letter-spacing:.5px;margin:25px 0}.divider:after,.divider:before{content:"";display:block;flex:1;height:1px;background-color:#dbdbdb}.divider:not(.is-right):after{margin-left:10px}.divider:not(.is-left):before{margin-right:10px}.divider.is-left:before,.divider.is-right:after{display:none}.divider.is-vertical{flex-direction:column;margin:0 25px}.divider.is-vertical:after,.divider.is-vertical:before{height:auto;width:1px}.divider.is-vertical:after{margin-left:0;margin-top:10px}.divider.is-vertical:before{margin-right:0;margin-bottom:10px}.divider.is-white:after,.divider.is-white:before{background-color:#fff}.divider.is-black:after,.divider.is-black:before{background-color:#0a0a0a}.divider.is-light:after,.divider.is-light:before{background-color:#f5f5f5}.divider.is-dark:after,.divider.is-dark:before{background-color:#363636}.divider.is-primary:after,.divider.is-primary:before{background-color:#00d1b2}.divider.is-primary.is-light:after,.divider.is-primary.is-light:before{background-color:#ebfffc}.divider.is-link:after,.divider.is-link:before{background-color:#3273dc}.divider.is-link.is-light:after,.divider.is-link.is-light:before{background-color:#eef3fc}.divider.is-info:after,.divider.is-info:before{background-color:#3298dc}.divider.is-info.is-light:after,.divider.is-info.is-light:before{background-color:#eef6fc}.divider.is-success:after,.divider.is-success:before{background-color:#48c774}.divider.is-success.is-light:after,.divider.is-success.is-light:before{background-color:#effaf3}.divider.is-warning:after,.divider.is-warning:before{background-color:#ffdd57}.divider.is-warning.is-light:after,.divider.is-warning.is-light:before{background-color:#fffbeb}.divider.is-danger:after,.divider.is-danger:before{background-color:#f14668}.divider.is-danger.is-light:after,.divider.is-danger.is-light:before{background-color:#feecf0}
|]
+
+bulmaCheckboxCSS :: String
+bulmaCheckboxCSS = [s|
+.is-checkradio[type=checkbox],.is-checkradio[type=radio]{outline:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:none;position:absolute;opacity:0}.is-checkradio[type=checkbox]+label,.is-checkradio[type=radio]+label{position:relative;display:initial;cursor:pointer;vertical-align:middle;margin:.5em;padding:.2rem .5rem .2rem 0;border-radius:4px}.is-checkradio[type=checkbox]+label:first-of-type,.is-checkradio[type=radio]+label:first-of-type{margin-left:0}.is-checkradio[type=checkbox]+label:hover::before,.is-checkradio[type=checkbox]+label:hover:before,.is-checkradio[type=radio]+label:hover::before,.is-checkradio[type=radio]+label:hover:before{-webkit-animation-duration:.4s;animation-duration:.4s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-name:hover-color;animation-name:hover-color}.is-checkradio[type=checkbox]+label::before,.is-checkradio[type=checkbox]+label:before,.is-checkradio[type=radio]+label::before,.is-checkradio[type=radio]+label:before{position:absolute;left:0;top:0;content:"";border:.1rem solid #dbdbdb}.is-checkradio[type=checkbox]+label::after,.is-checkradio[type=checkbox]+label:after,.is-checkradio[type=radio]+label::after,.is-checkradio[type=radio]+label:after{position:absolute;display:none;content:"";top:0}.is-checkradio[type=checkbox].is-rtl+label,.is-checkradio[type=radio].is-rtl+label{margin-right:0;margin-left:.5rem}.is-checkradio[type=checkbox].is-rtl+label::before,.is-checkradio[type=checkbox].is-rtl+label:before,.is-checkradio[type=radio].is-rtl+label::before,.is-checkradio[type=radio].is-rtl+label:before{left:auto;right:0}.is-checkradio[type=checkbox]:focus+label::before,.is-checkradio[type=checkbox]:focus+label:before,.is-checkradio[type=radio]:focus+label::before,.is-checkradio[type=radio]:focus+label:before{outline:1px dotted #b5b5b5}.is-checkradio[type=checkbox]:hover:not([disabled])+label::before,.is-checkradio[type=checkbox]:hover:not([disabled])+label:before,.is-checkradio[type=radio]:hover:not([disabled])+label::before,.is-checkradio[type=radio]:hover:not([disabled])+label:before{border-color:#00d1b2!important}.is-checkradio[type=checkbox]:checked+label::before,.is-checkradio[type=checkbox]:checked+label:before,.is-checkradio[type=radio]:checked+label::before,.is-checkradio[type=radio]:checked+label:before{border:.1rem solid #dbdbdb}.is-checkradio[type=checkbox]:checked[disabled],.is-checkradio[type=radio]:checked[disabled]{cursor:not-allowed}.is-checkradio[type=checkbox]:checked[disabled]+label,.is-checkradio[type=radio]:checked[disabled]+label{opacity:.5}.is-checkradio[type=checkbox]:checked+label::before,.is-checkradio[type=checkbox]:checked+label:before,.is-checkradio[type=radio]:checked+label::before,.is-checkradio[type=radio]:checked+label:before{-webkit-animation-name:none;animation-name:none}.is-checkradio[type=checkbox]:checked+label::after,.is-checkradio[type=checkbox]:checked+label:after,.is-checkradio[type=radio]:checked+label::after,.is-checkradio[type=radio]:checked+label:after{display:inline-block}.is-checkradio[type=checkbox][disabled],.is-checkradio[type=radio][disabled]{cursor:not-allowed}.is-checkradio[type=checkbox][disabled]+label,.is-checkradio[type=radio][disabled]+label{opacity:.5;cursor:not-allowed}.is-checkradio[type=checkbox][disabled]+label::after,.is-checkradio[type=checkbox][disabled]+label::before,.is-checkradio[type=checkbox][disabled]+label:after,.is-checkradio[type=checkbox][disabled]+label:before,.is-checkradio[type=checkbox][disabled]+label:hover,.is-checkradio[type=radio][disabled]+label::after,.is-checkradio[type=radio][disabled]+label::before,.is-checkradio[type=radio][disabled]+label:after,.is-checkradio[type=radio][disabled]+label:before,.is-checkradio[type=radio][disabled]+label:hover{cursor:not-allowed}.is-checkradio[type=checkbox][disabled]:hover,.is-checkradio[type=radio][disabled]:hover{cursor:not-allowed}.is-checkradio[type=checkbox][disabled]:hover::before,.is-checkradio[type=checkbox][disabled]:hover:before,.is-checkradio[type=radio][disabled]:hover::before,.is-checkradio[type=radio][disabled]:hover:before{-webkit-animation-name:none;animation-name:none}.is-checkradio[type=checkbox][disabled]::before,.is-checkradio[type=checkbox][disabled]:before,.is-checkradio[type=radio][disabled]::before,.is-checkradio[type=radio][disabled]:before{cursor:not-allowed}.is-checkradio[type=checkbox][disabled]::after,.is-checkradio[type=checkbox][disabled]:after,.is-checkradio[type=radio][disabled]::after,.is-checkradio[type=radio][disabled]:after{cursor:not-allowed}.is-checkradio[type=checkbox].has-no-border+label::before,.is-checkradio[type=checkbox].has-no-border+label:before,.is-checkradio[type=radio].has-no-border+label::before,.is-checkradio[type=radio].has-no-border+label:before{border:none!important}.is-checkradio[type=checkbox].is-block,.is-checkradio[type=radio].is-block{display:none!important}.is-checkradio[type=checkbox].is-block+label,.is-checkradio[type=radio].is-block+label{width:100%!important;background:#f5f5f5;color:rgba(0,0,0,.7);padding-right:.75em}.is-checkradio[type=checkbox].is-block:hover:not([disabled])+label,.is-checkradio[type=radio].is-block:hover:not([disabled])+label{background:#e8e8e8}.is-checkradio[type=checkbox]+label::before,.is-checkradio[type=checkbox]+label:before{border-radius:4px}.is-checkradio[type=checkbox]+label::after,.is-checkradio[type=checkbox]+label:after{box-sizing:border-box;transform:translateY(0) rotate(45deg);border-width:.1rem;border-style:solid;border-color:#00d1b2;border-top:0;border-left:0}.is-checkradio[type=checkbox].is-circle+label::before,.is-checkradio[type=checkbox].is-circle+label:before{border-radius:50%}.is-checkradio[type=checkbox]+label{font-size:1rem;padding-left:2rem}.is-checkradio[type=checkbox]+label::before,.is-checkradio[type=checkbox]+label:before{width:1.5rem;height:1.5rem}.is-checkradio[type=checkbox]+label::after,.is-checkradio[type=checkbox]+label:after{width:.375rem;height:.6rem;top:.405rem;left:.6rem}.is-checkradio[type=checkbox].is-block+label::before,.is-checkradio[type=checkbox].is-block+label:before{width:1.25rem;height:1.25rem;left:.175rem;top:.175rem}.is-checkradio[type=checkbox].is-block+label::after,.is-checkradio[type=checkbox].is-block+label:after{top:.325rem;left:.65rem}.is-checkradio[type=checkbox].is-rtl+label{padding-left:0;padding-right:2rem}.is-checkradio[type=checkbox].is-rtl+label::after,.is-checkradio[type=checkbox].is-rtl+label:after{left:auto;right:.6rem}.is-checkradio[type=checkbox].is-small+label{font-size:.75rem;padding-left:1.5rem}.is-checkradio[type=checkbox].is-small+label::before,.is-checkradio[type=checkbox].is-small+label:before{width:1.125rem;height:1.125rem}.is-checkradio[type=checkbox].is-small+label::after,.is-checkradio[type=checkbox].is-small+label:after{width:.28125rem;height:.45rem;top:.30375rem;left:.45rem}.is-checkradio[type=checkbox].is-small.is-block+label::before,.is-checkradio[type=checkbox].is-small.is-block+label:before{width:.9375rem;height:.9375rem;left:.175rem;top:.175rem}.is-checkradio[type=checkbox].is-small.is-block+label::after,.is-checkradio[type=checkbox].is-small.is-block+label:after{top:.29375rem;left:.5375rem}.is-checkradio[type=checkbox].is-small.is-rtl+label{padding-left:0;padding-right:1.5rem}.is-checkradio[type=checkbox].is-small.is-rtl+label::after,.is-checkradio[type=checkbox].is-small.is-rtl+label:after{left:auto;right:.45rem}.is-checkradio[type=checkbox].is-medium+label{font-size:1.25rem;padding-left:2.5rem}.is-checkradio[type=checkbox].is-medium+label::before,.is-checkradio[type=checkbox].is-medium+label:before{width:1.875rem;height:1.875rem}.is-checkradio[type=checkbox].is-medium+label::after,.is-checkradio[type=checkbox].is-medium+label:after{width:.46875rem;height:.75rem;top:.50625rem;left:.75rem}.is-checkradio[type=checkbox].is-medium.is-block+label::before,.is-checkradio[type=checkbox].is-medium.is-block+label:before{width:1.5625rem;height:1.5625rem;left:.175rem;top:.175rem}.is-checkradio[type=checkbox].is-medium.is-block+label::after,.is-checkradio[type=checkbox].is-medium.is-block+label:after{top:.35625rem;left:.7625rem}.is-checkradio[type=checkbox].is-medium.is-rtl+label{padding-left:0;padding-right:2.5rem}.is-checkradio[type=checkbox].is-medium.is-rtl+label::after,.is-checkradio[type=checkbox].is-medium.is-rtl+label:after{left:auto;right:.75rem}.is-checkradio[type=checkbox].is-large+label{font-size:1.5rem;padding-left:3rem}.is-checkradio[type=checkbox].is-large+label::before,.is-checkradio[type=checkbox].is-large+label:before{width:2.25rem;height:2.25rem}.is-checkradio[type=checkbox].is-large+label::after,.is-checkradio[type=checkbox].is-large+label:after{width:.5625rem;height:.9rem;top:.6075rem;left:.9rem}.is-checkradio[type=checkbox].is-large.is-block+label::before,.is-checkradio[type=checkbox].is-large.is-block+label:before{width:1.875rem;height:1.875rem;left:.175rem;top:.175rem}.is-checkradio[type=checkbox].is-large.is-block+label::after,.is-checkradio[type=checkbox].is-large.is-block+label:after{top:.3875rem;left:.875rem}.is-checkradio[type=checkbox].is-large.is-rtl+label{padding-left:0;padding-right:3rem}.is-checkradio[type=checkbox].is-large.is-rtl+label::after,.is-checkradio[type=checkbox].is-large.is-rtl+label:after{left:auto;right:.9rem}.is-checkradio[type=checkbox].is-white.has-background-color+label::before,.is-checkradio[type=checkbox].is-white.has-background-color+label:before{border-color:transparent!important;background-color:#fff!important}.is-checkradio[type=checkbox].is-white:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-white:hover:not([disabled])+label:before{border-color:#fff!important}.is-checkradio[type=checkbox].is-white:checked+label::after,.is-checkradio[type=checkbox].is-white:checked+label:after{border-color:#fff!important}.is-checkradio[type=checkbox].is-white:checked.has-background-color+label::before,.is-checkradio[type=checkbox].is-white:checked.has-background-color+label:before{border-color:transparent!important;background-color:#fff!important}.is-checkradio[type=checkbox].is-white:checked.has-background-color+label::after,.is-checkradio[type=checkbox].is-white:checked.has-background-color+label:after{border-color:#0a0a0a!important;background-color:#fff!important}.is-checkradio[type=checkbox].is-white.is-block:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-white.is-block:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-white.is-block:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-white.is-block:hover:not([disabled])+label:before{border-color:#fff!important}.is-checkradio[type=checkbox].is-white.is-block:checked+label{color:#0a0a0a;border-color:#fff!important;background:#fff}.is-checkradio[type=checkbox].is-white.is-block:checked+label::after,.is-checkradio[type=checkbox].is-white.is-block:checked+label:after{border-color:#0a0a0a!important}.is-checkradio[type=checkbox].is-white.is-block:checked:hover:not([disabled])+label{background:#f2f2f2}.is-checkradio[type=checkbox].is-white.is-block:checked:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-white.is-block:checked:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-white.is-block:checked:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-white.is-block:checked:hover:not([disabled])+label:before{border-color:#000!important}.is-checkradio[type=checkbox].is-black.has-background-color+label::before,.is-checkradio[type=checkbox].is-black.has-background-color+label:before{border-color:transparent!important;background-color:#0a0a0a!important}.is-checkradio[type=checkbox].is-black:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-black:hover:not([disabled])+label:before{border-color:#0a0a0a!important}.is-checkradio[type=checkbox].is-black:checked+label::after,.is-checkradio[type=checkbox].is-black:checked+label:after{border-color:#0a0a0a!important}.is-checkradio[type=checkbox].is-black:checked.has-background-color+label::before,.is-checkradio[type=checkbox].is-black:checked.has-background-color+label:before{border-color:transparent!important;background-color:#0a0a0a!important}.is-checkradio[type=checkbox].is-black:checked.has-background-color+label::after,.is-checkradio[type=checkbox].is-black:checked.has-background-color+label:after{border-color:#fff!important;background-color:#0a0a0a!important}.is-checkradio[type=checkbox].is-black.is-block:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-black.is-block:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-black.is-block:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-black.is-block:hover:not([disabled])+label:before{border-color:#0a0a0a!important}.is-checkradio[type=checkbox].is-black.is-block:checked+label{color:#fff;border-color:#0a0a0a!important;background:#0a0a0a}.is-checkradio[type=checkbox].is-black.is-block:checked+label::after,.is-checkradio[type=checkbox].is-black.is-block:checked+label:after{border-color:#fff!important}.is-checkradio[type=checkbox].is-black.is-block:checked:hover:not([disabled])+label{background:#000}.is-checkradio[type=checkbox].is-black.is-block:checked:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-black.is-block:checked:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-black.is-block:checked:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-black.is-block:checked:hover:not([disabled])+label:before{border-color:#f2f2f2!important}.is-checkradio[type=checkbox].is-light.has-background-color+label::before,.is-checkradio[type=checkbox].is-light.has-background-color+label:before{border-color:transparent!important;background-color:#f5f5f5!important}.is-checkradio[type=checkbox].is-light:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-light:hover:not([disabled])+label:before{border-color:#f5f5f5!important}.is-checkradio[type=checkbox].is-light:checked+label::after,.is-checkradio[type=checkbox].is-light:checked+label:after{border-color:#f5f5f5!important}.is-checkradio[type=checkbox].is-light:checked.has-background-color+label::before,.is-checkradio[type=checkbox].is-light:checked.has-background-color+label:before{border-color:transparent!important;background-color:#f5f5f5!important}.is-checkradio[type=checkbox].is-light:checked.has-background-color+label::after,.is-checkradio[type=checkbox].is-light:checked.has-background-color+label:after{border-color:rgba(0,0,0,.7)!important;background-color:#f5f5f5!important}.is-checkradio[type=checkbox].is-light.is-block:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-light.is-block:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-light.is-block:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-light.is-block:hover:not([disabled])+label:before{border-color:#f5f5f5!important}.is-checkradio[type=checkbox].is-light.is-block:checked+label{color:rgba(0,0,0,.7);border-color:#f5f5f5!important;background:#f5f5f5}.is-checkradio[type=checkbox].is-light.is-block:checked+label::after,.is-checkradio[type=checkbox].is-light.is-block:checked+label:after{border-color:rgba(0,0,0,.7)!important}.is-checkradio[type=checkbox].is-light.is-block:checked:hover:not([disabled])+label{background:#e8e8e8}.is-checkradio[type=checkbox].is-light.is-block:checked:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-light.is-block:checked:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-light.is-block:checked:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-light.is-block:checked:hover:not([disabled])+label:before{border-color:rgba(0,0,0,.7)!important}.is-checkradio[type=checkbox].is-dark.has-background-color+label::before,.is-checkradio[type=checkbox].is-dark.has-background-color+label:before{border-color:transparent!important;background-color:#363636!important}.is-checkradio[type=checkbox].is-dark:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-dark:hover:not([disabled])+label:before{border-color:#363636!important}.is-checkradio[type=checkbox].is-dark:checked+label::after,.is-checkradio[type=checkbox].is-dark:checked+label:after{border-color:#363636!important}.is-checkradio[type=checkbox].is-dark:checked.has-background-color+label::before,.is-checkradio[type=checkbox].is-dark:checked.has-background-color+label:before{border-color:transparent!important;background-color:#363636!important}.is-checkradio[type=checkbox].is-dark:checked.has-background-color+label::after,.is-checkradio[type=checkbox].is-dark:checked.has-background-color+label:after{border-color:#fff!important;background-color:#363636!important}.is-checkradio[type=checkbox].is-dark.is-block:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-dark.is-block:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-dark.is-block:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-dark.is-block:hover:not([disabled])+label:before{border-color:#363636!important}.is-checkradio[type=checkbox].is-dark.is-block:checked+label{color:#fff;border-color:#363636!important;background:#363636}.is-checkradio[type=checkbox].is-dark.is-block:checked+label::after,.is-checkradio[type=checkbox].is-dark.is-block:checked+label:after{border-color:#fff!important}.is-checkradio[type=checkbox].is-dark.is-block:checked:hover:not([disabled])+label{background:#292929}.is-checkradio[type=checkbox].is-dark.is-block:checked:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-dark.is-block:checked:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-dark.is-block:checked:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-dark.is-block:checked:hover:not([disabled])+label:before{border-color:#f2f2f2!important}.is-checkradio[type=checkbox].is-primary.has-background-color+label::before,.is-checkradio[type=checkbox].is-primary.has-background-color+label:before{border-color:transparent!important;background-color:#00d1b2!important}.is-checkradio[type=checkbox].is-primary:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-primary:hover:not([disabled])+label:before{border-color:#00d1b2!important}.is-checkradio[type=checkbox].is-primary:checked+label::after,.is-checkradio[type=checkbox].is-primary:checked+label:after{border-color:#00d1b2!important}.is-checkradio[type=checkbox].is-primary:checked.has-background-color+label::before,.is-checkradio[type=checkbox].is-primary:checked.has-background-color+label:before{border-color:transparent!important;background-color:#00d1b2!important}.is-checkradio[type=checkbox].is-primary:checked.has-background-color+label::after,.is-checkradio[type=checkbox].is-primary:checked.has-background-color+label:after{border-color:#fff!important;background-color:#00d1b2!important}.is-checkradio[type=checkbox].is-primary.is-block:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-primary.is-block:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-primary.is-block:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-primary.is-block:hover:not([disabled])+label:before{border-color:#00d1b2!important}.is-checkradio[type=checkbox].is-primary.is-block:checked+label{color:#fff;border-color:#00d1b2!important;background:#00d1b2}.is-checkradio[type=checkbox].is-primary.is-block:checked+label::after,.is-checkradio[type=checkbox].is-primary.is-block:checked+label:after{border-color:#fff!important}.is-checkradio[type=checkbox].is-primary.is-block:checked:hover:not([disabled])+label{background:#00b89c}.is-checkradio[type=checkbox].is-primary.is-block:checked:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-primary.is-block:checked:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-primary.is-block:checked:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-primary.is-block:checked:hover:not([disabled])+label:before{border-color:#f2f2f2!important}.is-checkradio[type=checkbox].is-link.has-background-color+label::before,.is-checkradio[type=checkbox].is-link.has-background-color+label:before{border-color:transparent!important;background-color:#485fc7!important}.is-checkradio[type=checkbox].is-link:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-link:hover:not([disabled])+label:before{border-color:#485fc7!important}.is-checkradio[type=checkbox].is-link:checked+label::after,.is-checkradio[type=checkbox].is-link:checked+label:after{border-color:#485fc7!important}.is-checkradio[type=checkbox].is-link:checked.has-background-color+label::before,.is-checkradio[type=checkbox].is-link:checked.has-background-color+label:before{border-color:transparent!important;background-color:#485fc7!important}.is-checkradio[type=checkbox].is-link:checked.has-background-color+label::after,.is-checkradio[type=checkbox].is-link:checked.has-background-color+label:after{border-color:#fff!important;background-color:#485fc7!important}.is-checkradio[type=checkbox].is-link.is-block:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-link.is-block:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-link.is-block:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-link.is-block:hover:not([disabled])+label:before{border-color:#485fc7!important}.is-checkradio[type=checkbox].is-link.is-block:checked+label{color:#fff;border-color:#485fc7!important;background:#485fc7}.is-checkradio[type=checkbox].is-link.is-block:checked+label::after,.is-checkradio[type=checkbox].is-link.is-block:checked+label:after{border-color:#fff!important}.is-checkradio[type=checkbox].is-link.is-block:checked:hover:not([disabled])+label{background:#3a51bb}.is-checkradio[type=checkbox].is-link.is-block:checked:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-link.is-block:checked:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-link.is-block:checked:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-link.is-block:checked:hover:not([disabled])+label:before{border-color:#f2f2f2!important}.is-checkradio[type=checkbox].is-info.has-background-color+label::before,.is-checkradio[type=checkbox].is-info.has-background-color+label:before{border-color:transparent!important;background-color:#3e8ed0!important}.is-checkradio[type=checkbox].is-info:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-info:hover:not([disabled])+label:before{border-color:#3e8ed0!important}.is-checkradio[type=checkbox].is-info:checked+label::after,.is-checkradio[type=checkbox].is-info:checked+label:after{border-color:#3e8ed0!important}.is-checkradio[type=checkbox].is-info:checked.has-background-color+label::before,.is-checkradio[type=checkbox].is-info:checked.has-background-color+label:before{border-color:transparent!important;background-color:#3e8ed0!important}.is-checkradio[type=checkbox].is-info:checked.has-background-color+label::after,.is-checkradio[type=checkbox].is-info:checked.has-background-color+label:after{border-color:#fff!important;background-color:#3e8ed0!important}.is-checkradio[type=checkbox].is-info.is-block:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-info.is-block:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-info.is-block:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-info.is-block:hover:not([disabled])+label:before{border-color:#3e8ed0!important}.is-checkradio[type=checkbox].is-info.is-block:checked+label{color:#fff;border-color:#3e8ed0!important;background:#3e8ed0}.is-checkradio[type=checkbox].is-info.is-block:checked+label::after,.is-checkradio[type=checkbox].is-info.is-block:checked+label:after{border-color:#fff!important}.is-checkradio[type=checkbox].is-info.is-block:checked:hover:not([disabled])+label{background:#3082c5}.is-checkradio[type=checkbox].is-info.is-block:checked:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-info.is-block:checked:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-info.is-block:checked:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-info.is-block:checked:hover:not([disabled])+label:before{border-color:#f2f2f2!important}.is-checkradio[type=checkbox].is-success.has-background-color+label::before,.is-checkradio[type=checkbox].is-success.has-background-color+label:before{border-color:transparent!important;background-color:#48c78e!important}.is-checkradio[type=checkbox].is-success:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-success:hover:not([disabled])+label:before{border-color:#48c78e!important}.is-checkradio[type=checkbox].is-success:checked+label::after,.is-checkradio[type=checkbox].is-success:checked+label:after{border-color:#48c78e!important}.is-checkradio[type=checkbox].is-success:checked.has-background-color+label::before,.is-checkradio[type=checkbox].is-success:checked.has-background-color+label:before{border-color:transparent!important;background-color:#48c78e!important}.is-checkradio[type=checkbox].is-success:checked.has-background-color+label::after,.is-checkradio[type=checkbox].is-success:checked.has-background-color+label:after{border-color:#fff!important;background-color:#48c78e!important}.is-checkradio[type=checkbox].is-success.is-block:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-success.is-block:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-success.is-block:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-success.is-block:hover:not([disabled])+label:before{border-color:#48c78e!important}.is-checkradio[type=checkbox].is-success.is-block:checked+label{color:#fff;border-color:#48c78e!important;background:#48c78e}.is-checkradio[type=checkbox].is-success.is-block:checked+label::after,.is-checkradio[type=checkbox].is-success.is-block:checked+label:after{border-color:#fff!important}.is-checkradio[type=checkbox].is-success.is-block:checked:hover:not([disabled])+label{background:#3abb81}.is-checkradio[type=checkbox].is-success.is-block:checked:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-success.is-block:checked:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-success.is-block:checked:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-success.is-block:checked:hover:not([disabled])+label:before{border-color:#f2f2f2!important}.is-checkradio[type=checkbox].is-warning.has-background-color+label::before,.is-checkradio[type=checkbox].is-warning.has-background-color+label:before{border-color:transparent!important;background-color:#ffe08a!important}.is-checkradio[type=checkbox].is-warning:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-warning:hover:not([disabled])+label:before{border-color:#ffe08a!important}.is-checkradio[type=checkbox].is-warning:checked+label::after,.is-checkradio[type=checkbox].is-warning:checked+label:after{border-color:#ffe08a!important}.is-checkradio[type=checkbox].is-warning:checked.has-background-color+label::before,.is-checkradio[type=checkbox].is-warning:checked.has-background-color+label:before{border-color:transparent!important;background-color:#ffe08a!important}.is-checkradio[type=checkbox].is-warning:checked.has-background-color+label::after,.is-checkradio[type=checkbox].is-warning:checked.has-background-color+label:after{border-color:rgba(0,0,0,.7)!important;background-color:#ffe08a!important}.is-checkradio[type=checkbox].is-warning.is-block:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-warning.is-block:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-warning.is-block:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-warning.is-block:hover:not([disabled])+label:before{border-color:#ffe08a!important}.is-checkradio[type=checkbox].is-warning.is-block:checked+label{color:rgba(0,0,0,.7);border-color:#ffe08a!important;background:#ffe08a}.is-checkradio[type=checkbox].is-warning.is-block:checked+label::after,.is-checkradio[type=checkbox].is-warning.is-block:checked+label:after{border-color:rgba(0,0,0,.7)!important}.is-checkradio[type=checkbox].is-warning.is-block:checked:hover:not([disabled])+label{background:#ffd970}.is-checkradio[type=checkbox].is-warning.is-block:checked:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-warning.is-block:checked:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-warning.is-block:checked:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-warning.is-block:checked:hover:not([disabled])+label:before{border-color:rgba(0,0,0,.7)!important}.is-checkradio[type=checkbox].is-danger.has-background-color+label::before,.is-checkradio[type=checkbox].is-danger.has-background-color+label:before{border-color:transparent!important;background-color:#f14668!important}.is-checkradio[type=checkbox].is-danger:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-danger:hover:not([disabled])+label:before{border-color:#f14668!important}.is-checkradio[type=checkbox].is-danger:checked+label::after,.is-checkradio[type=checkbox].is-danger:checked+label:after{border-color:#f14668!important}.is-checkradio[type=checkbox].is-danger:checked.has-background-color+label::before,.is-checkradio[type=checkbox].is-danger:checked.has-background-color+label:before{border-color:transparent!important;background-color:#f14668!important}.is-checkradio[type=checkbox].is-danger:checked.has-background-color+label::after,.is-checkradio[type=checkbox].is-danger:checked.has-background-color+label:after{border-color:#fff!important;background-color:#f14668!important}.is-checkradio[type=checkbox].is-danger.is-block:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-danger.is-block:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-danger.is-block:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-danger.is-block:hover:not([disabled])+label:before{border-color:#f14668!important}.is-checkradio[type=checkbox].is-danger.is-block:checked+label{color:#fff;border-color:#f14668!important;background:#f14668}.is-checkradio[type=checkbox].is-danger.is-block:checked+label::after,.is-checkradio[type=checkbox].is-danger.is-block:checked+label:after{border-color:#fff!important}.is-checkradio[type=checkbox].is-danger.is-block:checked:hover:not([disabled])+label{background:#ef2e55}.is-checkradio[type=checkbox].is-danger.is-block:checked:hover:not([disabled])+label::after,.is-checkradio[type=checkbox].is-danger.is-block:checked:hover:not([disabled])+label::before,.is-checkradio[type=checkbox].is-danger.is-block:checked:hover:not([disabled])+label:after,.is-checkradio[type=checkbox].is-danger.is-block:checked:hover:not([disabled])+label:before{border-color:#f2f2f2!important}.is-checkradio[type=checkbox]:indeterminate+label::after,.is-checkradio[type=checkbox]:indeterminate+label:after{display:inline-block;transform:rotate(90deg);border-bottom:none}.is-checkradio[type=checkbox]:indeterminate.is-white+label::after,.is-checkradio[type=checkbox]:indeterminate.is-white+label:after{border-color:#fff}.is-checkradio[type=checkbox]:indeterminate.is-black+label::after,.is-checkradio[type=checkbox]:indeterminate.is-black+label:after{border-color:#0a0a0a}.is-checkradio[type=checkbox]:indeterminate.is-light+label::after,.is-checkradio[type=checkbox]:indeterminate.is-light+label:after{border-color:#f5f5f5}.is-checkradio[type=checkbox]:indeterminate.is-dark+label::after,.is-checkradio[type=checkbox]:indeterminate.is-dark+label:after{border-color:#363636}.is-checkradio[type=checkbox]:indeterminate.is-primary+label::after,.is-checkradio[type=checkbox]:indeterminate.is-primary+label:after{border-color:#00d1b2}.is-checkradio[type=checkbox]:indeterminate.is-link+label::after,.is-checkradio[type=checkbox]:indeterminate.is-link+label:after{border-color:#485fc7}.is-checkradio[type=checkbox]:indeterminate.is-info+label::after,.is-checkradio[type=checkbox]:indeterminate.is-info+label:after{border-color:#3e8ed0}.is-checkradio[type=checkbox]:indeterminate.is-success+label::after,.is-checkradio[type=checkbox]:indeterminate.is-success+label:after{border-color:#48c78e}.is-checkradio[type=checkbox]:indeterminate.is-warning+label::after,.is-checkradio[type=checkbox]:indeterminate.is-warning+label:after{border-color:#ffe08a}.is-checkradio[type=checkbox]:indeterminate.is-danger+label::after,.is-checkradio[type=checkbox]:indeterminate.is-danger+label:after{border-color:#f14668}.is-checkradio[type=radio]+label::before,.is-checkradio[type=radio]+label:before{border-radius:50%}.is-checkradio[type=radio]+label::after,.is-checkradio[type=radio]+label:after{border-radius:50%;background:#00d1b2;left:0;transform:scale(.5)}.is-checkradio[type=radio]:checked.has-background-color+label::before,.is-checkradio[type=radio]:checked.has-background-color+label:before{border-color:#4a4a4a!important;background-color:#4a4a4a!important}.is-checkradio[type=radio]:checked.has-background-color+label::after,.is-checkradio[type=radio]:checked.has-background-color+label:after{border-color:#4a4a4a!important;background-color:#4a4a4a!important}.is-checkradio[type=radio].is-rtl+label{padding-left:0}.is-checkradio[type=radio].is-rtl+label::after,.is-checkradio[type=radio].is-rtl+label:after{left:auto;right:0}.is-checkradio[type=radio]+label{font-size:1rem;line-height:1.5rem;padding-left:2rem}.is-checkradio[type=radio]+label::after,.is-checkradio[type=radio]+label::before,.is-checkradio[type=radio]+label:after,.is-checkradio[type=radio]+label:before{width:1.5rem;height:1.5rem}.is-checkradio[type=radio].is-rtl+label{padding-right:2rem}.is-checkradio[type=radio].is-small+label{font-size:.75rem;line-height:1.125rem;padding-left:1.5rem}.is-checkradio[type=radio].is-small+label::after,.is-checkradio[type=radio].is-small+label::before,.is-checkradio[type=radio].is-small+label:after,.is-checkradio[type=radio].is-small+label:before{width:1.125rem;height:1.125rem}.is-checkradio[type=radio].is-small.is-rtl+label{padding-right:1.5rem}.is-checkradio[type=radio].is-medium+label{font-size:1.25rem;line-height:1.875rem;padding-left:2.5rem}.is-checkradio[type=radio].is-medium+label::after,.is-checkradio[type=radio].is-medium+label::before,.is-checkradio[type=radio].is-medium+label:after,.is-checkradio[type=radio].is-medium+label:before{width:1.875rem;height:1.875rem}.is-checkradio[type=radio].is-medium.is-rtl+label{padding-right:2.5rem}.is-checkradio[type=radio].is-large+label{font-size:1.5rem;line-height:2.25rem;padding-left:3rem}.is-checkradio[type=radio].is-large+label::after,.is-checkradio[type=radio].is-large+label::before,.is-checkradio[type=radio].is-large+label:after,.is-checkradio[type=radio].is-large+label:before{width:2.25rem;height:2.25rem}.is-checkradio[type=radio].is-large.is-rtl+label{padding-right:3rem}.is-checkradio[type=radio].is-white.has-background-color+label::before,.is-checkradio[type=radio].is-white.has-background-color+label:before{border-color:#fff!important;background-color:#fff!important}.is-checkradio[type=radio].is-white:hover:not([disabled])+label::before,.is-checkradio[type=radio].is-white:hover:not([disabled])+label:before{border-color:#fff!important}.is-checkradio[type=radio].is-white:checked+label::after,.is-checkradio[type=radio].is-white:checked+label:after{border-color:#fff!important;background-color:#fff!important}.is-checkradio[type=radio].is-white:checked.has-background-color+label::before,.is-checkradio[type=radio].is-white:checked.has-background-color+label:before{border-color:#fff!important;background-color:#fff!important}.is-checkradio[type=radio].is-white:checked.has-background-color+label::after,.is-checkradio[type=radio].is-white:checked.has-background-color+label:after{border-color:#0a0a0a!important;background-color:#0a0a0a!important}.is-checkradio[type=radio].is-black.has-background-color+label::before,.is-checkradio[type=radio].is-black.has-background-color+label:before{border-color:#0a0a0a!important;background-color:#0a0a0a!important}.is-checkradio[type=radio].is-black:hover:not([disabled])+label::before,.is-checkradio[type=radio].is-black:hover:not([disabled])+label:before{border-color:#0a0a0a!important}.is-checkradio[type=radio].is-black:checked+label::after,.is-checkradio[type=radio].is-black:checked+label:after{border-color:#0a0a0a!important;background-color:#0a0a0a!important}.is-checkradio[type=radio].is-black:checked.has-background-color+label::before,.is-checkradio[type=radio].is-black:checked.has-background-color+label:before{border-color:#0a0a0a!important;background-color:#0a0a0a!important}.is-checkradio[type=radio].is-black:checked.has-background-color+label::after,.is-checkradio[type=radio].is-black:checked.has-background-color+label:after{border-color:#fff!important;background-color:#fff!important}.is-checkradio[type=radio].is-light.has-background-color+label::before,.is-checkradio[type=radio].is-light.has-background-color+label:before{border-color:#f5f5f5!important;background-color:#f5f5f5!important}.is-checkradio[type=radio].is-light:hover:not([disabled])+label::before,.is-checkradio[type=radio].is-light:hover:not([disabled])+label:before{border-color:#f5f5f5!important}.is-checkradio[type=radio].is-light:checked+label::after,.is-checkradio[type=radio].is-light:checked+label:after{border-color:#f5f5f5!important;background-color:#f5f5f5!important}.is-checkradio[type=radio].is-light:checked.has-background-color+label::before,.is-checkradio[type=radio].is-light:checked.has-background-color+label:before{border-color:#f5f5f5!important;background-color:#f5f5f5!important}.is-checkradio[type=radio].is-light:checked.has-background-color+label::after,.is-checkradio[type=radio].is-light:checked.has-background-color+label:after{border-color:rgba(0,0,0,.7)!important;background-color:rgba(0,0,0,.7)!important}.is-checkradio[type=radio].is-dark.has-background-color+label::before,.is-checkradio[type=radio].is-dark.has-background-color+label:before{border-color:#363636!important;background-color:#363636!important}.is-checkradio[type=radio].is-dark:hover:not([disabled])+label::before,.is-checkradio[type=radio].is-dark:hover:not([disabled])+label:before{border-color:#363636!important}.is-checkradio[type=radio].is-dark:checked+label::after,.is-checkradio[type=radio].is-dark:checked+label:after{border-color:#363636!important;background-color:#363636!important}.is-checkradio[type=radio].is-dark:checked.has-background-color+label::before,.is-checkradio[type=radio].is-dark:checked.has-background-color+label:before{border-color:#363636!important;background-color:#363636!important}.is-checkradio[type=radio].is-dark:checked.has-background-color+label::after,.is-checkradio[type=radio].is-dark:checked.has-background-color+label:after{border-color:#fff!important;background-color:#fff!important}.is-checkradio[type=radio].is-primary.has-background-color+label::before,.is-checkradio[type=radio].is-primary.has-background-color+label:before{border-color:#00d1b2!important;background-color:#00d1b2!important}.is-checkradio[type=radio].is-primary:hover:not([disabled])+label::before,.is-checkradio[type=radio].is-primary:hover:not([disabled])+label:before{border-color:#00d1b2!important}.is-checkradio[type=radio].is-primary:checked+label::after,.is-checkradio[type=radio].is-primary:checked+label:after{border-color:#00d1b2!important;background-color:#00d1b2!important}.is-checkradio[type=radio].is-primary:checked.has-background-color+label::before,.is-checkradio[type=radio].is-primary:checked.has-background-color+label:before{border-color:#00d1b2!important;background-color:#00d1b2!important}.is-checkradio[type=radio].is-primary:checked.has-background-color+label::after,.is-checkradio[type=radio].is-primary:checked.has-background-color+label:after{border-color:#fff!important;background-color:#fff!important}.is-checkradio[type=radio].is-link.has-background-color+label::before,.is-checkradio[type=radio].is-link.has-background-color+label:before{border-color:#485fc7!important;background-color:#485fc7!important}.is-checkradio[type=radio].is-link:hover:not([disabled])+label::before,.is-checkradio[type=radio].is-link:hover:not([disabled])+label:before{border-color:#485fc7!important}.is-checkradio[type=radio].is-link:checked+label::after,.is-checkradio[type=radio].is-link:checked+label:after{border-color:#485fc7!important;background-color:#485fc7!important}.is-checkradio[type=radio].is-link:checked.has-background-color+label::before,.is-checkradio[type=radio].is-link:checked.has-background-color+label:before{border-color:#485fc7!important;background-color:#485fc7!important}.is-checkradio[type=radio].is-link:checked.has-background-color+label::after,.is-checkradio[type=radio].is-link:checked.has-background-color+label:after{border-color:#fff!important;background-color:#fff!important}.is-checkradio[type=radio].is-info.has-background-color+label::before,.is-checkradio[type=radio].is-info.has-background-color+label:before{border-color:#3e8ed0!important;background-color:#3e8ed0!important}.is-checkradio[type=radio].is-info:hover:not([disabled])+label::before,.is-checkradio[type=radio].is-info:hover:not([disabled])+label:before{border-color:#3e8ed0!important}.is-checkradio[type=radio].is-info:checked+label::after,.is-checkradio[type=radio].is-info:checked+label:after{border-color:#3e8ed0!important;background-color:#3e8ed0!important}.is-checkradio[type=radio].is-info:checked.has-background-color+label::before,.is-checkradio[type=radio].is-info:checked.has-background-color+label:before{border-color:#3e8ed0!important;background-color:#3e8ed0!important}.is-checkradio[type=radio].is-info:checked.has-background-color+label::after,.is-checkradio[type=radio].is-info:checked.has-background-color+label:after{border-color:#fff!important;background-color:#fff!important}.is-checkradio[type=radio].is-success.has-background-color+label::before,.is-checkradio[type=radio].is-success.has-background-color+label:before{border-color:#48c78e!important;background-color:#48c78e!important}.is-checkradio[type=radio].is-success:hover:not([disabled])+label::before,.is-checkradio[type=radio].is-success:hover:not([disabled])+label:before{border-color:#48c78e!important}.is-checkradio[type=radio].is-success:checked+label::after,.is-checkradio[type=radio].is-success:checked+label:after{border-color:#48c78e!important;background-color:#48c78e!important}.is-checkradio[type=radio].is-success:checked.has-background-color+label::before,.is-checkradio[type=radio].is-success:checked.has-background-color+label:before{border-color:#48c78e!important;background-color:#48c78e!important}.is-checkradio[type=radio].is-success:checked.has-background-color+label::after,.is-checkradio[type=radio].is-success:checked.has-background-color+label:after{border-color:#fff!important;background-color:#fff!important}.is-checkradio[type=radio].is-warning.has-background-color+label::before,.is-checkradio[type=radio].is-warning.has-background-color+label:before{border-color:#ffe08a!important;background-color:#ffe08a!important}.is-checkradio[type=radio].is-warning:hover:not([disabled])+label::before,.is-checkradio[type=radio].is-warning:hover:not([disabled])+label:before{border-color:#ffe08a!important}.is-checkradio[type=radio].is-warning:checked+label::after,.is-checkradio[type=radio].is-warning:checked+label:after{border-color:#ffe08a!important;background-color:#ffe08a!important}.is-checkradio[type=radio].is-warning:checked.has-background-color+label::before,.is-checkradio[type=radio].is-warning:checked.has-background-color+label:before{border-color:#ffe08a!important;background-color:#ffe08a!important}.is-checkradio[type=radio].is-warning:checked.has-background-color+label::after,.is-checkradio[type=radio].is-warning:checked.has-background-color+label:after{border-color:rgba(0,0,0,.7)!important;background-color:rgba(0,0,0,.7)!important}.is-checkradio[type=radio].is-danger.has-background-color+label::before,.is-checkradio[type=radio].is-danger.has-background-color+label:before{border-color:#f14668!important;background-color:#f14668!important}.is-checkradio[type=radio].is-danger:hover:not([disabled])+label::before,.is-checkradio[type=radio].is-danger:hover:not([disabled])+label:before{border-color:#f14668!important}.is-checkradio[type=radio].is-danger:checked+label::after,.is-checkradio[type=radio].is-danger:checked+label:after{border-color:#f14668!important;background-color:#f14668!important}.is-checkradio[type=radio].is-danger:checked.has-background-color+label::before,.is-checkradio[type=radio].is-danger:checked.has-background-color+label:before{border-color:#f14668!important;background-color:#f14668!important}.is-checkradio[type=radio].is-danger:checked.has-background-color+label::after,.is-checkradio[type=radio].is-danger:checked.has-background-color+label:after{border-color:#fff!important;background-color:#fff!important}
+|]
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/CSS/Own.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/CSS/Own.hs
index 8dfa3df10de..c3d19888827 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/CSS/Own.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/CSS/Own.hs
@@ -77,6 +77,10 @@ span[data-tooltip] {
margin: 30px 0;
}
+.table tbody tr:first-child td {
+ padding-top: 17px;
+}
+
.rt-view-show-hide-chart-group {
margin-top: 5px;
}
@@ -93,6 +97,10 @@ span[data-tooltip] {
font-size: 97%;
}
+.rt-view-logs-live-view-copies {
+ width: 40px;
+}
+
.rt-view-chart-area {
width: 100% !important;
}
@@ -111,6 +119,11 @@ span[data-tooltip] {
width: 45%;
}
+.rt-view-logs-live-view-modal {
+ width: 85%;
+ min-height: 65%;
+}
+
.rt-view-errors-modal {
width: 65%;
min-height: 65%;
@@ -130,6 +143,10 @@ span[data-tooltip] {
width: 60%;
}
+ .rt-view-logs-live-view-modal {
+ width: 80%;
+ }
+
.rt-view-errors-modal {
width: 70%;
}
@@ -144,6 +161,10 @@ span[data-tooltip] {
width: 70%;
}
+ .rt-view-logs-live-view-modal {
+ width: 85%;
+ }
+
.rt-view-errors-modal {
width: 75%;
}
@@ -164,6 +185,10 @@ span[data-tooltip] {
width: 80%;
}
+ .rt-view-logs-live-view-modal {
+ width: 95%;
+ }
+
.rt-view-errors-modal {
width: 85%;
}
@@ -180,8 +205,51 @@ span[data-tooltip] {
max-width: 200px;
}
-.rt-view-error-msg-input {
- max-width: 380px;
+.rt-view-logs-live-view-tbody {
+ font-family: monospace;
+ font-size: 90%;
+}
+
+.rt-view-logs-live-view-msg-timestamp {
+ font-size: 90%;
+ padding-right: 15px;
+ color: #888;
+}
+
+.rt-view-logs-live-view-msg-node {
+ font-size: 90%;
+ padding-right: 15px;
+}
+
+.rt-view-logs-live-view-msg-severity {
+ font-weight: bold;
+ background-color: #2b2929;
+ margin-right: 15px;
+}
+
+.rt-view-logs-live-view-msg-namespace {
+ color: #bcbcbc;
+ padding-right: 15px;
+}
+
+.rt-view-logs-live-view-msg-body {
+ color: #fff;
+}
+
+.rt-view-logs-live-view-node {
+ width: 9%;
+}
+
+.rt-view-logs-live-view-timestamp {
+ width: 14%;
+}
+
+.rt-view-logs-live-view-severity {
+ width: 8%;
+}
+
+.rt-view-logs-live-view-namespace {
+ width: 14%;
}
.rt-view-errors-timestamp {
@@ -200,11 +268,11 @@ span[data-tooltip] {
margin-top: 6px;
}
-.rt-view-search-errors-icon {
+.rt-view-search-logs-icon {
margin-top: 6px;
}
-.rt-view-search-errors-icon svg {
+.rt-view-search-logs-icon svg {
width: 18px;
color: whitesmoke;
}
@@ -494,6 +562,30 @@ span[data-tooltip] {
border-bottom-right-radius: 6px;
}
+.dark .rt-view-logs-live-view-title {
+ color: whitesmoke;
+}
+
+.dark .rt-view-logs-live-view-head {
+ color: whitesmoke;
+ background-color: #282841;
+ border-bottom: 1px solid #555;
+}
+
+.dark .rt-view-logs-live-view-body {
+ color: whitesmoke;
+ background-color: #131325;
+ border-bottom-left-radius: 6px;
+ border-bottom-right-radius: 6px;
+}
+
+.dark .rt-view-logs-live-view-foot {
+ color: whitesmoke;
+ background-color: #282841;
+ border-top: 1px solid #555;
+ display: block;
+}
+
.dark .rt-view-errors-title {
color: whitesmoke;
}
@@ -592,6 +684,24 @@ span[data-tooltip] {
vertical-align: middle;
}
+.dark .rt-view-logs-live-view-table {
+ background-color: #131325;
+ color: whitesmoke;
+}
+
+.dark .rt-view-logs-live-view-table td {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ border-bottom: 0px solid #444;
+}
+
+.dark .rt-view-logs-live-view-table th {
+ color: whitesmoke;
+ border-bottom: 2px solid #888;
+ vertical-align: middle;
+}
+
+
.dark .rt-view-errors-table {
background-color: #131325;
color: whitesmoke;
@@ -923,6 +1033,30 @@ span[data-tooltip] {
border-bottom-right-radius: 6px;
}
+.light .rt-view-logs-live-view-title {
+ color: #444;
+}
+
+.light .rt-view-logs-live-view-head {
+ color: #555;
+ background-color: whitesmoke;
+ border-bottom: 1px solid #bebebe;
+}
+
+.light .rt-view-logs-live-view-body {
+ color: #555;
+ background-color: #eaeaea;
+ border-bottom-left-radius: 6px;
+ border-bottom-right-radius: 6px;
+}
+
+.light .rt-view-logs-live-view-foot {
+ color: #555;
+ background-color: whitesmoke;
+ border-top: 1px solid #bebebe;
+ display: block;
+}
+
.light .rt-view-errors-title {
color: #444;
}
@@ -1021,6 +1155,23 @@ span[data-tooltip] {
vertical-align: middle;
}
+.light .rt-view-logs-live-view-table {
+ background-color: #eaeaea;
+ color: #444;
+}
+
+.light .rt-view-logs-live-view-table td {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ border-bottom: 0px solid #444;
+}
+
+.light .rt-view-logs-live-view-table th {
+ color: #444;
+ border-bottom: 2px solid #cfcfcf;
+ vertical-align: middle;
+}
+
.light .rt-view-errors-table {
background-color: #eaeaea;
color: #444;
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Charts.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Charts.hs
index 3edfc372558..ecd96ee8394 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Charts.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Charts.hs
@@ -15,6 +15,7 @@ module Cardano.Tracer.Handlers.RTView.UI.Charts
, addNodeDatasetsToCharts
, addPointsToChart
, addAllPointsToChart
+ , getSavedColorForNode
, restoreChartsSettings
, saveChartsSettings
, changeChartsToLightTheme
@@ -32,9 +33,8 @@ import Control.Exception.Extra (ignore, try_)
import Control.Monad (forM, forM_, unless, when)
import Control.Monad.Extra (whenJustM)
import Data.Aeson (decodeFileStrict', encodeFile)
-import Data.Char (isDigit)
-import Data.List (find, isInfixOf, isPrefixOf)
-import Data.List.Extra (chunksOf, lower)
+import Data.List (find, isInfixOf)
+import Data.List.Extra (chunksOf)
import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes)
import qualified Data.Set as S
@@ -332,17 +332,7 @@ getSavedColorForNode tracerEnv nodeName = do
Just colorFile ->
try_ (readFile colorFile) >>= \case
Left _ -> return Nothing
- Right code ->
- if itLooksLikeColor code
- then return . Just $ Color code
- else return Nothing
- where
- itLooksLikeColor :: String -> Bool
- itLooksLikeColor code =
- length code == 7
- && "#" `isPrefixOf` code
- && all (\c -> isDigit c || c `elem` ['a' .. 'f'] )
- (tail $ lower code)
+ Right code -> return . Just $ Color code
saveColorForNode
:: TracerEnv
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Body.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Body.hs
index 279080b0b07..a594dafdb02 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Body.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Body.hs
@@ -20,16 +20,17 @@ import Cardano.Tracer.Environment
import Cardano.Tracer.Handlers.RTView.State.Historical
import Cardano.Tracer.Handlers.RTView.UI.Charts
import Cardano.Tracer.Handlers.RTView.UI.HTML.About
+import Cardano.Tracer.Handlers.RTView.UI.HTML.Logs
import Cardano.Tracer.Handlers.RTView.UI.HTML.NoNodes
import Cardano.Tracer.Handlers.RTView.UI.HTML.Notifications
import Cardano.Tracer.Handlers.RTView.UI.Img.Icons
import Cardano.Tracer.Handlers.RTView.UI.JS.ChartJS
import qualified Cardano.Tracer.Handlers.RTView.UI.JS.Charts as Chart
-import Cardano.Tracer.Handlers.RTView.UI.JS.Utils
import Cardano.Tracer.Handlers.RTView.UI.Notifications
import Cardano.Tracer.Handlers.RTView.UI.Theme
import Cardano.Tracer.Handlers.RTView.UI.Types
import Cardano.Tracer.Handlers.RTView.UI.Utils
+import Cardano.Tracer.Handlers.RTView.Update.Logs
mkPageBody
:: TracerEnv
@@ -125,6 +126,16 @@ mkPageBody tracerEnv networkConfig dsIxs = do
on UI.click showHideResources . const $
changeVisibilityForCharts showHideResources "resources-charts" "Resources"
+ logsLiveView <- mkLogsLiveView
+ logsLiveViewButton <- UI.button ## "logs-live-view-button"
+ #. "button is-info is-medium"
+ # set text "Logs view"
+ # hideIt
+ on UI.click logsLiveViewButton . const $ do
+ fadeInModal logsLiveView
+ void $ element logsLiveView # set dataState "opened"
+ updateLogsLiveViewNodes tracerEnv
+
-- Body.
window <- askWindow
body <-
@@ -187,29 +198,14 @@ mkPageBody tracerEnv networkConfig dsIxs = do
]
, UI.tr ## "node-logs-row" #+
[ UI.td #+ [ image "rt-view-overview-icon" logsSVG
- , string "Logs"
+ , string "Logs paths"
]
]
- --, UI.tr ## "node-chunk-validation-row" #+
- -- [ UI.td #+ [ image "rt-view-overview-icon" dbSVG
- -- , string "Chunk validation"
- -- ]
- -- ]
- --, UI.tr ## "node-update-ledger-db-row" #+
- -- [ UI.td #+ [ image "rt-view-overview-icon" dbSVG
- -- , string "Ledger DB"
- -- ]
- -- ]
, UI.tr ## "node-peers-row" #+
[ UI.td #+ [ image "rt-view-overview-icon" peersSVG
, string "Peers"
]
]
- , UI.tr ## "node-errors-row" #+
- [ UI.td #+ [ image "rt-view-overview-icon" errorsSVG
- , string "Errors"
- ]
- ]
, UI.tr ## "node-leadership-row" #+
[ UI.td #+ [ image "rt-view-overview-icon" firstSVG
, string "Leadership"
@@ -236,6 +232,10 @@ mkPageBody tracerEnv networkConfig dsIxs = do
]
]
]
+ , UI.div #+
+ [ element logsLiveView
+ , element logsLiveViewButton
+ ]
]
, UI.mkElement "section" #. "section" #+
[ UI.div ## "main-charts-container"
@@ -328,7 +328,7 @@ mkPageBody tracerEnv networkConfig dsIxs = do
, UI.mkElement "script" # set UI.html chartJSPluginZoom
]
- closeModalsByEscapeButton
+ -- closeModalsByEscapeButton
Chart.prepareChartsJS
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Logs.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Logs.hs
new file mode 100644
index 00000000000..98592fa946e
--- /dev/null
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Logs.hs
@@ -0,0 +1,150 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Cardano.Tracer.Handlers.RTView.UI.HTML.Logs
+ ( mkLogsLiveView
+ ) where
+
+import Control.Monad (void)
+import qualified Graphics.UI.Threepenny as UI
+import Graphics.UI.Threepenny.Core
+
+import Cardano.Tracer.Handlers.RTView.UI.Img.Icons
+import Cardano.Tracer.Handlers.RTView.UI.Utils
+
+mkLogsLiveView :: UI Element
+mkLogsLiveView = do
+ closeIt <- UI.button #. "delete"
+
+ _searchMessagesInput <-
+ UI.input #. "input rt-view-search-messages"
+ # set UI.type_ "text"
+ # set (UI.attr "placeholder") "Search log items"
+ _searchMessages <-
+ UI.button #. "button is-info"
+ #+ [image "rt-view-search-logs-icon" searchSVG]
+
+ _whenToSearch <-
+ UI.div #. "dropdown is-hoverable" #+
+ [ UI.div #. "dropdown-trigger" #+
+ [ UI.button #. "button"
+ # set ariaHasPopup "true"
+ # set ariaControls "dropdown-menu4" #+
+ [ UI.span # set text "Last 3 days"
+ -- ICON?
+ ]
+ ]
+ , UI.div ## "dropdown-menu4"
+ #. "dropdown-menu"
+ # set role "menu" #+
+ [ UI.div #. "dropdown-content" #+
+ [ UI.div #. "dropdown-item" #+
+ [ UI.p # set text "TEST ME"
+ ]
+ ]
+ ]
+ ]
+
+ _whereToSearch <-
+ UI.div #. "dropdown is-hoverable" #+
+ [ UI.div #. "dropdown-trigger" #+
+ [ UI.button #. "button"
+ # set ariaHasPopup "true"
+ # set ariaControls "dropdown-menu4" #+
+ [ UI.span # set text "In message"
+ -- ICON?
+ ]
+ ]
+ , UI.div ## "dropdown-menu4"
+ #. "dropdown-menu"
+ # set role "menu" #+
+ [ UI.div #. "dropdown-content" #+
+ [ UI.div #. "dropdown-item" #+
+ [ UI.p # set text "TEST ME"
+ ]
+ ]
+ ]
+ ]
+
+ fontSetter <-
+ UI.input # set UI.type_ "range"
+ # set min_ "1"
+ # set max_ "4"
+ # set value "3"
+ on change fontSetter . const $ do
+ window <- askWindow
+ fontSize <-
+ get value fontSetter >>= \case
+ "1" -> return "70%"
+ "2" -> return "80%"
+ "3" -> return "90%"
+ "4" -> return "100%"
+ _ -> return "90%"
+ findAndSet (set style [("font-size", fontSize)]) window "node-logs-live-view-tbody"
+
+ logsLiveViewTable <-
+ UI.div ## "logs-live-view-modal-window" #. "modal" # set dataState "closed" #+
+ [ UI.div #. "modal-background" #+ []
+ , UI.div #. "modal-card rt-view-logs-live-view-modal" #+
+ [ UI.header #. "modal-card-head rt-view-logs-live-view-head" #+
+ [ UI.p #. "modal-card-title rt-view-logs-live-view-title" #+
+ [ string "Log items from connected nodes"
+ ]
+ , element closeIt
+ ]
+ , UI.mkElement "section" #. "modal-card-body rt-view-logs-live-view-body" #+
+ [ UI.div #. "columns" #+
+ [ UI.div #. "column is-8" #+
+ [ UI.div #. "bd-notification" #+
+ [ UI.div ## "logs-live-view-nodes-checkboxes" #. "field" #+ []
+ ]
+ ]
+ , UI.div #. "column has-text-right" #+
+ [ string "A" #. "is-size-5 mr-2"
+ , element fontSetter
+ , string "A" #. "is-size-3 ml-2"
+ ]
+ ]
+ , UI.div ## "logs-live-view-table-container" #. "table-container" #+
+ [ UI.div ## "node-logs-live-view-tbody"
+ #. "rt-view-logs-live-view-tbody"
+ # set dataState "0"
+ #+ []
+ ]
+ ]
+ {-
+ , UI.mkElement "footer" #. "modal-card-foot rt-view-logs-live-view-foot" #+
+ [ UI.div #. "columns" #+
+ [ UI.div #. "column" #+
+ [ UI.div #. "field has-addons" #+
+ [ UI.p #. "control" #+
+ [ element whenToSearch
+ ]
+ , UI.p #. "control" #+
+ [ element whereToSearch
+ ]
+ , UI.p #. "control" #+
+ [ element searchMessagesInput
+ ]
+ , UI.p #. "control" #+
+ [ element searchMessages
+ ]
+ ]
+ ]
+ --, UI.div #. "column has-text-right" #+
+ -- [ element exportToJSON
+ -- ]
+ ]
+ ]
+ -}
+ ]
+ ]
+ on UI.click closeIt . const $ do
+ void $ element logsLiveViewTable #. "modal"
+ void $ element logsLiveViewTable # set dataState "closed"
+
+ return logsLiveViewTable
+
+change :: Element -> Event ()
+change = void . domEvent "change"
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Main.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Main.hs
index b5977a9d9ad..012fa8d2e9b 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Main.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Main.hs
@@ -17,8 +17,8 @@ import Cardano.Tracer.Configuration
import Cardano.Tracer.Environment
import Cardano.Tracer.Handlers.RTView.State.Displayed
import Cardano.Tracer.Handlers.RTView.State.EraSettings
-import Cardano.Tracer.Handlers.RTView.State.Errors
import Cardano.Tracer.Handlers.RTView.State.Peers
+import Cardano.Tracer.Handlers.RTView.State.TraceObjects
import Cardano.Tracer.Handlers.RTView.UI.Charts
import Cardano.Tracer.Handlers.RTView.UI.CSS.Bulma
import Cardano.Tracer.Handlers.RTView.UI.CSS.Own
@@ -28,8 +28,8 @@ import Cardano.Tracer.Handlers.RTView.UI.Notifications
import Cardano.Tracer.Handlers.RTView.UI.Theme
import Cardano.Tracer.Handlers.RTView.UI.Utils
import Cardano.Tracer.Handlers.RTView.Update.EKG
-import Cardano.Tracer.Handlers.RTView.Update.Errors
import Cardano.Tracer.Handlers.RTView.Update.KES
+import Cardano.Tracer.Handlers.RTView.Update.Logs
import Cardano.Tracer.Handlers.RTView.Update.Nodes
import Cardano.Tracer.Handlers.RTView.Update.NodeState
import Cardano.Tracer.Handlers.RTView.Update.Peers
@@ -43,11 +43,10 @@ mkMainPage
-> PageReloadedFlag
-> NonEmpty LoggingParams
-> Network
- -> Errors
-> UI.Window
-> UI ()
mkMainPage tracerEnv displayedElements nodesEraSettings reloadFlag
- loggingConfig networkConfig nodesErrors window = do
+ loggingConfig networkConfig window = do
void $ return window # set UI.title pageTitle
void $ UI.getHead window #+
[ UI.link # set UI.rel "icon"
@@ -60,6 +59,7 @@ mkMainPage tracerEnv displayedElements nodesEraSettings reloadFlag
, UI.mkElement "style" # set UI.html bulmaPageloaderCSS
, UI.mkElement "style" # set UI.html bulmaSwitchCSS
, UI.mkElement "style" # set UI.html bulmaDividerCSS
+ , UI.mkElement "style" # set UI.html bulmaCheckboxCSS
, UI.mkElement "style" # set UI.html ownCSS
]
@@ -95,25 +95,25 @@ mkMainPage tracerEnv displayedElements nodesEraSettings reloadFlag
UI.stop uiNoNodesProgressTimer
findAndSet hiddenOnly window elId
- uiErrorsTimer <- UI.timer # set UI.interval 3000
- on UI.tick uiErrorsTimer . const $
- updateNodesErrors tracerEnv nodesErrors
-
whenM (liftIO $ readTVarIO reloadFlag) $ do
liftIO $ cleanupDisplayedValues displayedElements
updateUIAfterReload
tracerEnv
- displayedElements
+ displayedElements
loggingConfig
colors
datasetIndices
- nodesErrors
- uiErrorsTimer
uiNoNodesProgressTimer
liftIO $ pageWasNotReload reloadFlag
+ llvCounters <- liftIO initLogsLiveViewCounters
+
+ uiLogsLiveViewTimer <- UI.timer # set UI.interval 1000
+ on UI.tick uiLogsLiveViewTimer . const $
+ updateLogsLiveViewItems tracerEnv llvCounters
+
-- Uptime is a real-time clock, so update it every second.
uiUptimeTimer <- UI.timer # set UI.interval 1000
on UI.tick uiUptimeTimer . const $
@@ -124,7 +124,7 @@ mkMainPage tracerEnv displayedElements nodesEraSettings reloadFlag
updateEKGMetrics tracerEnv
uiNodesTimer <- UI.timer # set UI.interval 1000
- on UI.tick uiNodesTimer . const $
+ on UI.tick uiNodesTimer . const $ do
updateNodesUI
tracerEnv
displayedElements
@@ -132,8 +132,6 @@ mkMainPage tracerEnv displayedElements nodesEraSettings reloadFlag
loggingConfig
colors
datasetIndices
- nodesErrors
- uiErrorsTimer
uiNoNodesProgressTimer
uiPeersTimer <- UI.timer # set UI.interval 4000
@@ -142,20 +140,20 @@ mkMainPage tracerEnv displayedElements nodesEraSettings reloadFlag
updateNodesPeers tracerEnv peers
updateKESInfo tracerEnv nodesEraSettings displayedElements
+ UI.start uiLogsLiveViewTimer
UI.start uiUptimeTimer
UI.start uiNodesTimer
UI.start uiPeersTimer
- UI.start uiErrorsTimer
UI.start uiEKGTimer
UI.start uiNoNodesProgressTimer
on UI.disconnect window . const $ do
webPageIsClosed tracerEnv
+ UI.stop uiLogsLiveViewTimer
UI.stop uiNodesTimer
UI.stop uiUptimeTimer
UI.stop uiPeersTimer
UI.stop uiEKGTimer
- UI.stop uiErrorsTimer
UI.stop uiNoNodesProgressTimer
liftIO $ pageWasReload reloadFlag
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Node/Column.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Node/Column.hs
index 6f7bf15416e..062c3deafd8 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Node/Column.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Node/Column.hs
@@ -16,9 +16,7 @@ import System.FilePath ((>))
import Cardano.Tracer.Configuration
import Cardano.Tracer.Environment
-import Cardano.Tracer.Handlers.RTView.State.Errors
import Cardano.Tracer.Handlers.RTView.UI.HTML.Node.EKG
-import Cardano.Tracer.Handlers.RTView.UI.HTML.Node.Errors
import Cardano.Tracer.Handlers.RTView.UI.HTML.Node.Peers
import Cardano.Tracer.Handlers.RTView.UI.Img.Icons
import Cardano.Tracer.Handlers.RTView.UI.JS.Utils
@@ -30,11 +28,9 @@ import Cardano.Tracer.Utils
addNodeColumn
:: TracerEnv
-> NonEmpty LoggingParams
- -> Errors
- -> UI.Timer
-> NodeId
-> UI ()
-addNodeColumn tracerEnv loggingConfig nodesErrors updateErrorsTimer nodeId@(NodeId anId) = do
+addNodeColumn tracerEnv loggingConfig nodeId@(NodeId anId) = do
nodeName <- liftIO $ askNodeName tracerEnv nodeId
let id' = unpack anId
@@ -55,13 +51,6 @@ addNodeColumn tracerEnv loggingConfig nodesErrors updateErrorsTimer nodeId@(Node
# set text "Details"
on UI.click peersDetailsButton . const $ fadeInModal peersTable
- errorsTable <- mkErrorsTable tracerEnv nodeId nodesErrors updateErrorsTimer
- errorsDetailsButton <- UI.button ## (id' <> "__node-errors-details-button")
- #. "button is-danger"
- # set UI.enabled False
- # set text "Details"
- on UI.click errorsDetailsButton . const $ fadeInModal errorsTable
-
ekgMetricsWindow <- mkEKGMetricsWindow id'
ekgMetricsButton <- UI.button ## (id' <> "__node-ekg-metrics-button")
#. "button is-info"
@@ -94,12 +83,6 @@ addNodeColumn tracerEnv loggingConfig nodesErrors updateErrorsTimer nodeId@(Node
addNodeCell "start-time" st
addNodeCell "uptime" ut
addNodeCell "logs" ls
- --addNodeCell "chunk-validation" [ UI.span ## (id' <> "__node-chunk-validation")
- -- # set text "—"
- -- ]
- --addNodeCell "update-ledger-db" [ UI.span ## (id' <> "__node-update-ledger-db")
- -- # set html "0 %"
- -- ]
addNodeCell "peers" [ UI.div #. "buttons has-addons" #+
[ UI.button ## (id' <> "__node-peers-num")
#. "button is-static"
@@ -108,14 +91,6 @@ addNodeColumn tracerEnv loggingConfig nodesErrors updateErrorsTimer nodeId@(Node
]
, element peersTable
]
- addNodeCell "errors" [ UI.div #. "buttons has-addons" #+
- [ UI.button ## (id' <> "__node-errors-num")
- #. "button is-static"
- # set text "0"
- , element errorsDetailsButton
- ]
- , element errorsTable
- ]
addNodeCell "leadership" leadership
addNodeCell "kes" kes
addNodeCell "op-cert" opCert
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Node/Errors.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Node/Errors.hs
deleted file mode 100644
index 3d071570b55..00000000000
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/HTML/Node/Errors.hs
+++ /dev/null
@@ -1,136 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
-module Cardano.Tracer.Handlers.RTView.UI.HTML.Node.Errors
- ( mkErrorsTable
- ) where
-
-import Control.Monad (when)
-import Control.Monad.Extra (unlessM)
-import Data.Text (unpack)
-import qualified Graphics.UI.Threepenny as UI
-import Graphics.UI.Threepenny.Core
-
-import Cardano.Tracer.Environment
-import Cardano.Tracer.Handlers.RTView.State.Errors
-import Cardano.Tracer.Handlers.RTView.UI.Img.Icons
-import Cardano.Tracer.Handlers.RTView.UI.Utils
-import Cardano.Tracer.Handlers.RTView.Update.Errors
-import Cardano.Tracer.Types
-import Cardano.Tracer.Utils
-
-mkErrorsTable
- :: TracerEnv
- -> NodeId
- -> Errors
- -> UI.Timer
- -> UI Element
-mkErrorsTable tracerEnv nodeId@(NodeId anId) nodesErrors updateErrorsTimer = do
- window <- askWindow
- let id' = unpack anId
- closeIt <- UI.button #. "delete"
- deleteAll <- image "has-tooltip-multiline has-tooltip-left rt-view-delete-errors-icon" trashSVG
- # set dataTooltip "Click to delete all errors. This action cannot be undone!"
- on UI.click deleteAll . const $
- deleteAllErrorMessages window nodeId nodesErrors
-
- searchMessagesInput <- UI.input #. "input rt-view-search-messages"
- # set UI.type_ "text"
- # set (UI.attr "placeholder") "Search messages"
- searchMessages <- UI.button #. "button is-info"
- #+ [image "rt-view-search-errors-icon" searchSVG]
-
- -- If the user clicked the search button.
- on UI.click searchMessages . const $
- searchErrorMessages window searchMessagesInput nodeId nodesErrors updateErrorsTimer
- -- If the user hits Enter key.
- on UI.keyup searchMessagesInput $ \keyCode ->
- when (keyCode == 13) $
- searchErrorMessages window searchMessagesInput nodeId nodesErrors updateErrorsTimer
- -- If the user changed text in input...
- on UI.valueChange searchMessagesInput $ \inputText ->
- when (null inputText) $
- -- ... and this text is empty, it means that search/filter mode is off,
- -- so remove "search result" and start the timer for update errors again.
- unlessM (get UI.running updateErrorsTimer) $
- -- Ok, the timer is stopped, so we are in search/filter mode already, exit it.
- exitSearchMode window nodeId updateErrorsTimer
-
- sortByTimeIcon <- image "has-tooltip-multiline has-tooltip-right rt-view-sort-icon" sortSVG
- # set dataTooltip "Click to sort errors by time"
- # set dataState "asc"
- on UI.click sortByTimeIcon . const $
- sortErrorsByTime window nodeId sortByTimeIcon nodesErrors
-
- sortBySeverityIcon <- image "has-tooltip-multiline has-tooltip-right rt-view-sort-icon" sortSVG
- # set dataTooltip "Click to sort errors by severity"
- # set dataState "asc"
- on UI.click sortBySeverityIcon . const $
- sortErrorsBySeverity window nodeId sortBySeverityIcon nodesErrors
-
- exportToJSON <- image "has-tooltip-multiline has-tooltip-left rt-view-export-icon" exportSVG
- # set dataTooltip "Click to export errors to JSON-file"
- on UI.click exportToJSON . const $
- liftIO (askNodeName tracerEnv nodeId) >>= exportErrorsToJSONFile nodesErrors nodeId
-
- errorsTable <-
- UI.div #. "modal" #+
- [ UI.div #. "modal-background" #+ []
- , UI.div #. "modal-card rt-view-errors-modal" #+
- [ UI.header #. "modal-card-head rt-view-errors-head" #+
- [ UI.p #. "modal-card-title rt-view-errors-title" #+
- [ string "Errors from "
- , UI.span ## (id' <> "__node-name-for-errors")
- #. "has-text-weight-bold"
- # set text id'
- ]
- , element closeIt
- ]
- , UI.mkElement "section" #. "modal-card-body rt-view-errors-body" #+
- [ UI.div ## (id' <> "__errors-table-container") #. "table-container" #+
- [ UI.table ## (id' <> "__errors-table") #. "table is-fullwidth rt-view-errors-table" #+
- [ UI.mkElement "thead" #+
- [ UI.tr #+
- [ UI.th #. "rt-view-errors-timestamp" #+
- [ string "Timestamp"
- , element sortByTimeIcon
- ]
- , UI.th #. "rt-view-errors-severity" #+
- [ string "Severity"
- , element sortBySeverityIcon
- ]
- , UI.th #+
- [ string "Message"
- ]
- , UI.th #+
- [ element deleteAll
- ]
- ]
- ]
- , UI.mkElement "tbody" ## (id' <> "__node-errors-tbody")
- # set dataState "0"
- #+ []
- ]
- ]
- ]
- , UI.mkElement "footer" #. "modal-card-foot rt-view-errors-foot" #+
- [ UI.div #. "columns" #+
- [ UI.div #. "column" #+
- [ UI.div #. "field has-addons" #+
- [ UI.p #. "control" #+
- [ element searchMessagesInput
- ]
- , UI.p #. "control" #+
- [ element searchMessages
- ]
- ]
- ]
- , UI.div #. "column has-text-right" #+
- [ element exportToJSON
- ]
- ]
- ]
- ]
- ]
- on UI.click closeIt . const $ element errorsTable #. "modal"
- return errorsTable
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Img/Icons.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Img/Icons.hs
index dd4d77bf543..537133697d9 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Img/Icons.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Img/Icons.hs
@@ -35,6 +35,7 @@ module Cardano.Tracer.Handlers.RTView.UI.Img.Icons
, systemStartSVG
, uptimeSVG
, logsSVG
+ , logsAccessSVG
, directorySVG
, copySVG
, externalLinkSVG
@@ -270,6 +271,11 @@ logsSVG = [s|
|]
+logsAccessSVG :: String
+logsAccessSVG = [s|
+
+|]
+
directorySVG :: String
directorySVG = [s|
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Utils.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Utils.hs
index 41d734755ef..42aca6e0749 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Utils.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/UI/Utils.hs
@@ -5,10 +5,14 @@
module Cardano.Tracer.Handlers.RTView.UI.Utils
( (##)
+ , ariaControls
+ , ariaHasPopup
, dataAction
, dataParent
, dataState
, dataTooltip
+ , min_
+ , max_
, findByClassAndDo
, findAndDo
, findAndSet
@@ -33,12 +37,13 @@ module Cardano.Tracer.Handlers.RTView.UI.Utils
, visibleOnly
, pageTitle
, pageTitleNotify
+ , role
, shortenName
, shortenPath
, setDisplayedValue
, delete'
, fadeInModal
- , exportErrorsToJSONFile
+ -- , exportErrorsToJSONFile
, shownState
, hiddenState
, webPageIsOpened
@@ -52,8 +57,6 @@ import Control.Monad.Extra (whenJustM)
import Data.String.QQ
import Data.Text (Text, unpack)
import qualified Data.Text as T
-import Data.Time.Clock.System (getSystemTime, systemToUTCTime)
-import Data.Time.Format (defaultTimeLocale, formatTime)
import qualified Foreign.JavaScript as JS
import qualified Foreign.RemotePtr as Foreign
import qualified Graphics.UI.Threepenny as UI
@@ -62,8 +65,6 @@ import Graphics.UI.Threepenny.JQuery (Easing (..), fadeIn, fadeOut)
import Cardano.Tracer.Environment
import Cardano.Tracer.Handlers.RTView.State.Displayed
-import Cardano.Tracer.Handlers.RTView.State.Errors
-import Cardano.Tracer.Handlers.RTView.UI.JS.Utils
import Cardano.Tracer.Types
(##) :: UI Element -> String -> UI Element
@@ -210,6 +211,18 @@ pageTitle, pageTitleNotify :: String
pageTitle = "Cardano RTView"
pageTitleNotify = "(!) Cardano RTView"
+min_ :: WriteAttr Element String
+min_ = mkWriteAttr $ set' (attr "min")
+
+max_ :: WriteAttr Element String
+max_ = mkWriteAttr $ set' (attr "max")
+
+ariaHasPopup :: WriteAttr Element String
+ariaHasPopup = mkWriteAttr $ set' (attr "aria-haspopup")
+
+ariaControls :: WriteAttr Element String
+ariaControls = mkWriteAttr $ set' (attr "aria-controls")
+
dataTooltip :: WriteAttr Element String
dataTooltip = mkWriteAttr $ set' (attr "data-tooltip")
@@ -228,6 +241,9 @@ dataAttr name = mkReadWriteAttr getData setData
getData el = callFunction $ ffi "$(%1).data(%2)" el name
setData v el = runFunction $ ffi "$(%1).data(%2,%3)" el name v
+role :: WriteAttr Element String
+role = mkWriteAttr $ set' (attr "role")
+
image :: String -> String -> UI Element
image imgClass svg = UI.span #. imgClass # set html svg
@@ -271,6 +287,7 @@ fadeInModal modal = do
void $ element modal #. "modal is-active"
fadeIn modal 150 Swing $ return ()
+{-
exportErrorsToJSONFile
:: Errors
-> NodeId
@@ -282,6 +299,7 @@ exportErrorsToJSONFile nodesErrors nodeId nodeName =
let nowF = formatTime defaultTimeLocale "%FT%T%z" now
fileName = "node-" <> unpack nodeName <> "-errors-" <> nowF <> ".json"
downloadJSONFile fileName errorsAsJSON
+-}
webPageIsOpened, webPageIsClosed :: TracerEnv -> UI ()
webPageIsOpened TracerEnv{teRTViewPageOpened} = setFlag teRTViewPageOpened True
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Errors.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Errors.hs
deleted file mode 100644
index d0fb94d50eb..00000000000
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Errors.hs
+++ /dev/null
@@ -1,219 +0,0 @@
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
-module Cardano.Tracer.Handlers.RTView.Update.Errors
- ( runErrorsUpdater
- , updateNodesErrors
- , searchErrorMessages
- , deleteAllErrorMessages
- , exitSearchMode
- , sortErrorsByTime
- , sortErrorsBySeverity
- ) where
-
-import Control.Concurrent.STM.TVar (readTVarIO)
-import Control.Monad (forM, forM_, forever, unless, void, when)
-import Control.Monad.Extra (whenJust, whenJustM)
-import qualified Data.Map.Strict as M
-import qualified Data.Text as T
-import Data.Time.Format (defaultTimeLocale, formatTime)
-import qualified Graphics.UI.Threepenny as UI
-import Graphics.UI.Threepenny.Core
-import System.Time.Extra (sleep)
-import Text.Read (readMaybe)
-
-import Cardano.Logging (SeverityS (..))
-
-import Cardano.Tracer.Environment
-import Cardano.Tracer.Handlers.RTView.Notifications.Check
-import Cardano.Tracer.Handlers.RTView.State.Errors
-import Cardano.Tracer.Handlers.RTView.UI.Img.Icons
-import Cardano.Tracer.Handlers.RTView.UI.JS.Utils
-import Cardano.Tracer.Handlers.RTView.UI.Utils
-import Cardano.Tracer.Handlers.RTView.Utils
-import Cardano.Tracer.Types
-import Cardano.Tracer.Utils
-
-runErrorsUpdater
- :: TracerEnv
- -> Errors
- -> IO ()
-runErrorsUpdater tracerEnv nodesErrors = forever $ do
- sleep 2.0
- savedTraceObjects <- readTVarIO teSavedTO
- forConnected_ tracerEnv $ \nodeId ->
- whenJust (M.lookup nodeId savedTraceObjects) $ \savedTOForNode ->
- forM_ (M.toList savedTOForNode) $ \(_, trObInfo@(_, severity, _)) ->
- when (itIsError severity) $ do
- addError nodesErrors nodeId trObInfo
- checkCommonErrors nodeId trObInfo teEventsQueues
- where
- TracerEnv{teSavedTO, teEventsQueues} = tracerEnv
-
- itIsError sev =
- case sev of
- Warning -> True
- Error -> True
- Critical -> True
- Alert -> True
- Emergency -> True
- _ -> False
-
--- | Update error messages in a corresponding modal window.
-updateNodesErrors
- :: TracerEnv
- -> Errors
- -> UI ()
-updateNodesErrors tracerEnv nodesErrors = do
- window <- askWindow
- forConnectedUI_ tracerEnv $ \nodeId@(NodeId anId) -> do
- errorsFromNode <- liftIO $ getErrors nodesErrors nodeId
- unless (null errorsFromNode) $ do
- -- Update errors number (as it is in the state).
- setTextValue (anId <> "__node-errors-num") (showT $ length errorsFromNode)
- -- Enable 'Details' button.
- findAndSet (set UI.enabled True) window (anId <> "__node-errors-details-button")
- -- Add errors if needed.
- whenJustM (UI.getElementById window (T.unpack anId <> "__node-errors-tbody")) $ \el ->
- whenJustM (readMaybe <$> get dataState el) $ \(numberOfDisplayedRows :: Int) -> do
- let onlyNewErrors = drop numberOfDisplayedRows errorsFromNode
- doAddErrorRows nodeId onlyNewErrors el numberOfDisplayedRows
-
-doAddErrorRows
- :: NodeId
- -> [ErrorInfo]
- -> Element
- -> Int
- -> UI ()
-doAddErrorRows nodeId errorsToAdd parentEl numberOfDisplayedRows = do
- errorRows <-
- forM errorsToAdd $ \(errorIx, (msg, sev, ts)) ->
- mkErrorRow errorIx nodeId msg sev ts
- -- Add them actually and remember their new number.
- let newNumberOfDisplayedRows = numberOfDisplayedRows + length errorsToAdd
- void $ element parentEl # set dataState (show newNumberOfDisplayedRows)
- #+ errorRows
- where
- mkErrorRow _errorIx (NodeId anId) msg sev ts = do
- copyErrorIcon <- image "has-tooltip-multiline has-tooltip-left rt-view-copy-icon" copySVG
- # set dataTooltip "Click to copy this error"
- on UI.click copyErrorIcon . const $
- copyTextToClipboard $ errorToCopy ts sev msg
-
- return $
- UI.tr #. (T.unpack anId <> "-node-error-row") #+
- [ UI.td #+
- [ UI.span # set text (preparedTS ts)
- ]
- , UI.td #+
- [ UI.span #. "tag is-medium is-danger" # set text (show sev)
- ]
- , UI.td #+
- [ UI.p #. "control" #+
- [ UI.input #. "input rt-view-error-msg-input"
- # set UI.type_ "text"
- # set (UI.attr "readonly") "readonly"
- # set UI.value (T.unpack msg)
- ]
- ]
- , UI.td #+
- [ element copyErrorIcon
- ]
- ]
-
- preparedTS = formatTime defaultTimeLocale "%b %e, %Y %T"
-
- errorToCopy ts sev msg = "[" <> preparedTS ts <> "] [" <> show sev <> "] [" <> T.unpack msg <> "]"
-
-searchErrorMessages
- :: UI.Window
- -> Element
- -> NodeId
- -> Errors
- -> UI.Timer
- -> UI ()
-searchErrorMessages window searchInput nodeId@(NodeId anId) nodesErrors updateErrorsTimer = do
- textToSearch <- T.strip . T.pack <$> get value searchInput
- unless (T.null textToSearch) $ do
- -- Ok, there is non-empty text we want to search. It means that now we are
- -- in search/filter mode, and during this period the new messages shouldn't be added,
- -- so we stop update timer temporarily.
- UI.stop updateErrorsTimer
- liftIO (getErrorsFilteredByText textToSearch nodesErrors nodeId) >>= \case
- [] -> do
- -- There is nothing found. So we have to inform the user that
- -- there is no corresponding errors.
- findByClassAndDo window (anId <> "-node-error-row") delete'
- -- Reset number of currently displayed errors rows.
- whenJustM (UI.getElementById window (T.unpack anId <> "__node-errors-tbody")) $ \el ->
- void $ element el # set dataState "0"
- foundErrors -> do
- -- Delete displayed errors from window.
- findByClassAndDo window (anId <> "-node-error-row") delete'
- -- Do add found errors.
- whenJustM (UI.getElementById window (T.unpack anId <> "__node-errors-tbody")) $ \el ->
- doAddErrorRows nodeId foundErrors el 0
-
-deleteAllErrorMessages
- :: UI.Window
- -> NodeId
- -> Errors
- -> UI ()
-deleteAllErrorMessages window nodeId@(NodeId anId) nodesErrors = do
- -- Delete errors from window.
- findByClassAndDo window (anId <> "-node-error-row") delete'
- -- Delete errors from state.
- liftIO $ deleteAllErrors nodesErrors nodeId
- -- Reset number of errors and disable Detail button.
- setTextValue (anId <> "__node-errors-num") "0"
- findAndSet (set UI.enabled False) window (anId <> "__node-errors-details-button")
- -- Reset number of currently displayed errors rows.
- whenJustM (UI.getElementById window (T.unpack anId <> "__node-errors-tbody")) $ \el ->
- void $ element el # set dataState "0"
-
-exitSearchMode
- :: UI.Window
- -> NodeId
- -> UI.Timer
- -> UI ()
-exitSearchMode window (NodeId anId) updateErrorsTimer = do
- -- Delete errors (last search result) from window.
- findByClassAndDo window (anId <> "-node-error-row") delete'
- -- Reset number of currently displayed errors rows.
- whenJustM (UI.getElementById window (T.unpack anId <> "__node-errors-tbody")) $ \el ->
- void $ element el # set dataState "0"
- -- Start update errors timer again.
- UI.start updateErrorsTimer
-
-sortErrorsByTime, sortErrorsBySeverity
- :: UI.Window
- -> NodeId
- -> Element
- -> Errors
- -> UI ()
-sortErrorsByTime = sortErrors timeAsc timeDesc
-sortErrorsBySeverity = sortErrors severityAsc severityDesc
-
-sortErrors
- :: (ErrorInfo -> ErrorInfo -> Ordering)
- -> (ErrorInfo -> ErrorInfo -> Ordering)
- -> UI.Window
- -> NodeId
- -> Element
- -> Errors
- -> UI ()
-sortErrors orderingAsc orderingDesc window nodeId@(NodeId anId) sortIcon nodesErrors = do
- -- Delete errors from window.
- findByClassAndDo window (anId <> "-node-error-row") delete'
- get dataState sortIcon >>= \case
- "desc" -> doSortErrors orderingAsc "asc"
- _ -> doSortErrors orderingDesc "desc"
- where
- doSortErrors ordering orderState = do
- sortedErrors <- liftIO $ getErrorsSortedBy ordering nodesErrors nodeId
- -- Do add sorted errors.
- whenJustM (UI.getElementById window (T.unpack anId <> "__node-errors-tbody")) $ \el ->
- doAddErrorRows nodeId sortedErrors el 0
- void $ element sortIcon # set dataState orderState
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Logs.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Logs.hs
new file mode 100644
index 00000000000..3f51e665e8e
--- /dev/null
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Logs.hs
@@ -0,0 +1,147 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Cardano.Tracer.Handlers.RTView.Update.Logs
+ ( updateLogsLiveViewItems
+ , updateLogsLiveViewNodes
+ ) where
+
+import Control.Concurrent.STM.TVar (readTVarIO)
+import Control.Monad (forM_, void, when)
+import Control.Monad.Extra (whenJustM, whenM)
+import qualified Data.Text as T
+import Data.Time.Format (defaultTimeLocale, formatTime)
+import qualified Graphics.UI.Threepenny as UI
+import Graphics.UI.Threepenny.Core
+
+import Cardano.Logging (SeverityS (..))
+
+import Cardano.Tracer.Environment
+import Cardano.Tracer.Handlers.RTView.State.TraceObjects
+import Cardano.Tracer.Handlers.RTView.UI.Charts
+import Cardano.Tracer.Handlers.RTView.UI.Img.Icons
+import Cardano.Tracer.Handlers.RTView.UI.JS.Utils
+import Cardano.Tracer.Handlers.RTView.UI.Types
+import Cardano.Tracer.Handlers.RTView.UI.Utils
+import Cardano.Tracer.Handlers.RTView.Update.Nodes
+import Cardano.Tracer.Handlers.RTView.Utils
+import Cardano.Tracer.Types
+import Cardano.Tracer.Utils
+
+updateLogsLiveViewItems
+ :: TracerEnv
+ -> LogsLiveViewCounters
+ -> UI ()
+updateLogsLiveViewItems tracerEnv@TracerEnv{teSavedTO} llvCounters =
+ whenM logsLiveViewIsOpened $ do
+ window <- askWindow
+ whenJustM (UI.getElementById window "node-logs-live-view-tbody") $ \el ->
+ forConnectedUI_ tracerEnv $ \nodeId@(NodeId anId) -> do
+ nodeName <- liftIO $ askNodeName tracerEnv nodeId
+ nodeColor <- liftIO $ getSavedColorForNode tracerEnv nodeName
+ tosFromThisNode <- liftIO $ getTraceObjects teSavedTO nodeId
+ forM_ tosFromThisNode $ \trObInfo -> do
+ -- We should add log items only for nodes which is "enabled" via checkbox.
+ let checkId = T.unpack anId <> "__node-live-view-checkbox"
+ whenJustM (UI.getElementById window checkId) $ \checkbox -> do
+ whenM (get UI.checked checkbox) $ do
+ doAddItemRow nodeId nodeName nodeColor llvCounters el trObInfo
+ -- Since we have added a new item row, we have to check if there are
+ -- too many items already. If so - we have to remove old item row,
+ -- to prevent too big number of them (if the user opened the window
+ -- for a long time).
+ liftIO (getLogsLiveViewCounter llvCounters nodeId) >>= \case
+ Nothing -> return ()
+ Just currentNumber ->
+ when (currentNumber > maxNumberOfLogsLiveViewItems) $ do
+ -- Ok, we have to delete outdated item row.
+ let !outdatedItemNumber = currentNumber - maxNumberOfLogsLiveViewItems
+ outdatedItemId = nodeName <> "llv" <> showT outdatedItemNumber
+ findAndDo window outdatedItemId delete'
+ where
+ -- TODO: Probably it will be configured by the user.
+ maxNumberOfLogsLiveViewItems = 20
+
+logsLiveViewIsOpened :: UI Bool
+logsLiveViewIsOpened = do
+ window <- askWindow
+ UI.getElementById window "logs-live-view-modal-window" >>= \case
+ Nothing -> return False
+ Just el -> (==) "opened" <$> get dataState el
+
+doAddItemRow
+ :: NodeId
+ -> NodeName
+ -> Maybe Color
+ -> LogsLiveViewCounters
+ -> Element
+ -> (Namespace, TraceObjectInfo)
+ -> UI ()
+doAddItemRow nodeId@(NodeId anId) nodeName nodeColor
+ llvCounters parentEl (ns, (msg, sev, ts)) = do
+ liftIO $ incLogsLiveViewCounter llvCounters nodeId
+ aRow <- mkItemRow
+ void $ element parentEl #+ [aRow]
+ where
+ mkItemRow = do
+ copyItemIcon <- image "has-tooltip-multiline has-tooltip-left rt-view-copy-icon" copySVG
+ # set dataTooltip "Click to copy this log item"
+ on UI.click copyItemIcon . const $ copyTextToClipboard $
+ "[" <> preparedTS ts <> "] [" <> show sev <> "] [" <> T.unpack ns <> "] [" <> T.unpack msg <> "]"
+
+ let nodeNamePrepared = T.unpack $
+ if T.length nodeName > 13
+ then T.take 10 nodeName <> "..."
+ else nodeName
+
+ nodeNameLabel <-
+ case nodeColor of
+ Nothing -> UI.span #. "rt-view-logs-live-view-msg-node"
+ # set text nodeNamePrepared
+ Just (Color code) -> UI.span #. "rt-view-logs-live-view-msg-node"
+ # set style [("color", code)]
+ # set text nodeNamePrepared
+
+ let sevClass =
+ case sev of
+ Debug -> "has-text-primary"
+ Info -> "has-text-link"
+ Notice -> "has-text-info"
+ Warning -> "has-text-warning"
+ _ -> "has-text-danger"
+ severity <- UI.span #. ("rt-view-logs-live-view-msg-severity " <> sevClass) # set text (show sev)
+
+ logItemRowId <-
+ liftIO (getLogsLiveViewCounter llvCounters nodeId) >>= \case
+ Nothing -> return $ T.unpack nodeName <> "llv0"
+ Just currentNumber -> return $ T.unpack nodeName <> "llv" <> show currentNumber
+
+ return $
+ UI.p ## logItemRowId #. (T.unpack anId <> "-node-logs-live-view-row") #+
+ [ UI.span #. "rt-view-logs-live-view-msg-timestamp" # set text (preparedTS ts)
+ , element nodeNameLabel
+ , element severity
+ , UI.span #. "rt-view-logs-live-view-msg-namespace" # set text ("[" <> T.unpack ns <> "]")
+ , UI.span #. "rt-view-logs-live-view-msg-body" # set text (T.unpack msg)
+ -- , element copyItemIcon
+ ]
+
+ preparedTS = formatTime defaultTimeLocale "%D %T"
+
+-- | The userck clicks to button that opens live logs view window - so we should update its content.
+-- Particularly, update nodes' checkboxes.
+updateLogsLiveViewNodes :: TracerEnv -> UI ()
+updateLogsLiveViewNodes tracerEnv@TracerEnv{teConnectedNodes} = do
+ deleteAllNodesCheckboxes
+ addNodesCheckboxesForConnected
+ where
+ deleteAllNodesCheckboxes = do
+ window <- askWindow
+ findByClassAndDo window "rt-view-ncbl" delete'
+ findByClassAndDo window "is-checkradio is-medium rt-view-ncb" delete'
+
+ addNodesCheckboxesForConnected =
+ liftIO (readTVarIO teConnectedNodes) >>= doAddLiveViewNodesForConnected tracerEnv
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Nodes.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Nodes.hs
index 2f7ccd21104..35c30e15983 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Nodes.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Nodes.hs
@@ -1,4 +1,5 @@
{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
@@ -6,6 +7,7 @@
module Cardano.Tracer.Handlers.RTView.Update.Nodes
( addColumnsForConnected
, addDatasetsForConnected
+ , doAddLiveViewNodesForConnected
, checkNoNodesState
, updateNodesUI
, updateNodesUptime
@@ -13,8 +15,8 @@ module Cardano.Tracer.Handlers.RTView.Update.Nodes
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TVar
-import Control.Monad (forM_, unless, when)
-import Control.Monad.Extra (whenJust)
+import Control.Monad (forM_, unless, void, when)
+import Control.Monad.Extra (whenJust, whenJustM, whenM)
import Data.List (find)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.Map.Strict as M
@@ -38,8 +40,6 @@ import Cardano.Tracer.Environment
import Cardano.Tracer.Handlers.Metrics.Utils
import Cardano.Tracer.Handlers.RTView.State.Displayed
import Cardano.Tracer.Handlers.RTView.State.EraSettings
-import Cardano.Tracer.Handlers.RTView.State.Errors
-import Cardano.Tracer.Handlers.RTView.State.TraceObjects
import Cardano.Tracer.Handlers.RTView.UI.Charts
import Cardano.Tracer.Handlers.RTView.UI.HTML.Node.Column
import Cardano.Tracer.Handlers.RTView.UI.HTML.NoNodes
@@ -58,13 +58,11 @@ updateNodesUI
-> NonEmpty LoggingParams
-> Colors
-> DatasetsIndices
- -> Errors
- -> UI.Timer
-> UI.Timer
-> UI ()
-updateNodesUI tracerEnv@TracerEnv{teConnectedNodes, teAcceptedMetrics, teSavedTO}
+updateNodesUI tracerEnv@TracerEnv{teConnectedNodes, teAcceptedMetrics}
displayedElements nodesEraSettings loggingConfig colors
- datasetIndices nodesErrors updateErrorsTimer noNodesProgressTimer = do
+ datasetIndices noNodesProgressTimer = do
(connected, displayedEls) <- liftIO . atomically $ (,)
<$> readTVar teConnectedNodes
<*> readTVar displayedElements
@@ -74,12 +72,9 @@ updateNodesUI tracerEnv@TracerEnv{teConnectedNodes, teAcceptedMetrics, teSavedTO
let disconnected = displayed \\ connected -- In 'displayed' but not in 'connected'.
newlyConnected = connected \\ displayed -- In 'connected' but not in 'displayed'.
deleteColumnsForDisconnected connected disconnected
- addColumnsForConnected
- tracerEnv
- newlyConnected
- loggingConfig
- nodesErrors
- updateErrorsTimer
+ deleteLiveViewNodesForDisconnected tracerEnv disconnected
+ addColumnsForConnected tracerEnv newlyConnected loggingConfig
+ addLiveViewNodesForConnected tracerEnv newlyConnected
checkNoNodesState connected noNodesProgressTimer
askNSetNodeInfo tracerEnv newlyConnected displayedElements
addDatasetsForConnected tracerEnv newlyConnected colors datasetIndices
@@ -87,8 +82,6 @@ updateNodesUI tracerEnv@TracerEnv{teConnectedNodes, teAcceptedMetrics, teSavedTO
liftIO $
updateDisplayedElements displayedElements connected
setBlockReplayProgress connected teAcceptedMetrics
- setChunkValidationProgress connected teSavedTO
- setLedgerDBProgress connected teSavedTO
setProducerMode connected teAcceptedMetrics
setLeadershipStats connected displayedElements teAcceptedMetrics
setEraEpochInfo connected displayedElements teAcceptedMetrics nodesEraSettings
@@ -97,19 +90,14 @@ addColumnsForConnected
:: TracerEnv
-> Set NodeId
-> NonEmpty LoggingParams
- -> Errors
- -> UI.Timer
-> UI ()
-addColumnsForConnected tracerEnv newlyConnected loggingConfig nodesErrors updateErrorsTimer = do
+addColumnsForConnected tracerEnv newlyConnected loggingConfig = do
unless (S.null newlyConnected) $ do
window <- askWindow
findAndShow window "main-table-container"
+ findAndShow window "logs-live-view-button"
forM_ newlyConnected $
- addNodeColumn
- tracerEnv
- loggingConfig
- nodesErrors
- updateErrorsTimer
+ addNodeColumn tracerEnv loggingConfig
addDatasetsForConnected
:: TracerEnv
@@ -121,6 +109,7 @@ addDatasetsForConnected tracerEnv newlyConnected colors datasetIndices = do
unless (S.null newlyConnected) $ do
window <- askWindow
findAndShow window "main-charts-container"
+ findAndShow window "logs-live-view-button"
forM_ newlyConnected $
addNodeDatasetsToCharts tracerEnv colors datasetIndices
@@ -134,6 +123,7 @@ deleteColumnsForDisconnected connected disconnected = do
when (S.null connected) $ do
findAndHide window "main-table-container"
findAndHide window "main-charts-container"
+ findAndHide window "logs-live-view-button"
-- Please note that we don't remove historical data from charts
-- for disconnected node. Because the user may want to see the
-- historical data even for the node that already disconnected.
@@ -145,8 +135,13 @@ checkNoNodesState
checkNoNodesState connected noNodesProgressTimer = do
window <- askWindow
if S.null connected
- then showNoNodes window noNodesProgressTimer
- else hideNoNodes window noNodesProgressTimer
+ then do
+ showNoNodes window noNodesProgressTimer
+ whenM logsLiveViewIsOpened $ do
+ findAndSet (set UI.class_ "modal") window "logs-live-view-modal-window"
+ findAndSet (set dataState "closed") window "logs-live-view-modal-window"
+ else
+ hideNoNodes window noNodesProgressTimer
updateNodesUptime
:: TracerEnv
@@ -182,6 +177,72 @@ updateNodesUptime tracerEnv displayedElements = do
, (nodeUptimeSElId, T.pack secsNum <> "s")
]
+addLiveViewNodesForConnected
+ :: TracerEnv
+ -> Set NodeId
+ -> UI ()
+addLiveViewNodesForConnected tracerEnv newlyConnected =
+ whenM logsLiveViewIsOpened $
+ doAddLiveViewNodesForConnected tracerEnv newlyConnected
+
+doAddLiveViewNodesForConnected
+ :: TracerEnv
+ -> Set NodeId
+ -> UI ()
+doAddLiveViewNodesForConnected tracerEnv connected = do
+ window <- askWindow
+ whenJustM (UI.getElementById window "logs-live-view-nodes-checkboxes") $ \el ->
+ forM_ connected $ \nodeId@(NodeId anId) -> do
+ nodeName <- liftIO $ askNodeName tracerEnv nodeId
+ nodeColor <- liftIO $ getSavedColorForNode tracerEnv nodeName
+
+ let nodeNamePrepared = T.unpack $
+ if T.length nodeName > 13
+ then T.take 10 nodeName <> "..."
+ else nodeName
+ checkId = T.unpack anId <> "__node-live-view-checkbox"
+ checkLabelId = T.unpack anId <> "__node-live-view-checkbox-label"
+
+ void $ element el #+
+ [ UI.input ## checkId
+ #. "is-checkradio is-medium rt-view-ncb"
+ # set UI.type_ "checkbox"
+ # set UI.name checkId
+ # set UI.checked True
+ , case nodeColor of
+ Nothing ->
+ UI.label ## checkLabelId
+ #. "rt-view-ncbl"
+ # set UI.for checkId
+ # set text nodeNamePrepared
+ Just (Color code) ->
+ UI.label ## checkLabelId
+ #. "rt-view-ncbl"
+ # set UI.for checkId
+ # set style [("color", code)]
+ # set text nodeNamePrepared
+ ]
+
+deleteLiveViewNodesForDisconnected
+ :: TracerEnv
+ -> Set NodeId
+ -> UI ()
+deleteLiveViewNodesForDisconnected _tracerEnv disconnected =
+ whenM logsLiveViewIsOpened $ do
+ window <- askWindow
+ forM_ disconnected $ \(NodeId anId) -> do
+ let checkId = anId <> "__node-live-view-checkbox"
+ checkLabelId = anId <> "__node-live-view-checkbox-label"
+ findAndDo window checkId delete'
+ findAndDo window checkLabelId delete'
+
+logsLiveViewIsOpened :: UI Bool
+logsLiveViewIsOpened = do
+ window <- askWindow
+ UI.getElementById window "logs-live-view-modal-window" >>= \case
+ Nothing -> return False
+ Just el -> (==) "opened" <$> get dataState el
+
setBlockReplayProgress
:: Set NodeId
-> AcceptedMetrics
@@ -204,57 +265,6 @@ setBlockReplayProgress connected acceptedMetrics = do
then setTextAndClasses nodeBlockReplayElId "100 %" "rt-view-percent-done"
else setTextValue nodeBlockReplayElId $ progressPctS <> " %"
-setChunkValidationProgress
- :: Set NodeId
- -> SavedTraceObjects
- -> UI ()
-setChunkValidationProgress connected savedTO = do
- savedTraceObjects <- liftIO $ readTVarIO savedTO
- forM_ connected $ \nodeId@(NodeId anId) ->
- whenJust (M.lookup nodeId savedTraceObjects) $ \savedTOForNode -> do
- let nodeChunkValidationElId = anId <> "__node-chunk-validation"
- forM_ (M.toList savedTOForNode) $ \(namespace, (trObValue, _, _)) ->
- case namespace of
- "ChainDB.ImmutableDBEvent.ChunkValidation.ValidatedChunk" ->
- -- In this case we don't need to check if the value differs from displayed one,
- -- because this 'TraceObject' is forwarded only with new values, and after 100%
- -- the node doesn't forward it anymore.
- --
- -- Example: "Validated chunk no. 2262 out of 2423. Progress: 93.36%"
- case T.words trObValue of
- [_, _, _, current, _, _, from, _, progressPct] ->
- setTextValue nodeChunkValidationElId $
- T.init progressPct <> " %: no. " <> current <> " from " <> T.init from
- _ -> return ()
- "ChainDB.ImmutableDBEvent.ValidatedLastLocation" ->
- setTextAndClasses nodeChunkValidationElId "100 %" "rt-view-percent-done"
- _ -> return ()
-
-setLedgerDBProgress
- :: Set NodeId
- -> SavedTraceObjects
- -> UI ()
-setLedgerDBProgress connected savedTO = do
- savedTraceObjects <- liftIO $ readTVarIO savedTO
- forM_ connected $ \nodeId@(NodeId anId) ->
- whenJust (M.lookup nodeId savedTraceObjects) $ \savedTOForNode -> do
- let nodeLedgerDBUpdateElId = anId <> "__node-update-ledger-db"
- forM_ (M.toList savedTOForNode) $ \(namespace, (trObValue, _, _)) ->
- case namespace of
- "ChainDB.InitChainSelEvent.UpdateLedgerDb" ->
- -- In this case we don't need to check if the value differs from displayed one,
- -- because this 'TraceObject' is forwarded only with new values, and after 100%
- -- the node doesn't forward it anymore.
- --
- -- Example: "Pushing ledger state for block b1e6...fc5a at slot 54495204. Progress: 3.66%"
- case T.words trObValue of
- [_, _, _, _, _, _, _, _, _, _, progressPct] -> do
- if "100" `T.isInfixOf` progressPct
- then setTextAndClasses nodeLedgerDBUpdateElId "100 %" "rt-view-percent-done"
- else setTextValue nodeLedgerDBUpdateElId $ T.init progressPct <> " %"
- _ -> return ()
- _ -> return ()
-
setProducerMode
:: Set NodeId
-> AcceptedMetrics
diff --git a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Reload.hs b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Reload.hs
index 4cf61ed6011..f9089e05fa9 100644
--- a/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Reload.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Handlers/RTView/Update/Reload.hs
@@ -13,7 +13,6 @@ import Graphics.UI.Threepenny.Core
import Cardano.Tracer.Configuration
import Cardano.Tracer.Environment
import Cardano.Tracer.Handlers.RTView.State.Displayed
-import Cardano.Tracer.Handlers.RTView.State.Errors
import Cardano.Tracer.Handlers.RTView.UI.Charts
import Cardano.Tracer.Handlers.RTView.UI.Types
import Cardano.Tracer.Handlers.RTView.Update.NodeInfo
@@ -25,12 +24,10 @@ updateUIAfterReload
-> NonEmpty LoggingParams
-> Colors
-> DatasetsIndices
- -> Errors
- -> UI.Timer
-> UI.Timer
-> UI ()
-updateUIAfterReload tracerEnv displayedElements loggingConfig colors datasetIndices
- nodesErrors updateErrorsTimer noNodesProgressTimer = do
+updateUIAfterReload tracerEnv displayedElements loggingConfig colors
+ datasetIndices noNodesProgressTimer = do
-- Ok, web-page was reload (i.e. it's the first update after DOM-rendering),
-- so displayed state should be restored immediately.
connected <- liftIO $ readTVarIO (teConnectedNodes tracerEnv)
@@ -38,8 +35,6 @@ updateUIAfterReload tracerEnv displayedElements loggingConfig colors datasetIndi
tracerEnv
connected
loggingConfig
- nodesErrors
- updateErrorsTimer
checkNoNodesState connected noNodesProgressTimer
askNSetNodeInfo tracerEnv connected displayedElements
addDatasetsForConnected tracerEnv connected colors datasetIndices
diff --git a/cardano-tracer/src/Cardano/Tracer/Run.hs b/cardano-tracer/src/Cardano/Tracer/Run.hs
index f84df7d7f1d..0c2adf17012 100644
--- a/cardano-tracer/src/Cardano/Tracer/Run.hs
+++ b/cardano-tracer/src/Cardano/Tracer/Run.hs
@@ -42,7 +42,7 @@ doRunCardanoTracer config rtViewStateDir protocolsBrake dpRequestors = do
connectedNodes <- initConnectedNodes
connectedNodesNames <- initConnectedNodesNames
acceptedMetrics <- initAcceptedMetrics
- savedTO <- initSavedTraceObjects
+ savedTO <- initSavedTraceObjects
chainHistory <- initBlockchainHistory
resourcesHistory <- initResourcesHistory