From f1c407f361eb40bd33fbd38f9007d417c4be81cd Mon Sep 17 00:00:00 2001 From: Raphael Gauthier Date: Wed, 8 Jan 2020 13:52:29 +0100 Subject: [PATCH] Work in progress --- .../components/NodeGroupCategoryForm.scala | 145 +++++++++------- .../rudder/web/components/NodeGroupForm.scala | 105 ++++++------ .../web/components/SearchNodeComponent.scala | 71 +++++--- .../components/popup/RuleCategoryPopup.scala | 10 +- .../rudder/web/model/RudderBaseField.scala | 2 +- .../rudder/web/snippet/node/Groups.scala | 11 +- .../rudder/web/snippet/node/SearchNodes.scala | 2 +- .../main/webapp/javascript/rudder/rudder.js | 2 - .../webapp/secure/nodeManager/groups.html | 97 ++++------- .../secure/nodeManager/searchNodes.html | 2 +- .../webapp/style/rudder/rudder-datatable.css | 1 + .../webapp/style/rudder/rudder-groups.css | 156 +++++++++++------- .../main/webapp/style/rudder/rudder-menu.css | 104 +++++++++--- .../main/webapp/style/rudder/rudder-node.css | 4 +- .../src/main/webapp/style/rudder/rudder.css | 41 ++--- .../ComponentDirectiveEditForm.html | 2 +- .../components/NodeGroupForm.html | 139 ++++++++-------- .../server/server_details.html | 36 ++-- 18 files changed, 499 insertions(+), 431 deletions(-) diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupCategoryForm.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupCategoryForm.scala index ae5eb70081b..4e3db05f537 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupCategoryForm.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupCategoryForm.scala @@ -100,42 +100,59 @@ class NodeGroupCategoryForm( def showForm() : NodeSeq = { val html = SHtml.ajaxForm( -
-
-
Category details
+
+
+
+

+ + +

+ +
+ + + + +
+
+
+
+ +
+
+ + + + +
+ + +
+
- -
- -
- -
- -
-
- Rudder ID: -
{_nodeGroupCategory.id.value}
-
-
- -
-
) ( - "directive-name" #> name.toForm_! + "category-name" #> name + & "directive-name" #> name.toForm_! & "directive-description" #> description.toForm_! & "directive-container" #> container.toForm_! & "directive-notifications" #> updateAndDisplayNotifications() & (if (_nodeGroupCategory.isSystem) ( "input [disabled]" #> "true" & "textarea [disabled]" #> "true" - & "directive-save" #> SHtml.ajaxSubmit("Update", onSubmit _ , ("class","btn btn-success pull-right")) + & "directive-save" #> SHtml.ajaxSubmit("Update", onSubmit _ , ("class","btn btn-success")) & "directive-delete" #> deleteButton ) else ( "directive-save" #> ( - if (CurrentUser.checkRights(AuthorizationType.Group.Edit)) SHtml.ajaxSubmit("Update", onSubmit _ , ("class","btn btn-default")) + if (CurrentUser.checkRights(AuthorizationType.Group.Edit)) SHtml.ajaxSubmit("Update", onSubmit _ , ("class","btn btn-success")) else NodeSeq.Empty ) & "directive-delete" #> ( @@ -155,38 +172,35 @@ class NodeGroupCategoryForm( private[this] def deleteButton : NodeSeq = { if(parentCategory.isDefined && _nodeGroupCategory.children.isEmpty && _nodeGroupCategory.items.isEmpty) { - ( - -
- + ( + ) ++ Script(JsRaw(""" @@ -196,8 +210,8 @@ class NodeGroupCategoryForm( }); """)) } else { -
-
Note: Only empty and non root categories can be deleted.
+ ( Only empty and non root categories can be deleted.
"}>Delete + ) ++ Script(JsRaw("""$('.btn-tooltip').bsTooltip();""")) } } @@ -218,13 +232,18 @@ class NodeGroupCategoryForm( ///////////// fields for category settings /////////////////// private[this] val name = new WBTextField("Category name", _nodeGroupCategory.name) { override def setFilter = notNull _ :: trim _ :: Nil + override def className = "form-control" + override def labelClassName = "" + override def subContainerClassName = "" override def validations = valMinLen(1, "Name must not be empty") _ :: Nil } private[this] val description = new WBTextAreaField("Category description", _nodeGroupCategory.description.toString) { override def setFilter = notNull _ :: trim _ :: Nil - override def inputField = super.inputField % ("style" -> "height:10em") + override def className = "form-control" + override def labelClassName = "" + override def subContainerClassName = "" override def validations = Nil override def errorClassName = "field_errors paddscala" } @@ -239,7 +258,9 @@ class NodeGroupCategoryForm( _nodeGroupCategory.id .value, Seq("disabled" -> "true") ) { - override def className = "rudderBaseFieldSelectClassName" + override def className = "form-control" + override def labelClassName = "" + override def subContainerClassName = "" override def validations = valMinLen(1, "Please select a category") _ :: Nil } @@ -249,7 +270,9 @@ class NodeGroupCategoryForm( , categoryHierarchyDisplayer.getCategoriesHierarchy(rootCategory, exclude = Some( _.id == _nodeGroupCategory.id)). map { case (id, name) => (id.value -> name)} , parentCategoryId) { - override def className = "rudderBaseFieldSelectClassName" + override def className = "form-control" + override def labelClassName = "" + override def subContainerClassName = "" override def validations = valMinLen(1, "Please select a category") _ :: Nil } @@ -263,7 +286,7 @@ class NodeGroupCategoryForm( SetHtml(htmlIdCategory, showForm()) } - private[this] def error(msg:String) = {msg} + private[this] def error(msg:String) = {msg} private[this] def onSuccess : JsCmd = { @@ -273,7 +296,7 @@ class NodeGroupCategoryForm( private[this] def onFailure : JsCmd = { formTracker.addFormError(error("There was problem with your request.")) - updateFormClientSide & JsRaw("""scrollToElement("notifications","#groupDetails");""") + updateFormClientSide & JsRaw("""scrollToElement("notifications","#ajaxItemContainer");""") } private[this] def onSubmit() : JsCmd = { diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupForm.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupForm.scala index 97c514f19de..fc76a7bf596 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupForm.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/NodeGroupForm.scala @@ -144,7 +144,7 @@ class NodeGroupForm( def mainDispatch = Map( "showForm" -> { _:NodeSeq => showForm() }, "showGroup" -> { _:NodeSeq => searchNodeComponent.get match { - case Full(component) => component.buildQuery + case Full(component) => component.buildQuery(true) case _ =>
The component is not set
} } ) @@ -180,85 +180,88 @@ class NodeGroupForm( val rules = dependencyService.targetDependencies(target).map( _.rules.toSet.filter(!_.isSystem).map(_.id)).toOption RuleGrid.staticInit ++ ruleGrid.rulesGridWithUpdatedInfo(None, false, false) ++ Script(OnLoad(ruleGrid.asyncDisplayAllRules(rules).applied)) } - + private[this] val groupNameString = nodeGroup.fold( + t => rootCategory.allTargets.get(t).map(_.name).getOrElse(t.target) + , _.name + ) private[this] def showFormNodeGroup(nodeGroup: NodeGroup): CssSel = { + val nodesSel = "#gridResult" #> NodeSeq.Empty + val nodes = nodesSel(searchNodeComponent.get match { + case Full(req) => req.buildQuery(true) + case eb:EmptyBox => Error when retrieving the request, please try again + }) ( - "group-pendingchangerequest" #> PendingChangeRequestDisplayer.checkByGroup(pendingChangeRequestXml,nodeGroup.id) + "#group-name" #> groupNameString + & "group-pendingchangerequest" #> PendingChangeRequestDisplayer.checkByGroup(pendingChangeRequestXml,nodeGroup.id) & "group-name" #> groupName.toForm_! & "group-rudderid" #>
& "group-cfeclasses" #>
- - - - - -
& "#longDescriptionField *" #> (groupDescription.toForm_! ++ Script(OnLoad(JsRaw(s"""setupMarkdown(${Str(nodeGroup.description).toJsCmd}, "longDescriptionField")""")))) & "group-container" #> groupContainer.toForm_! & "group-static" #> groupStatic.toForm_! - & "group-showgroup" #> (searchNodeComponent.get match { - case Full(req) => req.buildQuery - case eb:EmptyBox => Error when retrieving the request, please try again - }) + & "group-showgroup" #> nodes & "group-clone" #> { if (CurrentUser.checkRights(AuthorizationType.Group.Write)) SHtml.ajaxButton("Clone", () => showCloneGroupPopup()) % ("id" -> "groupCloneButtonId") % ("class" -> " btn btn-default") else NodeSeq.Empty } & "group-save" #> { if (CurrentUser.checkRights(AuthorizationType.Group.Edit)) -
{ + { SHtml.ajaxSubmit("Save", onSubmit _) % ("id" -> saveButtonId) % ("class" -> " btn btn-success") - }
+ } else NodeSeq.Empty } & "group-delete" #> SHtml.ajaxButton("Delete", () => onSubmitDelete(), ("class" -> " btn btn-danger")) & "group-notifications" #> updateAndDisplayNotifications() & "#groupRuleTabsContent" #> showRulesForTarget(GroupTarget(nodeGroup.id)) + & "#group-shownodestable *" #> (searchNodeComponent.get match { + case Full(req) => req.displayNodesTable + case eb:EmptyBox => Error when retrieving the request, please try again + }) ) } private[this] def showFormTarget(target: SimpleTarget): CssSel = { - // we want to remove the query part which doesn't mean anything for - // system group - val nodesSel = "#SearchForm" #> NodeSeq.Empty - val nodes = nodesSel(searchNodeComponent.get match { - case Full(req) => req.buildQuery - case eb:EmptyBox => Error when retrieving the request, please try again - }) - - ( - "group-pendingchangerequest" #> NodeSeq.Empty - & "group-name" #> groupName.readOnlyValue - & "group-rudderid" #>
- - -
- & "group-cfeclasses" #> NodeSeq.Empty - & "#longDescriptionField" #> (groupDescription.toForm_! ++ Script(JsRaw(s"""setupMarkdown("", "longDescriptionField")"""))) - & "group-container" #> groupContainer.readOnlyValue - & "group-static" #> NodeSeq.Empty - & "group-showgroup" #> nodes - & "group-clone" #> NodeSeq.Empty - & "group-save" #> NodeSeq.Empty - & "group-delete" #> NodeSeq.Empty - & "group-notifications" #> NodeSeq.Empty - & "#groupRuleTabsContent" #> showRulesForTarget(target) + ( "group-pendingchangerequest" #> NodeSeq.Empty + & "#group-name" #> {groupNameString} + & "group-name" #> groupName.readOnlyValue + & "#groupTabMenu" #> + & "group-rudderid" #>
+ + +
+ & "group-cfeclasses" #> NodeSeq.Empty + & "#longDescriptionField" #> (groupDescription.toForm_! ++ Script(JsRaw(s"""setupMarkdown("", "longDescriptionField")"""))) + & "group-container" #> groupContainer.readOnlyValue + & "group-static" #> NodeSeq.Empty + & "group-showgroup" #> NodeSeq.Empty + & "group-clone" #> NodeSeq.Empty + & "group-save" #> NodeSeq.Empty + & "group-delete" #> NodeSeq.Empty + & "group-notifications" #> NodeSeq.Empty + & "#groupRuleTabsContent" #> showRulesForTarget(target) + & "#group-shownodestable *" #> (searchNodeComponent.get match { + case Full(req) => req.displayNodesTable + case eb:EmptyBox => Error when retrieving the request, please try again + }) ) - } ///////////// fields for category settings /////////////////// + private[this] val groupName = { - val name = nodeGroup.fold( - t => rootCategory.allTargets.get(t).map(_.name).getOrElse(t.target) - , _.name - ) - new WBTextField("Group name", name) { + new WBTextField("Group name", groupNameString) { override def setFilter = notNull _ :: trim _ :: Nil override def className = "form-control" override def labelClassName = "" @@ -309,13 +312,13 @@ class NodeGroupForm( } ) { override def setFilter = notNull _ :: trim _ :: Nil - override def className = "" + override def className = "switch" override def labelClassName = "" override def subContainerClassName = "" } } - private[this] val groupContainer = new WBSelectField("Group container", + private[this] val groupContainer = new WBSelectField("Category", (categoryHierarchyDisplayer.getCategoriesHierarchy(rootCategory, None).map { case (id, name) => (id.value -> name)}), parentCategoryId.value) { override def className = "form-control" @@ -333,7 +336,7 @@ class NodeGroupForm( private[this] def onFailure : JsCmd = { formTracker.addFormError(error("There was problem with your request.")) - updateFormClientSide() & JsRaw("""scrollToElement("errorNotification","#groupDetails");""") + updateFormClientSide() & JsRaw("""scrollToElement("errorNotification","#ajaxItemContainer");""") } private[this] def onSubmit() : JsCmd = { diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/SearchNodeComponent.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/SearchNodeComponent.scala index 56c8b37fcd4..fe758da11e5 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/SearchNodeComponent.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/SearchNodeComponent.scala @@ -95,7 +95,10 @@ class SearchNodeComponent( List("templates-hidden", "server", "server_details") , "query-searchnodes" ) - + private[this] def nodesTable = ChooseTemplate( + List("templates-hidden", "server", "server_details") + , "nodes-table" + ) private[this] def queryline = { @@ -127,7 +130,7 @@ class SearchNodeComponent( def getQuery() : Option[Query] = query var dispatch : DispatchIt = { - case "showQuery" => { _ => buildQuery } + case "showQuery" => { _ => buildQuery(false)} } var initUpdate = true // this is true when we arrive on the page, or when we've done an search @@ -138,13 +141,12 @@ class SearchNodeComponent( val errors = MutMap[CriterionLine,String]() - def buildQuery : NodeSeq = { + def buildQuery(isGroupsPage : Boolean) : NodeSeq = { if(None == query) query = Some(Query(NodeReturnType,And,List(defaultLine))) val lines = ArrayBuffer[CriterionLine]() var composition = query.get.composition var rType = query.get.returnType //for now, don't move - def addLine(i:Int) : JsCmd = { if(i >= 0) { //used same info than previous line @@ -155,7 +157,7 @@ class SearchNodeComponent( } query = Some(Query(rType, composition, lines.to[List])) initUpdate = false - ajaxCriteriaRefresh + ajaxCriteriaRefresh(isGroupsPage) } def removeLine(i:Int) : JsCmd ={ @@ -172,10 +174,10 @@ class SearchNodeComponent( query = Some(Query(rType, composition, lines.to[List])) } initUpdate = false - ajaxCriteriaRefresh + ajaxCriteriaRefresh(isGroupsPage) } - def processForm() : JsCmd = { + def processForm(isGroupPage : Boolean) : JsCmd = { //filter on non validate values errors.clear() lines.zipWithIndex.foreach { case (cl@CriterionLine(_,a,c,v),i) => @@ -196,31 +198,31 @@ class SearchNodeComponent( srvList = Empty searchFormHasError = true } - ajaxCriteriaRefresh & ajaxGridRefresh + ajaxCriteriaRefresh(isGroupsPage) & ajaxGridRefresh(isGroupsPage) + //ajaxGroupCriteriaRefresh & ajaxNodesTableRefresh() } /** * Refresh the query parameter part */ - def ajaxCriteriaRefresh : JsCmd = { + def ajaxCriteriaRefresh(isGroupPage : Boolean) : JsCmd = { lines.clear() - SetHtml("SearchForm", displayQuery(content))& activateButtonOnChange + SetHtml("SearchForm", displayQuery(content, isGroupPage)) & activateButtonOnChange } - def displayQueryLine(cl : CriterionLine, index:Int, addRemove:Boolean) : NodeSeq = { lines.append(cl) val initJs = cl.attribute.cType.initForm("v_"+index) - val inputAttributes = ("id","v_"+index) :: ("class", "queryInputValue") :: {if (cl.comparator.hasValue) Nil else ("disabled", "disabled") :: Nil } + val inputAttributes = ("id","v_"+index) :: ("class", "queryInputValue form-control input-sm") :: {if (cl.comparator.hasValue) Nil else ("disabled", "disabled") :: Nil } val input = cl.attribute.cType.toForm(cl.value, (x => lines(index) = lines(index).copy(value=x)), inputAttributes:_*) ( ".removeLine *" #> { if(addRemove) - SHtml.ajaxSubmit("-", () => removeLine(index), ("class", "removeLineButton btn btn-default btn-xs")) + SHtml.ajaxSubmit("-", () => removeLine(index), ("class", "removeLineButton btn btn-danger btn-xs")) else NodeSeq.Empty } & - ".addLine *" #> SHtml.ajaxSubmit("+", () => addLine(index), ("class", "removeLineButton btn btn-default btn-xs")) & + ".addLine *" #> SHtml.ajaxSubmit("+", () => addLine(index), ("class", "removeLineButton btn btn-success btn-xs")) & ".objectType *" #> objectTypeSelect(cl.objectType,lines,index) & ".attributeName *" #> attributeNameSelect(cl.objectType,cl.attribute,lines,index) & ".comparator *" #> comparatorSelect(cl.objectType,cl.attribute,cl.comparator,lines,index) & @@ -239,7 +241,7 @@ class SearchNodeComponent( * Caution, we pass an html different at the init part (whole content-query) * */ - def displayQuery(html: NodeSeq) = { + def displayQuery(html: NodeSeq, isGroupPage: Boolean) = { val Query(otName,comp, criteria) = query.get val checkBox = { SHtml.checkbox( @@ -270,7 +272,7 @@ class SearchNodeComponent( ( "#typeQuery" #> checkBox & "#composition" #> radio & - "#submitSearch * " #> SHtml.ajaxSubmit("Search", () => processForm, ("id" -> "SubmitSearch"), ("class" -> "submitButton btn btn-default")) & + "#submitSearch * " #> SHtml.ajaxSubmit("Search", () => processForm(isGroupsPage), ("id" -> "SubmitSearch"), ("class" -> "submitButton btn btn-primary")) & "#query_lines *" #> criteria.zipWithIndex.flatMap { case (cl,i) => displayQueryLine(cl,i, criteria.size > 1)} ).apply(html /*}:NodeSeq} ++ { if(criteria.size > 0) { @@ -284,20 +286,34 @@ class SearchNodeComponent( processKey(event , 'SubmitSearch') } ); """))) - } + /** + * Display the query part for group criteria + * Caution, we pass an html different at the init part (whole content-query) + * + */ /* * Show the search engine and the grid */ def showQueryAndGridContent() : NodeSeq = { ( - "content-query" #> {x:NodeSeq => displayQuery(x)} + "content-query" #> {x:NodeSeq => displayQuery(x, false)} & "update-gridresult" #> srvGrid.displayAndInit(Seq(),"serverGrid") // we need to set something, or IE moans )(searchNodes) } - showQueryAndGridContent() ++ Script(OnLoad(ajaxGridRefresh)) + showQueryAndGridContent() ++ Script(OnLoad(ajaxGridRefresh(false))) + } + + def displayNodesTable: NodeSeq = { + def showQueryAndGridContent() : NodeSeq = { + ( + "content-query" #> NodeSeq.Empty + & "update-nodestable" #> srvGrid.displayAndInit(Seq(),"groupNodesTable") // we need to set something, or IE moans + )(nodesTable) + } + showQueryAndGridContent() ++ Script(OnLoad(ajaxGridRefresh(true))) } /** @@ -306,9 +322,9 @@ class SearchNodeComponent( * zone, it must be removed before being added again, or problems will rises * @return */ - def ajaxGridRefresh() : JsCmd = { + def ajaxGridRefresh(isGroupPage: Boolean) : JsCmd = { activateButtonOnChange & - gridResult + gridResult(isGroupPage) } /** @@ -328,11 +344,12 @@ class SearchNodeComponent( /** * From the computed result, return the NodeSeq corresponding to the grid, plus the initialisation JS */ - def gridResult : JsCmd = { + def gridResult(isGroupsPage: Boolean) : JsCmd = { // Ideally this would just check the size first ? + val tableId = if(isGroupsPage){"groupNodesTable"}else{"serverGrid"} srvList match { case Full(seq) => - val refresh = srvGrid.refreshData(() => seq, onClickCallback, "serverGrid") + val refresh = srvGrid.refreshData(() => seq, onClickCallback, tableId) JsRaw(s"""(${refresh.toJsCmd}());createTooltip();""") case Empty => @@ -425,7 +442,7 @@ object SearchNodeComponent { case Nil => "" } comp.destroyForm(v_eltid) & - JsRaw("jQuery('#%s').replaceWith('%s')".format(v_eltid,comp.toForm(v_old,func,("id"->v_eltid), ("class" -> "queryInputValue")))) & + JsRaw("jQuery('#%s').replaceWith('%s')".format(v_eltid,comp.toForm(v_old,func,("id"->v_eltid), ("class" -> "queryInputValue form-control input-sm")))) & comp.initForm(v_eltid) & JsCmds.ReplaceOptions(c_eltid,comparators,Full(selectedComp)) & setIsEnableFor(selectedComp,v_eltid) & @@ -541,7 +558,7 @@ object SearchNodeComponent { }), ("id","ot_"+i), ("onchange", ajaxAttr(lines,i)._2.toJsCmd), - ("class","selectField") + ("class","selectField form-control input-sm") ) } @@ -555,7 +572,7 @@ object SearchNodeComponent { }), ("id","at_"+i), ("onchange", ajaxComp(lines,i)._2.toJsCmd), - ("class","selectField") + ("class","selectField form-control input-sm") ) } @@ -568,7 +585,7 @@ object SearchNodeComponent { }), ("id","ct_"+i), ("onchange", ajaxVal(lines,i)._2.toJsCmd), - ("class","selectComparator") + ("class","selectComparator form-control input-sm") ) } diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/popup/RuleCategoryPopup.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/popup/RuleCategoryPopup.scala index 3e2c71e579c..9e26c4a1a55 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/popup/RuleCategoryPopup.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/components/popup/RuleCategoryPopup.scala @@ -118,13 +118,9 @@ class RuleCategoryPopup( case None => NodeSeq.Empty case Some(c) =>
- -
- - -
-
+ + +
}) & "#saveCategory" #> SHtml.ajaxSubmit(TextForButton, () => onSubmit(), ("id", "createRuleCategorySaveButton") , ("tabindex","5"), ("style","margin-left:5px;")) andThen ".notifications *" #> updateAndDisplayNotifications() diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/model/RudderBaseField.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/model/RudderBaseField.scala index c9f46d68883..c74dbdc4edd 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/model/RudderBaseField.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/model/RudderBaseField.scala @@ -212,7 +212,7 @@ abstract class RudderBaseField extends BaseField {
-
+
{defaultValue}
diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/node/Groups.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/node/Groups.scala index bf08159b2d2..b3115f625b7 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/node/Groups.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/node/Groups.scala @@ -317,12 +317,6 @@ class Groups extends StatefulSnippet with DefaultExtendableSnippet[Groups] with $$.jstree.rollback(data.rlbk); } }); - adjustHeight('#groupsTree'); - adjustHeight('#groupDetails'); - $$(window).on('resize',function(){ - adjustHeight('#groupsTree'); - adjustHeight('#groupDetails'); - }); """)) )} } @@ -427,7 +421,7 @@ class Groups extends StatefulSnippet with DefaultExtendableSnippet[Groups] with selectedCategoryId = Full(category.id) //update UI - no modification here, so no refreshGroupLib refreshRightPanel(CategoryForm(category)) & - JsRaw("""$('#groupDetails').show();""") + JsRaw("""$('#ajaxItemContainer').show();""") } //adaptater @@ -440,10 +434,9 @@ class Groups extends StatefulSnippet with DefaultExtendableSnippet[Groups] with } refreshRightPanel(GroupForm(g, parentCategoryId))& JsRaw(s""" - jQuery('#groupDetails').show(); + jQuery('#ajaxItemContainer').show(); var groupId = JSON.stringify({${js}}); window.location.hash = "#"+groupId; - adjustHeight('#groupDetails'); """) } diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/node/SearchNodes.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/node/SearchNodes.scala index ace503fe6df..6482c58aa5f 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/node/SearchNodes.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/snippet/node/SearchNodes.scala @@ -167,7 +167,7 @@ class SearchNodes extends StatefulSnippet with Loggable { } def queryForm(sc : SearchNodeComponent) = { - SHtml.ajaxForm(sc.buildQuery) + SHtml.ajaxForm(sc.buildQuery(false)) } /** diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/javascript/rudder/rudder.js b/webapp/sources/rudder/rudder-web/src/main/webapp/javascript/rudder/rudder.js index 8e1f5683d1a..d6e30925226 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/javascript/rudder/rudder.js +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/javascript/rudder/rudder.js @@ -825,8 +825,6 @@ function generateMarkdown(text, container) { } function setupMarkdown(initialValue, id) { - console.log($("#" + id)) - console.log($("#" + id + " textarea")) $("#" + id + " textarea").keyup(function() { var value = $(this).val() console.log(value) diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/secure/nodeManager/groups.html b/webapp/sources/rudder/rudder-web/src/main/webapp/secure/nodeManager/groups.html index 9211638b328..3a8f9fd5ff3 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/secure/nodeManager/groups.html +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/secure/nodeManager/groups.html @@ -3,86 +3,49 @@ Rudder - Node Groups Management - + -
-
-
-
- Group hierarchy - -
Here comes the New item Button
-
-
-
-
-
-
-
- -
- -
- -
+
+ + + +
- -
-
+
diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-datatable.css b/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-datatable.css index b56c2b8759a..c0bd78ef4fc 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-datatable.css +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-datatable.css @@ -28,6 +28,7 @@ height: 30px; padding: 5px 10px; font-size: 12px; + font-weight: initial; line-height: 1.5; border-radius: 3px; border: 1px solid #d0d0d0; diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-groups.css b/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-groups.css index c1c527d0a71..d8e4ba9e4b4 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-groups.css +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-groups.css @@ -1,61 +1,57 @@ -.nodeGroupContainer .col-sm{ - background-color:transparent; -} -.nodeGroupContainer .col-sm>div{ - background-color:#fff; - float:left; - border-color: #ddd; - box-shadow: 0px 10px 10px -8px rgba(0, 0, 0, 0.15); - padding:0 15px; - width: 100%; - overflow: auto; -} -#groupParametersTab .btn.btn-success[disabled]{ - pointer-events: none; - background-color: #b4b4b4; - border-color: #7a7a7a; -} -#groupParametersTab .section-title{ - margin: 15px 13px 8px 8px; +/* +************************************************************************************* +* Copyright 2020 Normation SAS +************************************************************************************* +* +* This file is part of Rudder. +* +* Rudder is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* In accordance with the terms of section 7 (7. Additional Terms.) of +* the GNU General Public License version 3, the copyright holders add +* the following Additional permissions: +* Notwithstanding to the terms of section 5 (5. Conveying Modified Source +* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General +* Public License version 3, when you create a Related Module, this +* Related Module is not considered as a part of the work and may be +* distributed under the license agreement of your choice. +* A "Related Module" means a set of sources files including their +* documentation that, without modification of the Source Code, enables +* supplementary functions or services in addition to those offered by +* the Software. +* +* Rudder is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Rudder. If not, see . +* +************************************************************************************* +*/ +.rudder-template .header-title h1 .group-system:before, +.rudder-template .header-title h1 .group-system:after{ + margin-left: 8px; + opacity: .4; + font-size: 0.8em; +} +.rudder-template .header-title h1 .group-system:before{ + content: "-"; +} +.rudder-template .header-title h1 .group-system:after{ + content: "System"; } -.toggle-caret,.content-wrapper .toggle-caret,.content-wrapper label.toggle-caret{ - display:block; - text-decoration:none !important; -} -.toggle-caret label{ - cursor: pointer; -} -.toggle-caret .caret{ - transition-duration:.2s; - -webkit-transform: rotate(-90deg); - -ms-transform: rotate(-90deg); - -o-transform: rotate(-90deg); - transform: rotate(-90deg); - color:#333; -} -.toggle-caret.open .caret{ - -webkit-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); -} -.toggle-caret + .well{ - color:#555; - padding: 6px 12px; - background-color: #eeeeee; - border: 1px solid #d0d0d0; -} -#groupDetails{ - overflow-x: hidden; - overflow-y: auto; - border-left: 15px solid #ebf0f5; -} -#groupDetails .form-group.row{ - padding: 0px 27px 0px 22px; -} -#groupDetails .well.row{ - margin-left:auto; - margin-right:auto; + +#groupParametersTab, +#groupCriteriaTab, +#categoryParametersTab{ + padding: 0 15px; + float: left; + width: 100%; } #groupDetails .radio-inline .radioTextLabel{ margin:0; @@ -67,9 +63,6 @@ #pendingChangeRequestNotification + .wbBaseField .wbBaseFieldLabel{ margin:0 0 0 5px; } -#groupsTree > .page-title{ - padding-bottom:7px; -} #serverGrid tr.head th { padding: 4px 6px; } @@ -78,4 +71,45 @@ width: 100%; margin-bottom: 10px; } - +content-query{ + display:block; +} +#query_lines{ + width:100%; + table-layout:fixed; +} +#query_lines td{ + white-space: nowrap;} +#query_lines tbody tr td input, #query_lines tbody tr td select.selectField, #query_lines tbody tr td input.queryInputValue, #query_lines tbody tr td select.queryInputValue{ + width:100%; +} +#query_lines tbody tr td:first-child{ + width:130px; +} +#query_lines tbody tr td:nth-child(2){ + width:170px; +} +#query_lines tbody tr td:nth-child(3){ + width: 140px; +} +#query_lines tbody tr td:nth-child(5),#query_lines tbody tr td.last{ + width:25px; +} +.searchNodes .query_line .form-control.input-sm { + height: 24px; + padding: 0px 2px; +} +.rudder-form.form-sm{ + margin-bottom: 12px; +} +.searchNodes { + margin-top: 15px; +} +#ajaxItemContainer > form{ + height: 100%; +} +@media screen and (max-width: 1200px){ + #query_lines tbody tr td:nth-child(4){ + width :250px; + } +} \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-menu.css b/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-menu.css index e3245ecd0fd..a2db7344088 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-menu.css +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-menu.css @@ -234,7 +234,7 @@ pre.json-beautify.toggle:after, .content-wrapper pre.json-beautify.toggle:after{ } /* FAKE INPUT DISABLED */ .content-wrapper .well.well-disabled, .content-wrapper .readonly-field > div > div{ - cursor: no-drop; + cursor: default; padding: 6px 12px; background-color: #eee; border: 1px solid #d0d0d0; @@ -243,15 +243,7 @@ pre.json-beautify.toggle:after, .content-wrapper pre.json-beautify.toggle:after{ font-size: 12px; margin-bottom: 0; } -.content-wrapper .readonly-field > div{ - padding-left: 15px; - padding-right: 15px; - floart:left; -} -.content-wrapper .readonly-field > div > div{ - border-radius:4px; - min-height:31px; -} + /* TEXTAREA */ .content-wrapper textarea.form-control{ resize:vertical; @@ -1182,9 +1174,6 @@ ul.graph-legend > li.legend:hover{ #nodeStateSubmit{ margin-top:20px; } -#submitSearch { - padding-left: 7px; -} .errorSaving.alert-danger{ margin-top:10px; } @@ -2284,16 +2273,19 @@ form.rudder-form .policymode-group .btn-group .btn.enforce.active{ } /* RADIO BUTTONS + CHECKBOX */ -.rudder-form input[type=radio],.rudder-form input[type=checkbox]{ +.rudder-form input, +.rudder-form input{ display: none; } -.rudder-form input[type=radio]:disabled + .label-radio,.rudder-form input[type=checkbox]:disabled + .label-radio{ + +.rudder-form input:disabled + .label-radio{ background-color: rgb(236,236,236); } -.rudder-form input[type=radio]:disabled + .label-radio span,.rudder-form input[type=checkbox]:disabled + .label-radio span{ +.rudder-form input:disabled + .label-radio span,{ color:#898989; } -.rudder-form input[type=radio]:checked:disabled + .label-radio + .text-radio, .rudder-form input[type=checkbox]:checked:disabled + .label-radio + .text-radio,.rudder-form input[type=radio]:disabled + .label-radio + .text-radio, .rudder-form input[type=checkbox]:disabled + .label-radio + .text-radio{ +.rudder-form input:checked:disabled + .label-radio + .text-radio, +.rudder-form input:disabled + .label-radio + .text-radio{ color: #B7B4B4; -webkit-animation:none; -moz-animation:none; @@ -2321,7 +2313,7 @@ form.rudder-form .policymode-group .btn-group .btn.enforce.active{ -webkit-box-shadow: none; box-shadow: none; } -.rudder-form input[type=radio] + .label-radio,.rudder-form input[type=checkbox] + .label-radio{ +.rudder-form input + .label-radio{ background-color: #fff; display: inline-block; height: 15px; @@ -2334,22 +2326,24 @@ form.rudder-form .policymode-group .btn-group .btn.enforce.active{ margin-bottom:0; background-color: #fff; } +.rudder-form input[type=radio] + input[type=hidden] + .label-radio, .rudder-form input[type=radio] + .label-radio{ border-radius: 50%; } -.rudder-form input[type=radio] + .label-radio + .text-radio,.rudder-form input[type=checkbox] + .label-radio + .text-radio{ +.rudder-form input + .label-radio + .text-radio{ -webkit-animation: text-radio-off .2s ease-in forwards; -moz-animation: text-radio-off .2s ease-in forwards; -o-animation: text-radio-off .2s ease-in forwards; animation: text-radio-off .2s ease-in forwards; } -.rudder-form input[type=radio]:checked + .label-radio + .text-radio,.rudder-form input[type=checkbox]:checked + .label-radio + .text-radio{ +.rudder-form input:checked + .label-radio + .text-radio{ -webkit-animation: text-radio-on .2s ease-in forwards; -moz-animation: text-radio-on .2s ease-in forwards; -o-animation: text-radio-on .2s ease-in forwards; animation: text-radio-on .2s ease-in forwards; } -.rudder-form input[type=checkbox] + .label-radio span{ +.rudder-form input[type=checkbox] + input[type=hidden] + .label-radio span, +.rudder-form input[type=checkbox] + .label-radio span{ font-size: 9px; top: -12.5px; left: 0.1px; @@ -2358,6 +2352,8 @@ form.rudder-form .policymode-group .btn-group .btn.enforce.active{ animation: opacity-0 .1s linear forwards; color: #222D32; } + +.rudder-form input[type=radio] + input[type=hidden] + .label-radio span, .rudder-form input[type=radio] + .label-radio span{ color: #757575; font-size: 9px; @@ -2367,7 +2363,8 @@ form.rudder-form .policymode-group .btn-group .btn.enforce.active{ visibility:hidden; animation: opacity-0 .1s linear forwards; } -.rudder-form input[type=radio]:checked + .label-radio span,.rudder-form input[type=checkbox]:checked + .label-radio span{ +.rudder-form input:checked + input[type=hidden] + .label-radio span, +.rudder-form input:checked + .label-radio span{ visibility:visible; animation: opacity-1 .1s linear forwards; } @@ -2378,10 +2375,12 @@ form.rudder-form .policymode-group .btn-group .btn.enforce.active{ } /* CHECKBOX */ +.rudder-form input[type=checkbox] + input[type=hidden] + .label-radio, .rudder-form input[type=checkbox] + .label-radio{ border-radius: 3px; } -.rudder-form input[type=checkbox] + .label-radio + .check-icon,.rudder-form input[type=radio] + .label-radio + .check-icon{ +.rudder-form input + input[type=hidden] + .label-radio + .check-icon, +.rudder-form input + .label-radio + .check-icon{ position: absolute; right: 15px; top: 10px; @@ -2390,10 +2389,16 @@ form.rudder-form .policymode-group .btn-group .btn.enforce.active{ visibility:hidden; animation: opacity-0 .1s linear forwards; } -.rudder-form .disabled input[type=checkbox] + .label-radio + .check-icon,.rudder-form .disabled input[type=radio] + .label-radio + .check-icon{ +.rudder-form.form-sm input + input[type=hidden] + .label-radio + .check-icon, +.rudder-form.form-sm input + .label-radio + .check-icon{ + top: 8px; +} +.rudder-form .disabled input + input[type=hidden] + .label-radio + .check-icon, +.rudder-form .disabled input + .label-radio + .check-icon{ color: #7d7d7d; } -.rudder-form input[type=checkbox]:checked + .label-radio + .check-icon,.rudder-form input[type=radio]:checked + .label-radio + .check-icon{ +.rudder-form input:checked + input[type=hidden] + .label-radio + .check-icon, +.rudder-form input:checked + .label-radio + .check-icon{ visibility:visible; animation: opacity-1 .1s linear forwards; } @@ -2625,6 +2630,14 @@ ul > li.rudder-form > .input-group .input-group-addon{ padding: 3px 11px; background-color: #f4f4f4; } +ul > li.rudder-form.form-sm > .input-group .input-group-addon{ + padding: 0px 8px; +} +ul > li.rudder-form.form-sm > .input-group label.form-control{ + height: 30px; + color: #222d42; + font-size: 12px; +} ul.list-sm > li.rudder-form > .input-group .input-group-addon{ padding: 0 11px; } @@ -2730,6 +2743,46 @@ ul > li.rudder-form > .input-group.disabled *{ left: 6px; top:1px; } +/* WBASEFIELD RADIO SWITCH */ +.wbBaseField.form-group .switch { + margin-bottom: 10px; +} +.wbBaseField.form-group .switch input{ + display: none !important; +} +.wbBaseField.form-group .switch label.radio-inline{ + padding: 0; + margin: 0; +} +.wbBaseField.form-group .switch label.radio-inline input + .radioTextLabel{ + display: inline-block; + min-width: 75px; + margin: 0; + padding: 2px 10px; + border: 1px solid #e5e5e5; + text-align: center; + color: #222D42aa; + transition-duration: .2s; +} +.wbBaseField.form-group .switch label.radio-inline input + .radioTextLabel:hover{ + background-color: #f4f4f4; + border-color: #ddd; + color: #222D42; +} +.wbBaseField.form-group .switch label.radio-inline input:checked + .radioTextLabel{ + background-color: #3c8dbc; + border-color: #3178a0; + cursor: default; + color: #fff; +} +.wbBaseField.form-group .switch label.radio-inline:first-child input + .radioTextLabel{ + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +.wbBaseField.form-group .switch label.radio-inline:last-child input + .radioTextLabel{ + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} /* BOX */ .box{ margin-bottom: 14px; @@ -3580,7 +3633,6 @@ table a > i.fa-pencil:hover{ margin-bottom: 0; } - /* KEYFRAMES */ @keyframes opacity-1 { 0% {opacity:0;visibility: visible;} diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-node.css b/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-node.css index 16396fc8261..02753860fff 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-node.css +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder-node.css @@ -38,7 +38,9 @@ div#query-search-content { padding: 0 10px 10px 10px; } - +#queryParameters{ + padding: 0 15px; +} .rudder_col>.portlet{ padding: 0 !important; } diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder.css b/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder.css index baa0576f860..7cc0e7fad7b 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder.css +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/style/rudder/rudder.css @@ -190,8 +190,8 @@ form select.selectField { } form input.removeLineButton { - padding: 0em 0.3em; - width:18px !important; + padding: 0; + width: 22px !important; } input.deleteNetwork * { @@ -446,23 +446,11 @@ fieldset.searchNodes { clear: both; } -fieldset.groupCategoryUpdateComponent { - padding:5px; - margin:5px; - width: 580px; - -} - tr.query_line, .query_line td { padding: 4px 1px; text-align: center; } -.composition { - padding: 5px 0px 0px 5px; - margin: 0px 0px 0px 0px; -} - .composition span { display: inline-block; } @@ -1015,7 +1003,7 @@ table.fixedlayout{ vertical-align: middle; } -.treeActiveTechniqueName , .treeActiveTechniqueCategoryName , .treeGroupCategoryName, .treeRuleCategoryName { +.treeActiveTechniqueName , .treeActiveTechniqueCategoryName, .treeRuleCategoryName { color: #555 } @@ -1130,16 +1118,6 @@ fieldset { color: #555555; margin: 5px 0px 0px 0px; } - -.groupCategoryUpdateComponent { - width: 450px; -} - -#ajaxItemContainer { - float:left; - width: 66% -} - /** * Rollback */ @@ -1239,9 +1217,14 @@ li.excluded a{ } .searchNodes tr td { - padding: 0 5px; + padding: 0 5px 5px 5px; +} +.searchNodes tr > td:first-child { + padding-left: 0; +} +.searchNodes tr > td:last-child { + padding-right: 0; } - .deprecatedTechniqueIcon { margin-left : 3px; } @@ -1402,10 +1385,6 @@ label span.text-fit{ text-decoration: none; } -.jstree a.jstree-anchor .greyscala { - color: #999 -} - .jstree a.jstree-anchor .categoryAction, .jstree a.jstree-anchor .treeActions { opacity: 0.2; cursor: pointer !important; diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/components/ComponentDirectiveEditForm.html b/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/components/ComponentDirectiveEditForm.html index 5c5da026c35..87cf66e92fa 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/components/ComponentDirectiveEditForm.html +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/components/ComponentDirectiveEditForm.html @@ -69,7 +69,7 @@

Pending change requests

diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/components/NodeGroupForm.html b/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/components/NodeGroupForm.html index b09523f88f2..8ecd5f144d7 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/components/NodeGroupForm.html +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/components/NodeGroupForm.html @@ -29,83 +29,74 @@ -
- - -
- -
-
-
- Group details -
-
-
- - - - - - -
- -
-
-

No description defined, click on to edit

-
-
-
-
- Here comes the longDescription field -
-
- -
-
-
- - - -
-
Group criteria
-
- -
- - - -
-
-
- - -
-
- -
If your 'Save' button is disabled, it means that you have updated the query without Searching for new Nodes. - Please click on 'Search' to enable saving again
-
-
-
-
+
+
+
+

+ + +

+ +
+ + + +
+ If your 'Save' button is disabled, it means that you have updated the query without Searching for new Nodes. + Please click on 'Search' to enable saving again +
-
-
+ +
-
-
- This group is used in following rule targets (either as a target or exception group): + +
+
+ + + +
+ +
+
+

No description defined, click on to edit

+
+
+
+
+ Here comes the longDescription field +
+
+ +
+
+
+ + +
-
+
+
+ This group is used in following rule targets (either as a target or exception group): +
+
+
+
+ +
+ +
+
+
+
diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/server/server_details.html b/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/server/server_details.html index ef868f0d17d..4417cf4888f 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/server/server_details.html +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/templates-hidden/server/server_details.html @@ -6,16 +6,28 @@
-
-
- - -
- - Match the criteria below with this operand: - - - +
+
    +
  • +
    + + +
    +
  • +
+
+ +
+ +
@@ -29,6 +41,10 @@ + + + + This part is the detail of a node