From fd3fe05ffc89069463d3de6c19305aaaa7087544 Mon Sep 17 00:00:00 2001 From: Raphael Gauthier Date: Thu, 5 Dec 2024 17:00:34 +0100 Subject: [PATCH] Fixes #25824: Properties tab : Non-rearrangement of the pop-ups --- .../elm/sources/NodeProperties/ViewUtils.elm | 5 +- .../src/main/elm/sources/Nodeproperties.elm | 2 +- .../src/main/elm/sources/Nodes/ApiCalls.elm | 46 +++++ .../src/main/elm/sources/Nodes/DataTypes.elm | 41 +++++ .../src/main/elm/sources/Nodes/Init.elm | 15 ++ .../main/elm/sources/Nodes/JsonDecoder.elm | 21 +++ .../main/elm/sources/Nodes/JsonEncoder.elm | 16 ++ .../src/main/elm/sources/Nodes/View.elm | 79 ++++++++ .../src/main/elm/sources/Nodes/ViewUtils.elm | 117 ++++++++++++ .../src/main/javascript/rudder/rudder.js | 22 ++- .../src/main/style/rudder/rudder-main.scss | 173 ++++++++++-------- 11 files changed, 454 insertions(+), 83 deletions(-) create mode 100644 webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/ApiCalls.elm create mode 100644 webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/DataTypes.elm create mode 100644 webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/Init.elm create mode 100644 webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/JsonDecoder.elm create mode 100644 webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/JsonEncoder.elm create mode 100644 webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/View.elm create mode 100644 webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/ViewUtils.elm diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeProperties/ViewUtils.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeProperties/ViewUtils.elm index 611f682f315..22be07c6b60 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeProperties/ViewUtils.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/NodeProperties/ViewUtils.elm @@ -184,15 +184,14 @@ displayNodePropertyRow model = let pTitle = case pr of "inherited" -> "

Inherited

This property is inherited " ++ (Maybe.withDefault "" p.hierarchy) ++ "
." - "overridden" -> "

Overridden

This property is overridden on this object and was inherited " ++ (Maybe.withDefault "" p.hierarchy) ++ "
." + "overridden" -> "

Overridden

This property is overridden on this object and was inherited " ++ (Maybe.withDefault "" p.hierarchy) ++ "
." _ -> "

" ++ pr ++ "

This property is managed by its provider ‘" ++ pr ++ "’ and can not be modified manually. Check Rudder’s settings to adjust this provider’s configuration.
" in (span [ class "rudder-label label-provider label-sm bs-tooltip" , attribute "data-bs-toggle" "tooltip" , attribute "data-bs-placement" "right" - , attribute "data-bs-html" "true" - , attribute "data-bs-container" "body" + , attribute "data-bs-trigger" "click" , title pTitle ] [ text pr ] , pr == "overridden" || pr == "inherited" diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodeproperties.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodeproperties.elm index ee2d61a7552..cb788352bc2 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodeproperties.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodeproperties.elm @@ -172,7 +172,7 @@ update msg model = let ui = model.ui in - ({model | ui = { ui | filters = tableFilters}}, Cmd.none) + ({model | ui = { ui | filters = tableFilters}}, initTooltips "") ShowMore id -> let diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/ApiCalls.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/ApiCalls.elm new file mode 100644 index 00000000000..a203436cfe8 --- /dev/null +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/ApiCalls.elm @@ -0,0 +1,46 @@ +module Nodes.ApiCalls exposing (..) + +import Http exposing (..) +import Url.Builder exposing (QueryParameter) + +import Nodes.DataTypes exposing (..) +import Nodes.JsonDecoder exposing (..) +import Nodes.JsonEncoder exposing (..) + + +getUrl: Model -> List String -> List QueryParameter -> String +getUrl m url p= + Url.Builder.relative (m.contextPath :: "secure" :: "api" :: url ) p + +getNodes : Model -> Cmd Msg +getNodes model = + let + req = + request + { method = "GET" + , headers = [] + , url = getUrl model [ "nodes" , "details"] [] + , body = emptyBody + , expect = expectJson GetNodes decodeGetNodes + , timeout = Nothing + , tracker = Nothing + } + in + req + +getNodeDetails : Model -> Cmd Msg +getNodeDetails model = + let + changeAction = "Disable " + req = + request + { method = "POST" + , headers = [] + , url = getUrl model [ "nodes" , "details"] [] + , body = encodeDetails model |> jsonBody + , expect = expectJson GetNodes decodeGetNodeDetails + , timeout = Nothing + , tracker = Nothing + } + in + req \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/DataTypes.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/DataTypes.elm new file mode 100644 index 00000000000..82aec47122e --- /dev/null +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/DataTypes.elm @@ -0,0 +1,41 @@ +module Nodes.DataTypes exposing (..) + +import Http exposing (Error) + +import Ui.Datatable exposing (..) +-- +-- All our data types +-- + +type alias NodeId = { value : String } + +type alias Node = + { id : NodeId + , hostname : String + } + +type SortBy + = Id + | Hostname + +type alias UI = + { hasReadRights : Bool + , loading : Bool + , filters : TableFilters SortBy + , editColumns : Bool + , columns : List SortBy + } + +type alias Model = + { contextPath : String + , policyMode : String + , nodes : List Node + , ui : UI + } + +type Msg + = Ignore + | Copy String + | CallApi (Model -> Cmd Msg) + | GetNodes (Result Error (List Node)) + | UpdateUI UI diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/Init.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/Init.elm new file mode 100644 index 00000000000..1bc21a1638a --- /dev/null +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/Init.elm @@ -0,0 +1,15 @@ +module Nodes.Init exposing (..) + +import Nodes.DataTypes exposing (..) +import Nodes.ApiCalls exposing (getNodeDetails) + + +init : { contextPath : String, hasReadRights : Bool, policyMode : String} -> ( Model, Cmd Msg ) +init flags = + let + initUi = UI flags.hasReadRights True (TableFilters Hostname Asc "") False [] -- TODO : Get columns list from browser cache + initModel = Model flags.contextPath flags.policyMode [] initUi + in + ( initModel + , getNodeDetails initModel + ) \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/JsonDecoder.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/JsonDecoder.elm new file mode 100644 index 00000000000..5cdc0f9b4d3 --- /dev/null +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/JsonDecoder.elm @@ -0,0 +1,21 @@ +module Nodes.JsonDecoder exposing (..) + +import Json.Decode exposing (..) +import Json.Decode.Pipeline exposing (..) + +import Nodes.DataTypes exposing (..) + + +-- GENERAL +decodeGetNodes = + at [ "data", "nodes" ] (list decodeNode) + +decodeGetNodeDetails = + list decodeNode + +decodeNode : Decoder Node +decodeNode = + succeed Node + |> required "id" (map NodeId string) + |> required "name" string + diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/JsonEncoder.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/JsonEncoder.elm new file mode 100644 index 00000000000..bb8b6b255e5 --- /dev/null +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/JsonEncoder.elm @@ -0,0 +1,16 @@ +module Nodes.JsonEncoder exposing (..) + +import Json.Encode exposing (Value, object, string, list) +import Json.Encode.Extra exposing (maybe) + +import Nodes.DataTypes exposing (..) + +encodeDetails : Model -> Value +encodeDetails model = + let + data = object + [ ("properties" , list string [] ) + , ("software" , list string [] ) + ] + in + data \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/View.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/View.elm new file mode 100644 index 00000000000..cfb8d75ca96 --- /dev/null +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/View.elm @@ -0,0 +1,79 @@ +module Nodes.View exposing (..) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick, onInput) + +import Nodes.DataTypes exposing (..) +import Nodes.ViewUtils exposing (..) +import Nodes.ApiCalls exposing (getNodeDetails) + +import Ui.Datatable exposing (generateLoadingTable) + +view : Model -> Html Msg +view model = + if model.ui.hasReadRights then + let + nodes = model.nodes + ui = model.ui + filters = ui.filters + + editColumnsBtn = + if ui.editColumns then + button [class "btn btn-success btn-sm btn-icon", style "min-width" "120px", onClick (UpdateUI {ui | editColumns = False})] + [ text "Confirm" + , i [class "fa fa-check"][] + ] + else + button [class "btn btn-default btn-sm btn-icon", style "min-width" "120px", onClick (UpdateUI {ui | editColumns = True})] + [ text "Edit columns" + , i [class "fa fa-pencil"][] + ] + + displayColumnsEdit = + if ui.editColumns then + let + colOptions = allColumns + |> List.filter (\c -> not (List.member c ui.columns)) + |> List.map (\c -> option[value (getColumnTitle c)][text (getColumnTitle c)]) + in + div[class "more-filters edit-columns"] + [ select[](colOptions) + ] + else + text "" + in + div [class "rudder-template"] + [ div [class "one-col"] + [ div [class "main-header"] + [ div [class "header-title"] + [ h1[][ span[][text "Nodes"] ] + ] + ] + , div [class "one-col-main"] + [ div [class "template-main"] + [ if model.ui.loading then + generateLoadingTable False 5 + else + div [class "main-table tab-table-content col-sm-12"] + [ div [class "table-header extra-filters"] + [ div [class "main-filters"] + [ input [type_ "text", placeholder "Filter", class "input-sm form-control", onInput (\s -> UpdateUI {ui | filters = {filters | filter = s}})][] + , editColumnsBtn + , button [class "btn btn-default btn-sm btn-refresh", onClick (CallApi getNodeDetails)][i [class "fa fa-refresh"][]] + ] + , displayColumnsEdit + ] + , div [class "table-container"] + [ table [ class "no-footer dataTable"] + [ thead [] [nodesTableHeader model.ui] + , tbody [] (buildNodesTable model) + ] + ] + ] + ] + ] + ] + ] + else + text "No rights" diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/ViewUtils.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/ViewUtils.elm new file mode 100644 index 00000000000..835cbbe03dd --- /dev/null +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Nodes/ViewUtils.elm @@ -0,0 +1,117 @@ +module Nodes.ViewUtils exposing (..) + +import Html exposing (..) +import Html.Attributes exposing (class, href, attribute, title, style, colspan, rowspan ) +import Html.Events exposing (onClick, onInput) +import Json.Decode exposing (decodeValue) +import NaturalOrdering as N exposing (compare) + +import Nodes.DataTypes exposing (..) +import Ui.Datatable exposing (..) + + +getSortFunction : Model -> Node -> Node -> Order +getSortFunction model n1 n2 = + let + order = case model.ui.filters.sortBy of + Id -> N.compare n1.id.value n2.id.value + Hostname -> N.compare n1.hostname n2.hostname + _ -> N.compare n1.hostname n2.hostname + -- TODO : Add all cases + in + if model.ui.filters.sortOrder == Asc then + order + else + case order of + LT -> GT + EQ -> EQ + GT -> LT + +searchField : Node -> List String +searchField node = + [ node.id.value + , node.hostname + ] + +buildNodesTable : Model -> List (Html Msg) +buildNodesTable model = + let + nodes = model.nodes + sortedNodesList = nodes + |> List.filter (\n -> filterSearch model.ui.filters.filter (searchField n)) + |> List.sortWith (getSortFunction model) + + rowTable : Node -> Html Msg + rowTable n = + let + test = "" + in + tr[] + [ td[][ text n.hostname ] + , td[][ text n.id.value ] + ] + in + if List.length sortedNodesList > 0 then + List.map rowTable sortedNodesList + else + [ tr[][td [class "empty", colspan 5][i [class "fa fa-exclamation-triangle"][], text "No nodes match your filters."]]] + +nodesTableHeader : UI -> Html Msg +nodesTableHeader ui = + let + filters = ui.filters + in + tr [class "head"] + [ th [ class (thClass filters Hostname) , rowspan 1, colspan 1 + , onClick (UpdateUI {ui | filters = (sortTable filters Hostname)}) + ] [ text "Hostname" ] + , th [ class (thClass filters Id) , rowspan 1, colspan 1 + , onClick (UpdateUI {ui | filters = (sortTable filters Id)}) + ] [ text "Id" ] + ] + +allColumns : List SortBy +allColumns = + [ Hostname + , Id + , PolicyServer + , Ram + , AgentVersion + , Software "" + , NodeProperty "" False + , PolicyMode + , IpAddresses + , MachineType + , Kernel + , Os + , NodeCompliance + , LastRun + , InventoryDate + ] + +defaultColumns : List SortBy +defaultColumns = + [ Hostname + , PolicyMode + , Os + , NodeCompliance + ] + +getColumnTitle : SortBy -> String +getColumnTitle col = + case col of + Hostname -> "Hostname" + Id -> "Node ID" + PolicyServer -> "Policy server" + Ram -> "RAM" + AgentVersion -> "Agent version" + Software _ -> "Software" + NodeProperty _ _ -> "Property" + PolicyMode -> "Policy mode" + IpAddresses -> "IP addresses" + MachineType -> "Machine type" + Kernel -> "Kernel" + Os -> "OS" + NodeCompliance -> "Compliance" + LastRun -> "Last run" + InventoryDate -> "Inventory date" \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-web/src/main/javascript/rudder/rudder.js b/webapp/sources/rudder/rudder-web/src/main/javascript/rudder/rudder.js index 3eeb2cb30d0..51e889dcc31 100644 --- a/webapp/sources/rudder/rudder-web/src/main/javascript/rudder/rudder.js +++ b/webapp/sources/rudder/rudder-web/src/main/javascript/rudder/rudder.js @@ -373,7 +373,17 @@ $(document).ready(function() { } } ); sidebarControl(); + // Init tooltips initBsTooltips(); + + // Hide any open tooltips when the anywhere else in the body is clicked + $('body').on('click', function (e) { + $('[data-bs-toggle=tooltip]').each(function () { + if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.tooltip').has(e.target).length === 0) { + $(this).tooltip('hide'); + } + }); + }); }); function checkMigrationButton(currentVersion,selectId) { @@ -864,12 +874,22 @@ function logout(cb){ function initBsTooltips(){ var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); return tooltipTriggerList.map(function (tooltipTriggerEl) { - return new bootstrap.Tooltip(tooltipTriggerEl,{container : "body", html : true, trigger : 'hover'}); + let dataTrigger = $(tooltipTriggerEl).attr('data-bs-trigger'); + let trigger = dataTrigger === undefined ? 'hover' : dataTrigger; + return new bootstrap.Tooltip(tooltipTriggerEl,{container : "body", html : true, trigger : trigger}); }); } function removeBsTooltips(){ document.querySelectorAll(".tooltip").forEach(e => e.remove()); } +$('body').on('click', function (e) { + $('[data-bs-toggle=tooltip]').each(function () { + // hide any open tooltips when the anywhere else in the body is clicked + if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.tooltip').has(e.target).length === 0) { + $(this).tooltip('hide'); + } + }); +}); function initBsModal(modalName){ var selector = document.querySelector('#' + modalName); var modal = bootstrap.Modal.getInstance(selector); diff --git a/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-main.scss b/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-main.scss index 3915fc9527d..8858f92bf8c 100644 --- a/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-main.scss +++ b/webapp/sources/rudder/rudder-web/src/main/style/rudder/rudder-main.scss @@ -543,87 +543,101 @@ pre.json-beautify code.elmsh { #groupTree ul.jstree-container-ul>li>ul>li>a .tooltip{ left:0 !important; } + +// TOOLTIP .tooltip{ z-index:9999; -} -.tooltip > .tooltip-inner { - background-color: #fff; - padding: 0px !important; - text-align: left !important; - width: 360px; - max-width: 360px; - font-size: 12px; - box-shadow: 0px 0px 5px rgba(0, 0, 0, .12); - border-radius: 0px; - border: 1px solid #ccc; - border-radius: 3px; -} -.tooltip > .tooltip-inner > ul > li { - margin-bottom: 4px; -} -.tooltip > .tooltip-inner > ul { - margin-bottom: 0; -} -.tooltip.top > .tooltip-arrow{ - border-top-color: #ccc; -} -.tooltip.bottom > .tooltip-arrow{ - border-bottom-color: #ccc; -} -.tooltip.right > .tooltip-arrow{ - border-right-color: #ccc; -} -.tooltip > .tooltip-inner h4{ - background-color: #fff; - font-weight: 600; - border-bottom: 1px solid #d6deef; - padding: 5px; - font-size: 14px; - margin: 10px 8px; -} -.tooltip > .tooltip-inner > div { - padding: 0 8px; -} -.tooltip > .tooltip-inner i.empty { - color: #738195; -} -.tooltip > .tooltip-inner .small{ - color: #738195; - margin-left: 4px; -} -.tooltip > .tooltip-inner label{ - margin-right: 4px; -} -form .tooltip-content p { - padding:0; -} -.tooltip > .tooltip-inner .tooltip-content{ - padding: 0px 10px 10px 10px; -} -.tooltip > .tooltip-inner .tooltip-content i b{ - color:#041922; - background: none; - -webkit-background-clip: initial; - -webkit-text-fill-color: initial; -} -.tooltip > .tooltip-inner .tooltip-content p{ - margin-bottom: 5px; -} -.tooltip > .tooltip-inner .tooltip-content > ul{ - padding-left: 0; -} -.tooltip > .tooltip-inner .tooltip-content > ul:last-child, -.tooltip > .tooltip-inner .tooltip-content > p:last-child{ - margin-bottom : 0; -} -.tooltip.in { - opacity: 1; - filter: alpha(opacity=100); -} -.tooltip .tooltip-inner-content > ul:last-child { - margin-bottom: 0; + & > .tooltip-inner { + background-color: #fff; + padding: 0px !important; + text-align: left !important; + width: auto; + max-width: 460px; + font-size: 12px; + box-shadow: 0px 0px 5px rgba(0, 0, 0, .12); + border-radius: 0px; + border: 1px solid #ccc; + border-radius: 3px; + + & > div { + padding: 0 8px; + } + .tooltip-content{ + padding: 0px 10px 10px 10px; + p { + padding:0; + margin-bottom: 5px; + } + i b{ + color:#041922; + background: none; + -webkit-background-clip: initial; + -webkit-text-fill-color: initial; + } + ul{ + padding-left: 0; + } + & > ul:last-child, + & > p:last-child{ + margin-bottom : 0; + } + } + & > .tooltip-inner-content{ + max-height: 400px; + overflow: auto; + + pre{ + width: 100%; + word-wrap: break-word; + white-space: pre-wrap; + } + + & > :last-child{ + margin-bottom: 0; + } + } + & > ul { + margin-bottom: 0; + & > li { + margin-bottom: 4px; + } + } + h4{ + background-color: #fff; + font-weight: 600; + border-bottom: 1px solid #d6deef; + padding: 5px; + font-size: 14px; + margin: 10px 8px; + } + i.empty { + color: #738195; + } + .small{ + color: #738195; + margin-left: 4px; + } + label{ + margin-right: 4px; + } + } + &.top > .tooltip-arrow{ + border-top-color: #ccc; + } + &.bottom > .tooltip-arrow{ + border-bottom-color: #ccc; + } + &.right > .tooltip-arrow{ + border-right-color: #ccc; + } + &.in { + opacity: 1; + filter: alpha(opacity=100); + } } + +// LABEL .content-wrapper .rudder-label{ cursor: help; display: inline-block; @@ -634,6 +648,10 @@ form .tooltip-content p { border-radius: 3px; min-width: 78px; background-color: #738195; + + &[data-bs-trigger=click] { + cursor: pointer !important; + } } .text-align-center{ @@ -663,7 +681,6 @@ form .tooltip-content p { } .label-provider, .dataTable .rudder-label.label-sm.label-provider { - background-color: #999; padding:0 5px !important; margin-top:4px; margin-left: 6px;