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 = "

" + contentTag = "

" + 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.