diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/ApiCalls.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/ApiCalls.elm
new file mode 100644
index 00000000000..03d94db93c1
--- /dev/null
+++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/ApiCalls.elm
@@ -0,0 +1,64 @@
+module NodeCompliance.ApiCalls exposing (..)
+
+import Http exposing (..)
+import Url.Builder exposing (QueryParameter)
+
+import NodeCompliance.DataTypes exposing (..)
+import NodeCompliance.JsonDecoder exposing (..)
+
+
+--
+-- This files contains all API calls for the Directive compliance UI
+--
+
+getUrl: Model -> List String -> List QueryParameter -> String
+getUrl m url p=
+ Url.Builder.relative (m.contextPath :: "secure" :: "api" :: url) p
+
+getPolicyMode : Model -> Cmd Msg
+getPolicyMode model =
+ let
+ req =
+ request
+ { method = "GET"
+ , headers = []
+ , url = getUrl model [ "settings", "global_policy_mode" ] []
+ , body = emptyBody
+ , expect = expectJson GetPolicyModeResult decodeGetPolicyMode
+ , timeout = Nothing
+ , tracker = Nothing
+ }
+ in
+ req
+
+getDirectiveCompliance : Model -> Cmd Msg
+getDirectiveCompliance model =
+ let
+ req =
+ request
+ { method = "GET"
+ , headers = []
+ , url = getUrl model [ "compliance", "nodes", model.nodeId.value ] []
+ , body = emptyBody
+ , expect = expectJson GetDirectiveComplianceResult decodeGetDirectiveCompliance
+ , timeout = Nothing
+ , tracker = Nothing
+ }
+ in
+ req
+
+getCSVExport : Model -> Cmd Msg
+getCSVExport model =
+ let
+ req =
+ request
+ { method = "GET"
+ , headers = []
+ , url = getUrl model [ "compliance", "directives", model.nodeId.value ] [ Url.Builder.string "format" "csv"]
+ , body = emptyBody
+ , expect = expectString Export
+ , timeout = Nothing
+ , tracker = Nothing
+ }
+ in
+ req
\ No newline at end of file
diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/DataTypes.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/DataTypes.elm
new file mode 100644
index 00000000000..f7960efe49e
--- /dev/null
+++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/DataTypes.elm
@@ -0,0 +1,84 @@
+module NodeCompliance.DataTypes exposing (..)
+
+import Dict exposing (Dict)
+import Http exposing (Error)
+
+import Compliance.DataTypes exposing (..)
+import Rules.DataTypes exposing (RuleCompliance)
+--
+-- All our data types
+--
+
+type alias RuleId = { value : String }
+type alias DirectiveId = { value : String }
+type alias NodeId = { value : String }
+
+type alias DirectiveCompliance =
+ { compliance : Float
+ , policyMode : String
+ , complianceDetails : ComplianceDetails
+ , rules : List (RuleCompliance NodeValueCompliance)
+ , nodes : List NodeCompliance
+ }
+
+type alias RuleCompliance value =
+ { ruleId : RuleId
+ , name : String
+ , compliance : Float
+ , complianceDetails : ComplianceDetails
+ , components : List (ComponentCompliance value)
+ }
+
+type alias NodeValueCompliance =
+ { nodeId : NodeId
+ , name : String
+ , policyMode : String
+ , compliance : Float
+ , complianceDetails : ComplianceDetails
+ , values : List ValueCompliance
+ }
+
+type alias NodeCompliance =
+ { nodeId : NodeId
+ , name : String
+ , compliance : Float
+ , policyMode : String
+ , complianceDetails : ComplianceDetails
+ , rules : List (RuleCompliance ValueCompliance)
+ }
+
+
+type alias TableFilters =
+ { sortOrder : SortOrder
+ , filter : String
+ , openedRows : Dict String (String, SortOrder)
+ }
+
+type SortOrder = Asc | Desc
+
+type alias UI =
+ { tableFilters : TableFilters
+ , complianceFilters : ComplianceFilters
+ , loading : Bool
+ }
+
+type alias Model =
+ { nodeId : NodeId
+ , contextPath : String
+ , policyMode : String
+ , ui : UI
+ , directiveCompliance : Maybe DirectiveCompliance
+ }
+
+type Msg
+ = Ignore
+ | UpdateFilters TableFilters
+ | UpdateComplianceFilters ComplianceFilters
+ | GoTo String
+ | ToggleRow String String
+ | ToggleRowSort String String SortOrder
+ | GetPolicyModeResult (Result Error String)
+ | GetDirectiveComplianceResult (Result Error DirectiveCompliance)
+ | Export (Result Error String)
+ | CallApi (Model -> Cmd Msg)
+
diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/Init.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/Init.elm
new file mode 100644
index 00000000000..55fd3394c7c
--- /dev/null
+++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/Init.elm
@@ -0,0 +1,23 @@
+module NodeCompliance.Init exposing (..)
+
+import Dict exposing (Dict)
+
+import NodeCompliance.ApiCalls exposing (..)
+import NodeCompliance.DataTypes exposing (..)
+import Compliance.DataTypes exposing (..)
+
+
+init : { nodeId : String, contextPath : String } -> ( Model, Cmd Msg )
+init flags =
+ let
+ initFilters = (TableFilters Asc "" Dict.empty)
+ initUI = UI initFilters (ComplianceFilters False False []) True
+ initModel = Model (DirectiveId flags.nodeId) flags.contextPath "" initUI Nothing
+ listInitActions =
+ [ getPolicyMode initModel
+ , getDirectiveCompliance initModel
+ ]
+ in
+ ( initModel
+ , Cmd.batch listInitActions
+ )
\ No newline at end of file
diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/JsonDecoder.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/JsonDecoder.elm
new file mode 100644
index 00000000000..bd853dc106a
--- /dev/null
+++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/JsonDecoder.elm
@@ -0,0 +1,109 @@
+module NodeCompliance.JsonDecoder exposing (..)
+
+import Json.Decode exposing (..)
+import Json.Decode.Pipeline exposing (..)
+import Json.Decode.Field exposing (require)
+
+import NodeCompliance.DataTypes exposing (..)
+import Compliance.DataTypes exposing (..)
+
+
+decodeGetPolicyMode : Decoder String
+decodeGetPolicyMode =
+ at ["data", "settings", "global_policy_mode" ] string
+
+
+decodeGetDirectiveCompliance : Decoder DirectiveCompliance
+decodeGetDirectiveCompliance =
+ at ["data", "directiveCompliance" ] decodeDirectiveCompliance
+
+decodeDirectiveCompliance : Decoder DirectiveCompliance
+decodeDirectiveCompliance =
+ succeed DirectiveCompliance
+ |> required "compliance" float
+ |> required "policyMode" string
+ |> required "complianceDetails" decodeComplianceDetails
+ |> required "rules" (list (decodeRuleCompliance "nodes" decodeNodeCompliance) )
+ |> required "nodes" (list decodeRuleComplianceByNode )
+
+decodeRuleComplianceByNode : Decoder NodeCompliance
+decodeRuleComplianceByNode =
+ succeed NodeCompliance
+ |> required "id" (map NodeId string)
+ |> required "name" string
+ |> required "compliance" float
+ |> required "policyMode" string
+ |> required "complianceDetails" decodeComplianceDetails
+ |> required "rules" (list (decodeRuleCompliance "values" decodeValueCompliance ))
+
+decodeReport : Decoder Report
+decodeReport =
+ succeed Report
+ |> required "status" string
+ |> optional "message" (maybe string) Nothing
+
+decodeNodeCompliance : Decoder NodeValueCompliance
+decodeNodeCompliance =
+ succeed NodeValueCompliance
+ |> required "id" (map NodeId string)
+ |> required "name" string
+ |> required "policyMode" string
+ |> required "compliance" float
+ |> required "complianceDetails" decodeComplianceDetails
+ |> required "values" (list decodeValueCompliance)
+
+decodeValueCompliance : Decoder ValueCompliance
+decodeValueCompliance =
+ succeed ValueCompliance
+ |> required "value" string
+ |> required "reports" (list decodeReport)
+
+decodeRuleCompliance : String -> Decoder a -> Decoder (RuleCompliance a)
+decodeRuleCompliance elem decoder =
+ succeed RuleCompliance
+ |> required "id" (map RuleId string)
+ |> required "name" string
+ |> required "compliance" float
+ |> required "complianceDetails" decodeComplianceDetails
+ |> required "components" (list (decodeComponentCompliance elem decoder ))
+
+decodeComplianceDetails : Decoder ComplianceDetails
+decodeComplianceDetails =
+ succeed ComplianceDetails
+ |> optional "successNotApplicable" (map Just float) Nothing
+ |> optional "successAlreadyOK" (map Just float) Nothing
+ |> optional "successRepaired" (map Just float) Nothing
+ |> optional "error" (map Just float) Nothing
+ |> optional "auditCompliant" (map Just float) Nothing
+ |> optional "auditNonCompliant" (map Just float) Nothing
+ |> optional "auditError" (map Just float) Nothing
+ |> optional "auditNotApplicable" (map Just float) Nothing
+ |> optional "unexpectedUnknownComponent" (map Just float) Nothing
+ |> optional "unexpectedMissingComponent" (map Just float) Nothing
+ |> optional "noReport" (map Just float) Nothing
+ |> optional "reportsDisabled" (map Just float) Nothing
+ |> optional "applying" (map Just float) Nothing
+ |> optional "badPolicyMode" (map Just float) Nothing
+
+decodeComponentValueCompliance : String -> Decoder a -> Decoder (ComponentValueCompliance a)
+decodeComponentValueCompliance elem decoder =
+ succeed ComponentValueCompliance
+ |> required "name" string
+ |> required "compliance" float
+ |> required "complianceDetails" decodeComplianceDetails
+ |> required elem (list decoder)
+
+decodeComponentCompliance : String -> Decoder a -> Decoder (ComponentCompliance a)
+decodeComponentCompliance elem decoder =
+ oneOf [
+ map (\b -> Block b) <| decodeBlockCompliance elem decoder ()
+ , map (\v -> Value v) <| decodeComponentValueCompliance elem decoder
+ ]
+
+decodeBlockCompliance : String -> Decoder a -> () -> Decoder (BlockCompliance a)
+decodeBlockCompliance elem decoder _ =
+ require "name" string <| \name ->
+ require "compliance" float <| \compliance ->
+ require "complianceDetails" decodeComplianceDetails <| \details ->
+ require "components" (list (decodeComponentCompliance elem decoder)) <| \components ->
+ succeed ({ component = name, compliance = compliance, complianceDetails = details, components = components } )
\ No newline at end of file
diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/View.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/View.elm
new file mode 100644
index 00000000000..5404ac8e45c
--- /dev/null
+++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/View.elm
@@ -0,0 +1,16 @@
+module NodeCompliance.View exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (onClick)
+import List
+import Html.Lazy
+
+import NodeCompliance.DataTypes exposing (..)
+import NodeCompliance.ViewUtils exposing (..)
+import NodeCompliance.ViewCompliance exposing (..)
+
+
+view : Model -> Html Msg
+view model =
+ div [class "tab-table-content"][Html.Lazy.lazy displayComplianceTable model]
\ No newline at end of file
diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/ViewCompliance.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/ViewCompliance.elm
new file mode 100644
index 00000000000..c67d680ef0e
--- /dev/null
+++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/ViewCompliance.elm
@@ -0,0 +1,78 @@
+module NodeCompliance.ViewCompliance exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (onClick, onInput)
+import List
+import List.Extra
+import String
+import Tuple3
+import Dict
+
+import NodeCompliance.ApiCalls exposing (..)
+import NodeCompliance.DataTypes exposing (..)
+import NodeCompliance.ViewUtils exposing (..)
+import Compliance.Utils exposing (displayComplianceFilters, filterDetailsByCompliance)
+
+
+displayComplianceTable : Model -> Html Msg
+displayComplianceTable model =
+ let
+ filters = model.ui.tableFilters
+ complianceFilters = model.ui.complianceFilters
+ fun = byNodeCompliance model complianceFilters
+ col = "Node"
+ childs = case model.directiveCompliance of
+ Just dc -> dc.nodes
+ Nothing -> []
+ childrenSort = childs
+ |> List.filter (\n -> (filterSearch filters.filter (searchFieldNodeCompliance n)))
+ |> List.filter (filterDetailsByCompliance complianceFilters)
+ |> List.sortWith sort
+
+ (children, order, newOrder) = case sortOrder of
+ Asc -> (childrenSort, "asc", Desc)
+ Desc -> (List.reverse childrenSort, "desc", Asc)
+
+ rowId = "by" ++ col ++ "s/"
+ rows = List.map Tuple3.first fun.rows
+ (sortId, sortOrder) = Dict.get rowId filters.openedRows |> Maybe.withDefault (col, Asc)
+ sort = case List.Extra.find (Tuple3.first >> (==) sortId) fun.rows of
+ Just (_,_,sortFun) -> (\i1 i2 -> sortFun (fun.data model i1) (fun.data model i2))
+ Nothing -> (\_ _ -> EQ)
+ in
+ ( if model.ui.loading then
+ generateLoadingTable
+ else
+ div[]
+ [ div [class "table-header extra-filters"]
+ [ div[class "main-filters"]
+ [ input [type_ "text", placeholder "Filter", class "input-sm form-control", value filters.filter, onInput (\s -> (UpdateFilters {filters | filter = s} ))][]
+ , button [class "btn btn-default btn-sm btn-icon", onClick (UpdateComplianceFilters {complianceFilters | showComplianceFilters = not complianceFilters.showComplianceFilters}), style "min-width" "170px"]
+ [ text ((if complianceFilters.showComplianceFilters then "Hide " else "Show ") ++ "compliance filters")
+ , i [class ("fa " ++ (if complianceFilters.showComplianceFilters then "fa-minus" else "fa-plus"))][]
+ ]
+ ]
+ , displayComplianceFilters complianceFilters UpdateComplianceFilters
+ ]
+ , div[class "table-container"]
+ [ table [class "dataTable compliance-table"]
+ [ thead []
+ [ tr [ class "head" ]
+ ( List.map (\row -> th [onClick (ToggleRowSort rowId row (if row == sortId then newOrder else Asc)), class ("sorting" ++ (if row == sortId then "_"++order else ""))] [ text row ]) rows )
+ ]
+ , tbody []
+ ( if List.length childs <= 0 then
+ [ tr[]
+ [ td[class "empty", colspan 2][i [class"fa fa-exclamation-triangle"][], text "There is no compliance for this directive."] ]
+ ]
+ else if List.length children == 0 then
+ [ tr[]
+ [ td[class "empty", colspan 2][i [class"fa fa-exclamation-triangle"][], text "No nodes match your filter."] ]
+ ]
+ else
+ List.concatMap (\d -> showComplianceDetails fun d "" filters.openedRows model) children
+ )
+ ]
+ ]
+ ])
diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/ViewUtils.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/ViewUtils.elm
new file mode 100644
index 00000000000..5d92e2e4c77
--- /dev/null
+++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeCompliance/ViewUtils.elm
@@ -0,0 +1,377 @@
+module NodeCompliance.ViewUtils exposing (..)
+
+import Dict exposing (Dict)
+import Either exposing (Either(..))
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (onClick, onInput, custom)
+import List.Extra
+import List
+import Maybe.Extra
+import String exposing (fromFloat)
+import Json.Decode as Decode
+import Tuple3
+import NaturalOrdering as N exposing (compare)
+
+import NodeCompliance.ApiCalls exposing (..)
+import NodeCompliance.DataTypes exposing (..)
+import Compliance.DataTypes exposing (..)
+import Compliance.Utils exposing (..)
+
+onCustomClick : msg -> Html.Attribute msg
+onCustomClick msg =
+ custom "click"
+ (Decode.succeed
+ { message = msg
+ , stopPropagation = True
+ , preventDefault = True
+ }
+ )
+--
+-- DATATABLES & TREES
+--
+badgePolicyMode : String -> String -> Html Msg
+badgePolicyMode globalPolicyMode policyMode =
+ let
+ mode = if policyMode == "default" then globalPolicyMode else policyMode
+ defaultMsg = "This mode is the globally defined default. You can change it in the global settings."
+ msg =
+ case mode of
+ "enforce" -> "
This rule is in enforce mode.
" ++ defaultMsg
+ "audit" -> "This rule is in audit mode.
" ++ defaultMsg
+ "mixed" ->
+ """
+ This rule is in mixed mode.
+ This rule is applied on at least one node or directive that will enforce
+ one configuration, and at least one that will audit them.
+ """
+ _ -> "Unknown policy mode"
+
+ in
+ span [class ("treeGroupName tooltipable bs-tooltip rudder-label label-sm label-" ++ mode), attribute "data-toggle" "tooltip", attribute "data-placement" "bottom", attribute "data-container" "body", attribute "data-html" "true", attribute "data-original-title" (buildTooltipContent "Policy mode" msg)][]
+
+subItemOrder : ItemFun item subItem data -> Model -> String -> (item -> item -> Order)
+subItemOrder fun model id =
+ case List.Extra.find (Tuple3.first >> (==) id) fun.rows of
+ Just (_,_,sort) -> (\i1 i2 -> sort (fun.data model i1) (fun.data model i2))
+ Nothing -> (\_ _ -> EQ)
+
+type alias ItemFun item subItem data =
+ { children : item -> Model -> String -> List subItem
+ , data : Model -> item -> data
+ , rows : List (String, data -> Html Msg, (data -> data -> Order) )
+ , id : item -> String
+ , childDetails : Maybe (subItem -> String -> Dict String (String, SortOrder) -> Model -> List (Html Msg))
+ , subItemRows : item -> List String
+ , filterItems : item -> Bool
+ }
+
+valueCompliance : ComplianceFilters -> ItemFun ValueCompliance () ValueCompliance
+valueCompliance complianceFilters =
+ ItemFun
+ (\ _ _ _ -> [])
+ (\_ i -> i)
+ [ ("Value" , .value >> text, (\d1 d2 -> N.compare d1.value d2.value))
+ , ("Messages", .reports >> List.filter (filterReports complianceFilters) >> List.map (\r -> Maybe.withDefault "" r.message) >> List.foldl (++) "\n" >> text, (\d1 d2 -> N.compare d1.value d2.value) )
+ , ("Status" , .reports >> List.filter (filterReports complianceFilters) >> buildComplianceReport, (\d1 d2 -> Basics.compare d1.value d2.value))
+ ]
+ .value
+ Nothing
+ (always [])
+ (filterReportsByCompliance complianceFilters)
+
+byComponentCompliance : ItemFun value subValue valueData -> ComplianceFilters -> ItemFun (ComponentCompliance value) (Either (ComponentCompliance value) value) (ComponentCompliance value)
+byComponentCompliance subFun complianceFilters =
+ let
+ name = \item ->
+ case item of
+ Block b -> b.component
+ Value c -> c.component
+ compliance = \item ->
+ case item of
+ Block b -> b.complianceDetails
+ Value c -> c.complianceDetails
+ in
+ ItemFun
+ ( \item model sortId ->
+ case item of
+ Block b ->
+ let
+ sortFunction = subItemOrder (byComponentCompliance subFun complianceFilters) model sortId
+ in
+ b.components
+ |> List.filter (filterByCompliance complianceFilters)
+ |> List.sortWith sortFunction
+ |> List.map Left
+ Value c ->
+ let
+ sortFunction = subItemOrder subFun model sortId
+ in
+ c.values
+ |> List.filter subFun.filterItems
+ |> List.sortWith sortFunction
+ |> List.map Right
+ )
+ (\_ i -> i)
+ [ ("Component", name >> text, (\d1 d2 -> N.compare (name d1) (name d2)))
+ , ("Compliance", \i -> buildComplianceBar complianceFilters (compliance i), (\d1 d2 -> Basics.compare (name d1) (name d2)) )
+ ]
+ name
+ (Just ( \x ->
+ case x of
+ Left value -> showComplianceDetails (byComponentCompliance subFun complianceFilters) value
+ Right value -> showComplianceDetails subFun value
+ ))
+ ( \x ->
+ case x of
+ Block _ -> (List.map Tuple3.first (byComponentCompliance subFun complianceFilters).rows)
+ Value _ -> (List.map Tuple3.first subFun.rows)
+ )
+ (always True)
+
+byNodeCompliance : Model -> ComplianceFilters -> ItemFun NodeCompliance (RuleCompliance ValueCompliance) NodeCompliance
+byNodeCompliance mod complianceFilters =
+ let
+ rule = byRuleCompliance mod (valueCompliance complianceFilters) complianceFilters
+ in
+ ItemFun
+ (\item model sortId ->
+ let
+ sortFunction = subItemOrder rule mod sortId
+ in
+ item.rules
+ |> List.filter (filterDetailsByCompliance complianceFilters)
+ |> List.sortWith sortFunction
+ )
+ (\m i -> i)
+ [ ("Node", (\nId -> span[][ (badgePolicyMode mod.policyMode nId.policyMode), text nId.name, goToBtn (getNodeLink mod.contextPath nId.nodeId.value)]), (\n1 n2 -> N.compare n1.name n2.name))
+ , ("Compliance", .complianceDetails >> buildComplianceBar complianceFilters, (\n1 n2 -> Basics.compare n1.compliance n2.compliance))
+ ]
+ (.nodeId >> .value)
+ (Just (\b -> showComplianceDetails rule b))
+ (always (List.map Tuple3.first rule.rows))
+ (always True)
+
+byRuleCompliance : Model -> ItemFun value subValue valueData -> ComplianceFilters -> ItemFun (RuleCompliance value) (ComponentCompliance value) (RuleCompliance value)
+byRuleCompliance model subFun complianceFilters =
+ let
+ contextPath = model.contextPath
+ in
+ ItemFun
+ (\item m sortId ->
+ let
+ sortFunction = subItemOrder (byComponentCompliance subFun complianceFilters) m sortId
+ in
+ item.components
+ |> List.filter (filterByCompliance complianceFilters)
+ |> List.sortWith sortFunction
+ )
+ (\m i -> i )
+ [ ("Rule", \i -> span [] [ (badgePolicyMode model.policyMode (Maybe.map .policyMode model.directiveCompliance|> Maybe.withDefault "default")), text i.name , goToBtn (getRuleLink contextPath i.ruleId) ], (\r1 r2 -> N.compare r1.name r2.name ))
+ , ("Compliance", \i -> buildComplianceBar complianceFilters i.complianceDetails, (\(r1) (r2) -> Basics.compare r1.compliance r2.compliance ))
+ ]
+ (.ruleId >> .value)
+ (Just (\b -> showComplianceDetails (byComponentCompliance subFun complianceFilters) b))
+ (always (List.map Tuple3.first (byComponentCompliance subFun complianceFilters).rows))
+ (always True)
+
+nodeValueCompliance : Model -> ComplianceFilters -> ItemFun NodeValueCompliance ValueCompliance NodeValueCompliance
+nodeValueCompliance mod complianceFilters =
+ ItemFun
+ (\item model sortId ->
+ let
+ sortFunction = subItemOrder (valueCompliance complianceFilters) model sortId
+ in
+ item.values
+ |> List.filter (filterReportsByCompliance complianceFilters)
+ |> List.sortWith sortFunction
+ )
+ (\_ i -> i)
+ [ ("Node", (\nId -> span[][text nId.name, goToBtn (getNodeLink mod.contextPath nId.nodeId.value)]), (\d1 d2 -> N.compare d1.name d2.name))
+ , ("Compliance", .complianceDetails >> buildComplianceBar complianceFilters , (\d1 d2 -> Basics.compare d1.compliance d2.compliance))
+ ]
+ (.nodeId >> .value)
+ (Just (\item -> showComplianceDetails (valueCompliance complianceFilters) item))
+ (always (List.map Tuple3.first (valueCompliance complianceFilters).rows))
+ (filterDetailsByCompliance complianceFilters)
+
+showComplianceDetails : ItemFun item subItems data -> item -> String -> Dict String (String, SortOrder) -> Model -> List (Html Msg)
+showComplianceDetails fun compliance parent openedRows model =
+ let
+ itemRows = List.map Tuple3.second (fun.rows)
+ data = fun.data model compliance
+ detailsRows = List.map (\row -> td [class "ok"] [row data]) itemRows
+ id = fun.id compliance
+ rowId = parent ++ "/" ++ id
+ rowOpened = Dict.get rowId openedRows
+ defaultSort = Maybe.withDefault "" (List.head (fun.subItemRows compliance))
+ clickEvent =
+ if Maybe.Extra.isJust fun.childDetails then
+ [ onClick (ToggleRow rowId defaultSort) ]
+ else
+ []
+ (details, classes) =
+ case (fun.childDetails, rowOpened) of
+ (Just detailsFun, Just (sortId, sortOrder)) ->
+ let
+ childrenSort = fun.children compliance model sortId
+ (children, order, newOrder) = case sortOrder of
+ Asc -> (childrenSort, "asc", Desc)
+ Desc -> (List.reverse childrenSort, "desc", Asc)
+ in
+ (
+ [ tr [ class "details" ]
+ [ td [ class "details", colspan 2 ]
+ [ div [ class "innerDetails" ]
+ [
+ table [class "dataTable compliance-table"] [
+ thead [] [
+ tr [ class "head" ]
+ (List.map (\row -> th [onClick (ToggleRowSort rowId row (if row == sortId then newOrder else Asc)) , class ("sorting" ++ (if row == sortId then "_"++order else "")) ] [ text row ]) (fun.subItemRows compliance) )
+ ]
+ , tbody []
+ ( if(List.isEmpty children) then
+ [ tr [] [ td [colspan 2, class "dataTables_empty" ] [ text "There is no compliance details" ] ] ]
+ else
+ List.concatMap (\child ->
+ (detailsFun child) rowId openedRows model
+ ) children
+ )
+ ]
+ ]
+ ] ] ],
+ "row-foldable row-open")
+ (Just _, Nothing) -> ([], "row-foldable row-folded")
+ (Nothing, _) -> ([],"")
+ in
+ (tr ( class classes :: clickEvent)
+ detailsRows)
+ :: details
+
+searchFieldRuleCompliance r =
+ [ r.ruleId.value
+ , r.name
+ ]
+
+searchFieldNodeCompliance n =
+ [ n.nodeId.value
+ , n.name
+ ]
+
+filterSearch : String -> List String -> Bool
+filterSearch filterString searchFields =
+ let
+ -- Join all the fields into one string to simplify the search
+ stringToCheck = searchFields
+ |> String.join "|"
+ |> String.toLower
+
+ searchString = filterString
+ |> String.toLower
+ |> String.trim
+ in
+ String.contains searchString stringToCheck
+
+
+htmlEscape : String -> String
+htmlEscape s =
+ String.replace "&" "&" s
+ |> String.replace ">" ">"
+ |> String.replace "<" "<"
+ |> String.replace "\"" """
+ |> String.replace "'" "'"
+ |> String.replace "\\" "/"
+
+
+-- WARNING:
+--
+-- Here the content is an HTML so it need to be already escaped.
+buildTooltipContent : String -> String -> String
+buildTooltipContent title content =
+ let
+ headingTag = ""
+ closeTag = "
"
+ in
+ headingTag ++ title ++ contentTag ++ content ++ closeTag
+
+buildComplianceReport : List Report -> Html Msg
+buildComplianceReport reports =
+ let
+ complianceTxt : String -> String
+ complianceTxt val =
+ case val of
+ "reportsDisabled" -> "Reports Disabled"
+ "noReport" -> "No report"
+ "error" -> "Error"
+ "successAlreadyOK" -> "Success"
+ "successRepaired" -> "Repaired"
+ "applying" -> "Applying"
+ "auditNotApplicable" -> "Not applicable"
+ "unexpectedUnknownComponent" -> "Unexpected"
+ "unexpectedMissingComponent" -> "Missing"
+ "enforceNotApplicable" -> "Not applicable"
+ "auditError" -> "Error"
+ "auditCompliant" -> "Compliant"
+ "auditNonCompliant" -> "Non compliant"
+ "badPolicyMode" -> "Bad Policy Mode"
+ _ -> val
+ in
+ td [class "report-compliance"]
+ [ div[]
+ ( List.map (\r -> span[class r.status][text (complianceTxt r.status)]) reports )
+ ]
+
+getRuleLink : String -> RuleId -> String
+getRuleLink contextPath id =
+ contextPath ++ "/secure/configurationManager/ruleManagement/rule/" ++ id.value
+
+getNodeLink : String -> String -> String
+getNodeLink contextPath id =
+ contextPath ++ "/secure/nodeManager/node/" ++ id
+
+
+goToBtn : String -> Html Msg
+goToBtn link =
+ a [ class "btn-goto", href link , onCustomClick (GoTo link)] [ i[class "fa fa-pen"][] ]
+
+goToIcon : Html Msg
+goToIcon =
+ span [ class "btn-goto" ] [ i[class "fa fa-pen"][] ]
+
+generateLoadingTable : Html Msg
+generateLoadingTable =
+ div [class "table-container skeleton-loading", style "margin-top" "17px"]
+ [ div [class "dataTables_wrapper_top table-filter"]
+ [ div [class "form-group"]
+ [ span[][]
+ ]
+ ]
+ , table [class "dataTable"]
+ [ thead []
+ [ tr [class "head"]
+ [ th [][ span[][] ]
+ , th [][ span[][] ]
+ ]
+ ]
+ , tbody []
+ [ tr[] [ td[][span[style "width" "45%"][]], td[][span[][]]]
+ , tr[] [ td[][span[][]], td[][span[][]] ]
+ , tr[] [ td[][span[style "width" "30%"][]], td[][span[][]] ]
+ , tr[] [ td[][span[style "width" "75%"][]], td[][span[][]] ]
+ , tr[] [ td[][span[][]], td[][span[][]] ]
+ , tr[] [ td[][span[style "width" "45%"][]], td[][span[][]] ]
+ , tr[] [ td[][span[][]], td[][span[][]] ]
+ , tr[] [ td[][span[style "width" "70%"][]], td[][span[][]] ]
+ , tr[] [ td[][span[][]], td[][span[][]] ]
+ , tr[] [ td[][span[][]], td[][span[][]] ]
+ , tr[] [ td[][span[style "width" "80%"][]], td[][span[][]] ]
+ , tr[] [ td[][span[style "width" "30%"][]], td[][span[][]] ]
+ , tr[] [ td[][span[style "width" "75%"][]], td[][span[][]] ]
+ , tr[] [ td[][span[style "width" "45%"][]], td[][span[][]] ]
+ , tr[] [ td[][span[][]], td[][span[][]] ]
+ , tr[] [ td[][span[style "width" "70%"][]], td[][span[][]] ]
+ , tr[] [ td[][span[][]], td[][span[][]] ]
+ ]
+ ]
+ ]
\ No newline at end of file
diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodecompliance.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodecompliance.elm
new file mode 100644
index 00000000000..9992476c5eb
--- /dev/null
+++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodecompliance.elm
@@ -0,0 +1,143 @@
+port module Nodecompliance exposing (..)
+
+import Browser
+import Browser.Navigation as Nav
+import Dict
+import Dict.Extra
+import Http exposing (..)
+import Result
+import String exposing (replace)
+import File
+import File.Download
+import File.Select
+
+import NodeCompliance.ApiCalls exposing (..)
+import NodeCompliance.DataTypes exposing (..)
+import NodeCompliance.Init exposing (init)
+import NodeCompliance.View exposing (view)
+
+
+-- PORTS / SUBSCRIPTIONS
+port errorNotification : String -> Cmd msg
+port initTooltips : String -> Cmd msg
+port loadCompliance : (String -> msg) -> Sub msg
+
+
+subscriptions : Model -> Sub Msg
+subscriptions _ =
+ Sub.none
+
+main =
+ Browser.element
+ { init = init
+ , view = view
+ , update = update
+ , subscriptions = subscriptions
+ }
+
+--
+-- update loop --
+--
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+ case msg of
+ CallApi apiCall ->
+ ( model , apiCall model)
+ Ignore ->
+ ( model , Cmd.none)
+
+ UpdateFilters newFilters ->
+ let
+ ui = model.ui
+ newUi = { ui | tableFilters = newFilters}
+ in
+ ({model | ui = newUi}, initTooltips "")
+
+ UpdateComplianceFilters newFilters ->
+ let
+ ui = model.ui
+ newUi = { ui | complianceFilters = newFilters }
+ in
+ ({model | ui = newUi}, initTooltips "")
+ GoTo link -> (model, Nav.load link)
+
+ ToggleRow rowId defaultSortId ->
+ let
+ ui = model.ui
+ filters = ui.tableFilters
+ newFilters =
+ { filters | openedRows = if Dict.member rowId filters.openedRows then
+ Dict.remove rowId filters.openedRows
+ else
+ Dict.insert rowId (defaultSortId, Asc) filters.openedRows
+ }
+ newUi = { ui | tableFilters = newFilters}
+ newModel = { model | ui = newUi }
+ in
+ (newModel, Cmd.none)
+
+ ToggleRowSort rowId sortId order ->
+ let
+ ui = model.ui
+ tableFilters = ui.tableFilters
+ newFilters = { tableFilters | openedRows = Dict.update rowId (always (Just (sortId,order))) tableFilters.openedRows }
+ newUi = { ui | tableFilters = newFilters}
+ newModel = { model | ui = newUi }
+ in
+ (newModel, Cmd.none)
+
+ GetPolicyModeResult res ->
+ case res of
+ Ok p ->
+ ( { model | policyMode = p }
+ , Cmd.none
+ )
+ Err err ->
+ processApiError "Getting Policy Mode" err model
+
+ GetDirectiveComplianceResult res ->
+ let
+ ui = model.ui
+ newModel = {model | ui = {ui | loading = False}}
+ in
+ case res of
+ Ok compliance ->
+ ( { newModel | directiveCompliance = Just compliance }
+ , Cmd.none
+ )
+ Err err ->
+ processApiError "Getting directive compliance" err newModel
+
+ Export res ->
+ case res of
+ Ok content ->
+ (model, File.Download.string (model.nodeId.value ++ ".csv") "text/csv" content)
+ Err err ->
+ processApiError "Export directive compliance" err model
+
+processApiError : String -> Error -> Model -> ( Model, Cmd Msg )
+processApiError apiName err model =
+ let
+ modelUi = model.ui
+ message =
+ case err of
+ Http.BadUrl url ->
+ "The URL " ++ url ++ " was invalid"
+ Http.Timeout ->
+ "Unable to reach the server, try again"
+ Http.NetworkError ->
+ "Unable to reach the server, check your network connection"
+ Http.BadStatus 500 ->
+ "The server had a problem, try again later"
+ Http.BadStatus 400 ->
+ "Verify your information and try again"
+ Http.BadStatus _ ->
+ "Unknown error"
+ Http.BadBody errorMessage ->
+ errorMessage
+
+ in
+ (model, errorNotification ("Error when "++apiName ++", details: \n" ++ message ) )
+
+getUrl : Model -> String
+getUrl model = model.contextPath ++ "/secure/configurationManager/directiveManagement"
\ No newline at end of file
diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ReportDisplayer.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ReportDisplayer.scala
index 85681fed025..c19fa332006 100644
--- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ReportDisplayer.scala
+++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/ReportDisplayer.scala
@@ -541,7 +541,7 @@ class ReportDisplayer(
} else {
"createExpectedReportTable"
}
-
+/*
++
Script(JsRaw(s"""
${jsFunctionName}("${tableId}",${data.toJsCmd},"${S.contextPath}", ${refreshReportDetail(
@@ -552,6 +552,26 @@ class ReportDisplayer(
).toJsCmd});
createTooltip();
"""))
+ */
+ ++
+ Script(JsRaw(
+ s"""
+ |var main = document.getElementById("nodecompliance-app")
+ |var initValues = {
+ | nodeId : "${node.id.value}",
+ | contextPath : contextPath
+ |};
+ |var app = Elm.Nodecompliance.init({node: main, flags: initValues});
+ |app.ports.errorNotification.subscribe(function(str) {
+ | createErrorNotification(str)
+ |});
+ |// Initialize tooltips
+ |app.ports.initTooltips.subscribe(function(msg) {
+ | setTimeout(function(){
+ | $$('.bs-tooltip').bsTooltip();
+ | }, 400);
+ |});
+ |""".stripMargin))
}
// this method cannot return an IOResult, as it uses S.