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