diff --git a/pom.xml b/pom.xml index 49139ec..1913af8 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,16 @@ workflow-multibranch 2.21 + + io.jenkins.plugins + data-tables-api + 1.10.20-12 + + + com.github.spotbugs + spotbugs-annotations + 4.0.0 + diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/BuildAddUrl.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/BuildAddUrl.java index e33849f..899a083 100644 --- a/src/main/java/org/jenkinsci/plugins/environmentdashboard/BuildAddUrl.java +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/BuildAddUrl.java @@ -8,20 +8,37 @@ import jenkins.tasks.SimpleBuildStep; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import javax.annotation.Nonnull; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Arrays; + public class BuildAddUrl extends Builder implements SimpleBuildStep { private final String title; private final String url; - + private String users; + private String groups; + @DataBoundConstructor public BuildAddUrl(String title, String url) { this.url = url; this.title = title; } + + @DataBoundSetter + public void setUsers(String users) { + this.users = users; + } + + @DataBoundSetter + public void setGroups(String groups) { + this.groups = groups; + } public String getTitle() { return title; @@ -31,6 +48,14 @@ public String getUrl() { return url; } + public String getUsers() { + return users; + } + + public String getGroups() { + return groups; + } + @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; @@ -43,7 +68,7 @@ public void perform( @Nonnull Launcher launcher, @Nonnull TaskListener listener ) throws InterruptedException, IOException { - run.addAction(new BuildUrlAction(title, url)); + run.addAction(new BuildUrlAction(title, url, users, groups)); } @Extension @@ -64,15 +89,49 @@ public boolean isApplicable(Class extends AbstractProject> t) { public static class BuildUrlAction implements Action { private final String title; private final String url; + private final ArrayList userList = new ArrayList(); + private final ArrayList groupList = new ArrayList(); - BuildUrlAction(String title, String url) { + BuildUrlAction(String title, String url, String users, String groups) { this.title = title; this.url = url; + if (users != null && !"".equals(users)) { + this.userList.addAll(Arrays.asList(users.split(","))); + } + if (groups != null && !"".equals(groups)) { + this.groupList.addAll(Arrays.asList(groups.split(","))); + } } @Override public String getIconFileName() { - return String.format("/plugin/%s/deploy.png", getClass().getPackage().getImplementationTitle()); + String iconFileName = String.format("/plugin/%s/deploy.png", getClass().getPackage().getImplementationTitle()); + + User currentUser = User.current(); + if (currentUser == null) return null; + + String currentUserId = currentUser.getId(); + List currentUserGroups = currentUser.getAuthorities(); + + if (!checkPermissions() || isUserInList(currentUserId) || isUserInGroup(currentUserGroups)) + return iconFileName; + + return null; + } + + private boolean checkPermissions() { + return userList.size() > 0 || groupList.size() > 0; + } + + private boolean isUserInList(String userId) { + return userList.contains(userId); + } + + private boolean isUserInGroup(List userGroups) { + for(String userGroup: userGroups) + if (groupList.contains(userGroup)) + return true; + return false; } @Override diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableConfiguration.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableConfiguration.java new file mode 100644 index 0000000..b39aee7 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableConfiguration.java @@ -0,0 +1,34 @@ +package org.jenkinsci.plugins.environmentdashboard; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.jenkins.plugins.datatables.TableConfiguration; + +public class DeploymentTableConfiguration extends TableConfiguration{ + + private final Map configuration = new HashMap<>(); + + DeploymentTableConfiguration() { + super(); + + this.configuration.put("rowsGroup", new int[]{0}); + this.configuration.put("pagingType", "full_numbers"); + this.configuration.put("stateSave", true); + } + + @Override + public String getConfiguration() { + try { + return new ObjectMapper().writeValueAsString(configuration); + } + catch (JsonProcessingException exception) { + throw new IllegalArgumentException( + String.format("Can't convert table configuration '%s' to JSON object", configuration), exception); + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableModel.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableModel.java new file mode 100644 index 0000000..c5a7265 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableModel.java @@ -0,0 +1,151 @@ +package org.jenkinsci.plugins.environmentdashboard; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.jenkinsci.plugins.environmentdashboard.Deployment.DeploymentAction; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; + +import hudson.model.Job; +import hudson.model.Run; +import hudson.model.TopLevelItem; +import io.jenkins.plugins.datatables.TableColumn; +import io.jenkins.plugins.datatables.TableConfiguration; +import io.jenkins.plugins.datatables.TableModel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +public class DeploymentTableModel extends TableModel { + private String id; + List extends TopLevelItem> items; + + DeploymentTableModel(String id) { + this.id = id; + } + + public TableModel populate(List extends TopLevelItem> items) { + this.items = items; + return this; + } + + private String getStatusHTML(String altText, String imageUrl, String href) { + // Issue with URL + href = href.substring(href.indexOf("/job")); + return new StringBuffer().append("") + .toString(); + } + + private String getPopupHTML(int row, int col, String envName) { + return new StringBuffer().append("").append(envName).append("") + .toString(); + } + + @Override + public String getId() { + return id; + } + + @Override + public TableConfiguration getTableConfiguration() { + return new DeploymentTableConfiguration(); + } + + @Override + public List getColumns() { + List columns = new ArrayList(); + columns.add(new TableColumn("Job", "job")); + columns.add(new TableColumn("Environment", "environment")); + columns.add(new TableColumn("Release", "release")); + columns.add(new TableColumn("Result", "result")); + columns.add(new TableColumn("Completed", "completed")); + return columns; + } + + @Override + public List getRows() { + List rows = new ArrayList(); + + int i=0; + for (Unit unit: getUnits(items)) { + int j=0; + for (Unit.Environment env: unit.environments) { + DeploymentAction action = env.getCurrentAction(); + Run run = action.getRun(); + rows.add(new DeploymentTableRow(unit.job.getName(), + getPopupHTML(i, j, env.name), + action.getBuildNumber(), + getStatusHTML(run.getDescription(), + run.getIconColor().getImageOf("32x32"), + run.getUrl()), + run.getTimestampString() + " ago")); + ++j; + } + ++i; + } + return rows; + } + + private List getEnvs(TopLevelItem item) { + List runs = Collections.emptyList(); + if (item instanceof WorkflowMultiBranchProject) { + runs = ((WorkflowMultiBranchProject) item) + .getItems() + .stream() + .map(Job::getBuilds) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } else if (item instanceof WorkflowJob) { + runs = ((WorkflowJob) item).getBuilds(); + } + + return runs + .stream() + .map(run -> run.getActions(DeploymentAction.class)) + .flatMap(List::stream) + .filter(Objects::nonNull) + .collect(Collectors.groupingBy(DeploymentAction::getEnv)) + .entrySet() + .stream() + .map(e -> new Unit.Environment(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + } + + public List getUnits(List extends TopLevelItem> items) { + return items.stream() + .map(item -> new Unit(item, getEnvs(item))) + .filter(unit -> !unit.getEnvironments().isEmpty()) + .collect(Collectors.toList()); + } + + @Getter + @RequiredArgsConstructor + public static class Unit { + private final TopLevelItem job; + private final List environments; + + @Getter + @RequiredArgsConstructor + public static class Environment { + private final String name; + private final List actions; + + public DeploymentAction getCurrentAction() { + return actions.get(0); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableRow.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableRow.java new file mode 100644 index 0000000..23f3877 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentTableRow.java @@ -0,0 +1,22 @@ +package org.jenkinsci.plugins.environmentdashboard; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class DeploymentTableRow { + private String job; + private String environment; + private String release; + private String result; + private String completed; + + DeploymentTableRow(String job, String environment, String release, String result, String completed) { + this.job = job; + this.environment = environment; + this.release = release; + this.result = result; + this.completed = completed; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java index 7a26624..0cc4adb 100644 --- a/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java +++ b/src/main/java/org/jenkinsci/plugins/environmentdashboard/DeploymentView.java @@ -1,88 +1,37 @@ package org.jenkinsci.plugins.environmentdashboard; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.annotation.Nonnull; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.bind.JavaScriptMethod; + import hudson.Extension; import hudson.Util; -import hudson.model.Job; import hudson.model.ListView; import hudson.model.TopLevelItem; import hudson.model.ViewDescriptor; import hudson.util.FormValidation; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +import io.jenkins.plugins.datatables.AsyncTableContentProvider; +import io.jenkins.plugins.datatables.TableModel; import net.sf.json.JSONObject; -import org.jenkinsci.plugins.environmentdashboard.Deployment.DeploymentAction; -import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.jenkinsci.plugins.workflow.job.WorkflowRun; -import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest; -import javax.annotation.Nonnull; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import java.util.stream.Collectors; +public class DeploymentView extends ListView implements AsyncTableContentProvider{ + private DeploymentTableModel model; -public class DeploymentView extends ListView { @DataBoundConstructor - public DeploymentView(String name) { + public DeploymentView(final String name) { super(name); } - private List getEnvs(TopLevelItem item) { - List runs = Collections.emptyList(); - if (item instanceof WorkflowMultiBranchProject) { - runs = ((WorkflowMultiBranchProject) item) - .getItems() - .stream() - .map(Job::getBuilds) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - } else if (item instanceof WorkflowJob) { - runs = ((WorkflowJob) item).getBuilds(); - } - - return runs - .stream() - .map(run -> run.getAction(DeploymentAction.class)) - .filter(Objects::nonNull) - .collect(Collectors.groupingBy(DeploymentAction::getEnv)) - .entrySet() - .stream() - .map(e -> new Unit.Environment(e.getKey(), e.getValue())) - .collect(Collectors.toList()); - } - - public List getUnits(List extends TopLevelItem> items) { - return items - .stream() - .map(item -> new Unit(item, getEnvs(item))) - .filter(unit -> !unit.getEnvironments().isEmpty()) - .collect(Collectors.toList()); - } - - @Getter - @RequiredArgsConstructor - public static class Unit { - private final TopLevelItem job; - private final List environments; - - @Getter - @RequiredArgsConstructor - public static class Environment { - private final String name; - private final List actions; - - public DeploymentAction getCurrentAction() { - return actions.get(0); - } - } - } - @Extension public static class DeploymentViewDescriptor extends ViewDescriptor { public DeploymentViewDescriptor() { @@ -96,13 +45,14 @@ public String getDisplayName() { return "Deployment View"; } - // Copy-n-paste from ListView$Descriptor as sadly we cannot inherit from that class - public FormValidation doCheckIncludeRegex(@QueryParameter String value) { - String v = Util.fixEmpty(value); + // Copy-n-paste from ListView$Descriptor as sadly we cannot inherit from that + // class + public FormValidation doCheckIncludeRegex(@QueryParameter final String value) { + final String v = Util.fixEmpty(value); if (v != null) { try { Pattern.compile(v); - } catch (PatternSyntaxException pse) { + } catch (final PatternSyntaxException pse) { return FormValidation.error(pse.getMessage()); } } @@ -110,10 +60,42 @@ public FormValidation doCheckIncludeRegex(@QueryParameter String value) { } @Override - public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + public boolean configure(final StaplerRequest req, final JSONObject json) throws FormException { save(); return true; } } + + + @Override + public TableModel getTableModel(String id) { + if (model == null) { + model = new DeploymentTableModel(id); + } + return model; + } + + public TableModel getTableModel(String id, List extends TopLevelItem> items) { + if (model == null) { + model = new DeploymentTableModel(id); + } + return model.populate(items); + } + + @Override + @JavaScriptMethod + public String getTableRows(final String id) { + return toJsonArray(getTableModel(id).getRows()); + } + + private String toJsonArray(final List rows) { + try { + return new ObjectMapper().writeValueAsString(rows); + } + catch (JsonProcessingException exception) { + throw new IllegalArgumentException( + String.format("Can't convert table rows '%s' to JSON object", rows), exception); + } + } } diff --git a/src/main/resources/org/jenkinsci/plugins/environmentdashboard/DeploymentView/main.jelly b/src/main/resources/org/jenkinsci/plugins/environmentdashboard/DeploymentView/main.jelly index 01b762b..8997d57 100644 --- a/src/main/resources/org/jenkinsci/plugins/environmentdashboard/DeploymentView/main.jelly +++ b/src/main/resources/org/jenkinsci/plugins/environmentdashboard/DeploymentView/main.jelly @@ -1,9 +1,9 @@ + xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt" xmlns:dt="/data-tables"> - + @@ -15,77 +15,22 @@ - - - - - - - - Job - - - - - Environment - - - - - Release - - - - - Result - - - - - Completed - - - - - - - - - ${unit.getJob().name} - - - - - ${environment.getName()} - - - - - ${environment.getCurrentAction().buildNumber} - - - - - - - ${environment.getCurrentAction().run.timestampString} - - - - - - - - + + + + + + - + - - ${unit.getJob().name} | ${environment.getName()} + ${unit.getJob().name} | ${environment.getName()} + + + @@ -111,16 +56,22 @@ - - ${deployment.buildNumber} - + ${deployment.buildNumber} + [ + changes + ] - + + + [ + console log + ] - ${deployment.run.timestampString} + ${deployment.run.timestampString} ago @@ -132,8 +83,7 @@ - - + + \ No newline at end of file diff --git a/src/main/webapp/js/dataTables.rowsGroup.js b/src/main/webapp/js/dataTables.rowsGroup.js new file mode 100644 index 0000000..2dc3868 --- /dev/null +++ b/src/main/webapp/js/dataTables.rowsGroup.js @@ -0,0 +1,285 @@ +/*! RowsGroup for DataTables v2.0.0 + * 2015-2016 Alexey Shildyakov ashl1future@gmail.com + * 2016 Tibor Wekerle + */ + +/** + * @summary RowsGroup + * @description Group rows by specified columns + * @version 2.0.0 + * @file dataTables.rowsGroup.js + * @author Alexey Shildyakov (ashl1future@gmail.com) + * @contact ashl1future@gmail.com + * @copyright Alexey Shildyakov + * + * License MIT - http://datatables.net/license/mit + * + * This feature plug-in for DataTables automatically merges columns cells + * based on it's values equality. It supports multi-column row grouping + * in according to the requested order with dependency from each previous + * requested columns. Now it supports ordering and searching. + * Please see the example.html for details. + * + * Rows grouping in DataTables can be enabled by using any one of the following + * options: + * + * * Setting the `rowsGroup` parameter in the DataTables initialisation + * to array which containes columns selectors + * (https://datatables.net/reference/type/column-selector) used for grouping. i.e. + * rowsGroup = [1, 'columnName:name', ] + * * Setting the `rowsGroup` parameter in the DataTables defaults + * (thus causing all tables to have this feature) - i.e. + * `$.fn.dataTable.defaults.RowsGroup = [0]`. + * * Creating a new instance: `new $.fn.dataTable.RowsGroup( table, columnsForGrouping );` + * where `table` is a DataTable's API instance and `columnsForGrouping` is the array + * described above. + * + * For more detailed information please see: + * + */ + +(function($){ + +ShowedDataSelectorModifier = { + order: 'current', + page: 'current', + search: 'applied', +} + +GroupedColumnsOrderDir = 'asc'; + + +/* + * columnsForGrouping: array of DTAPI:cell-selector for columns for which rows grouping is applied + */ +var RowsGroup = function ( dt, columnsForGrouping ) +{ + this.table = dt.table(); + this.columnsForGrouping = columnsForGrouping; + // set to True when new reorder is applied by RowsGroup to prevent order() looping + this.orderOverrideNow = false; + this.mergeCellsNeeded = false; // merge after init + this.order = [] + + var self = this; + dt.on('order.dt', function ( e, settings) { + if (!self.orderOverrideNow) { + self.orderOverrideNow = true; + self._updateOrderAndDraw() + } else { + self.orderOverrideNow = false; + } + }) + + dt.on('preDraw.dt', function ( e, settings) { + if (self.mergeCellsNeeded) { + self.mergeCellsNeeded = false; + self._mergeCells() + } + }) + + dt.on('column-visibility.dt', function ( e, settings) { + self.mergeCellsNeeded = true; + }) + + dt.on('search.dt', function ( e, settings) { + // This might to increase the time to redraw while searching on tables + // with huge shown columns + self.mergeCellsNeeded = true; + }) + + dt.on('page.dt', function ( e, settings) { + self.mergeCellsNeeded = true; + }) + + dt.on('length.dt', function ( e, settings) { + self.mergeCellsNeeded = true; + }) + + dt.on('xhr.dt', function ( e, settings) { + self.mergeCellsNeeded = true; + }) + + this._updateOrderAndDraw(); + +/* Events sequence while Add row (also through Editor) + * addRow() function + * draw() function + * preDraw() event + * mergeCells() - point 1 + * Appended new row breaks visible elements because the mergeCells() on previous step doesn't apllied to already processing data + * order() event + * _updateOrderAndDraw() + * preDraw() event + * mergeCells() + * Appended new row now has properly visibility as on current level it has already applied changes from first mergeCells() call (point 1) + * draw() event + */ +}; + + +RowsGroup.prototype = { + setMergeCells: function(){ + this.mergeCellsNeeded = true; + }, + + mergeCells: function() + { + this.setMergeCells(); + this.table.draw('page'); + }, + + _getOrderWithGroupColumns: function (order, groupedColumnsOrderDir) + { + if (groupedColumnsOrderDir === undefined) + groupedColumnsOrderDir = GroupedColumnsOrderDir + + var self = this; + var groupedColumnsIndexes = this.columnsForGrouping.map(function(columnSelector){ + return self.table.column(columnSelector).index() + }) + var groupedColumnsKnownOrder = order.filter(function(columnOrder){ + return groupedColumnsIndexes.indexOf(columnOrder[0]) >= 0 + }) + var nongroupedColumnsOrder = order.filter(function(columnOrder){ + return groupedColumnsIndexes.indexOf(columnOrder[0]) < 0 + }) + var groupedColumnsKnownOrderIndexes = groupedColumnsKnownOrder.map(function(columnOrder){ + return columnOrder[0] + }) + var groupedColumnsOrder = groupedColumnsIndexes.map(function(iColumn){ + var iInOrderIndexes = groupedColumnsKnownOrderIndexes.indexOf(iColumn) + if (iInOrderIndexes >= 0) + return [iColumn, groupedColumnsKnownOrder[iInOrderIndexes][1]] + else + return [iColumn, groupedColumnsOrderDir] + }) + + groupedColumnsOrder.push.apply(groupedColumnsOrder, nongroupedColumnsOrder) + return groupedColumnsOrder; + }, + + // Workaround: the DT reset ordering to 'asc' from multi-ordering if user order on one column (without shift) + // but because we always has multi-ordering due to grouped rows this happens every time + _getInjectedMonoSelectWorkaround: function(order) + { + if (order.length === 1) { + // got mono order - workaround here + var orderingColumn = order[0][0] + var previousOrder = this.order.map(function(val){ + return val[0] + }) + var iColumn = previousOrder.indexOf(orderingColumn); + if (iColumn >= 0) { + // assume change the direction, because we already has that in previos order + return [[orderingColumn, this._toogleDirection(this.order[iColumn][1])]] + } // else This is the new ordering column. Proceed as is. + } // else got milti order - work normal + return order; + }, + + _mergeCells: function() + { + var columnsIndexes = this.table.columns(this.columnsForGrouping, ShowedDataSelectorModifier).indexes().toArray() + var showedRowsCount = this.table.rows(ShowedDataSelectorModifier)[0].length + this._mergeColumn(0, showedRowsCount - 1, columnsIndexes) + }, + + // the index is relative to the showed data + // (selector-modifier = {order: 'current', page: 'current', search: 'applied'}) index + _mergeColumn: function(iStartRow, iFinishRow, columnsIndexes) + { + var columnsIndexesCopy = columnsIndexes.slice() + currentColumn = columnsIndexesCopy.shift() + currentColumn = this.table.column(currentColumn, ShowedDataSelectorModifier) + + var columnNodes = currentColumn.nodes() + var columnValues = currentColumn.data() + + var newSequenceRow = iStartRow, + iRow; + for (iRow = iStartRow + 1; iRow <= iFinishRow; ++iRow) { + + if (columnValues[iRow] === columnValues[newSequenceRow]) { + $(columnNodes[iRow]).hide() + } else { + $(columnNodes[newSequenceRow]).show() + $(columnNodes[newSequenceRow]).attr('rowspan', (iRow-1) - newSequenceRow + 1) + + if (columnsIndexesCopy.length > 0) + this._mergeColumn(newSequenceRow, (iRow-1), columnsIndexesCopy) + + newSequenceRow = iRow; + } + + } + $(columnNodes[newSequenceRow]).show() + $(columnNodes[newSequenceRow]).attr('rowspan', (iRow-1)- newSequenceRow + 1) + if (columnsIndexesCopy.length > 0) + this._mergeColumn(newSequenceRow, (iRow-1), columnsIndexesCopy) + }, + + _toogleDirection: function(dir) + { + return dir == 'asc'? 'desc': 'asc'; + }, + + _updateOrderAndDraw: function() + { + this.mergeCellsNeeded = true; + + var currentOrder = this.table.order(); + currentOrder = this._getInjectedMonoSelectWorkaround(currentOrder); + this.order = this._getOrderWithGroupColumns(currentOrder) + this.table.order($.extend(true, Array(), this.order)) + this.table.draw('page') + }, +}; + + +$.fn.dataTable.RowsGroup = RowsGroup; +$.fn.DataTable.RowsGroup = RowsGroup; + +// Automatic initialisation listener +$(document).on( 'init.dt', function ( e, settings ) { + if ( e.namespace !== 'dt' ) { + return; + } + + var api = new $.fn.dataTable.Api( settings ); + + if ( settings.oInit.rowsGroup || + $.fn.dataTable.defaults.rowsGroup ) + { + options = settings.oInit.rowsGroup? + settings.oInit.rowsGroup: + $.fn.dataTable.defaults.rowsGroup; + var rowsGroup = new RowsGroup( api, options ); + $.fn.dataTable.Api.register( 'rowsgroup.update()', function () { + rowsGroup.mergeCells(); + return this; + } ); + $.fn.dataTable.Api.register( 'rowsgroup.updateNextDraw()', function () { + rowsGroup.setMergeCells(); + return this; + } ); + } +} ); + +}(jQuery)); + +/* + +TODO: Provide function which determines the all s and s with "rowspan" html-attribute is parent (groupped) for the specified or . To use in selections, editing or hover styles. + +TODO: Feature +Use saved order direction for grouped columns + Split the columns into grouped and ungrouped. + + user = grouped+ungrouped + grouped = grouped + saved = grouped+ungrouped + + For grouped uses following order: user -> saved (because 'saved' include 'grouped' after first initialisation). This should be done with saving order like for 'groupedColumns' + For ungrouped: uses only 'user' input ordering +*/