diff --git a/api/http/handler/endpoints/endpoint_force_update_service.go b/api/http/handler/endpoints/endpoint_force_update_service.go
new file mode 100644
index 000000000..4735c2f32
--- /dev/null
+++ b/api/http/handler/endpoints/endpoint_force_update_service.go
@@ -0,0 +1,108 @@
+package endpoints
+
+import (
+ "context"
+ "net/http"
+ "strings"
+
+ portaineree "github.com/portainer/portainer/api"
+ "github.com/portainer/portainer/api/docker/consts"
+ "github.com/portainer/portainer/api/docker/images"
+ httperror "github.com/portainer/portainer/pkg/libhttp/error"
+ "github.com/portainer/portainer/pkg/libhttp/request"
+ "github.com/portainer/portainer/pkg/libhttp/response"
+
+ "github.com/docker/docker/api/types"
+ dockertypes "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/filters"
+)
+
+type forceUpdateServicePayload struct {
+ // ServiceId to update
+ ServiceID string
+ // PullImage if true will pull the image
+ PullImage bool
+}
+
+func (payload *forceUpdateServicePayload) Validate(r *http.Request) error {
+ return nil
+}
+
+// @id endpointForceUpdateService
+// @summary force update a docker service
+// @description force update a docker service
+// @description **Access policy**: authenticated
+// @tags endpoints
+// @security ApiKeyAuth
+// @security jwt
+// @accept json
+// @produce json
+// @param id path int true "endpoint identifier"
+// @param body body forceUpdateServicePayload true "details"
+// @success 200 {object} dockertypes.ServiceUpdateResponse "Success"
+// @failure 400 "Invalid request"
+// @failure 403 "Permission denied"
+// @failure 404 "endpoint not found"
+// @failure 500 "Server error"
+// @router /endpoints/{id}/forceupdateservice [put]
+func (handler *Handler) endpointForceUpdateService(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
+ endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
+ if err != nil {
+ return httperror.BadRequest("Invalid environment identifier route variable", err)
+ }
+
+ var payload forceUpdateServicePayload
+ err = request.DecodeAndValidateJSONPayload(r, &payload)
+ if err != nil {
+ return httperror.BadRequest("Invalid request payload", err)
+ }
+
+ endpoint, err := handler.DataStore.Endpoint().Endpoint(portaineree.EndpointID(endpointID))
+ if handler.DataStore.IsErrObjectNotFound(err) {
+ return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
+ } else if err != nil {
+ return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
+ }
+
+ err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
+ if err != nil {
+ return httperror.Forbidden("Permission denied to force update service", err)
+ }
+
+ dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "", nil)
+ if err != nil {
+ return httperror.InternalServerError("Error creating docker client", err)
+ }
+ defer dockerClient.Close()
+
+ service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), payload.ServiceID, dockertypes.ServiceInspectOptions{InsertDefaults: true})
+ if err != nil {
+ return httperror.InternalServerError("Error looking up service", err)
+ }
+
+ service.Spec.TaskTemplate.ForceUpdate++
+
+ if payload.PullImage {
+ service.Spec.TaskTemplate.ContainerSpec.Image = strings.Split(service.Spec.TaskTemplate.ContainerSpec.Image, "@sha")[0]
+ }
+
+ newService, err := dockerClient.ServiceUpdate(context.Background(), payload.ServiceID, service.Version, service.Spec, dockertypes.ServiceUpdateOptions{QueryRegistry: true})
+ if err != nil {
+ return httperror.InternalServerError("Error force update service", err)
+ }
+
+ go func() {
+ images.EvictImageStatus(payload.ServiceID)
+ images.EvictImageStatus(service.Spec.Labels[consts.SwarmStackNameLabel])
+ containers, _ := dockerClient.ContainerList(context.TODO(), types.ContainerListOptions{
+ All: true,
+ Filters: filters.NewArgs(filters.Arg("label", consts.SwarmServiceIdLabel+"="+payload.ServiceID)),
+ })
+
+ for _, container := range containers {
+ images.EvictImageStatus(container.ID)
+ }
+ }()
+
+ return response.JSON(w, newService)
+}
diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go
index 59fb8f474..23bff58af 100644
--- a/api/http/handler/endpoints/handler.go
+++ b/api/http/handler/endpoints/handler.go
@@ -6,6 +6,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/demo"
+ dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
@@ -36,6 +37,7 @@ type Handler struct {
K8sClientFactory *cli.ClientFactory
ComposeStackManager portainer.ComposeStackManager
AuthorizationService *authorization.Service
+ DockerClientFactory *dockerclient.ClientFactory
BindAddress string
BindAddressHTTPS string
PendingActionsService *pendingactions.PendingActionsService
@@ -79,6 +81,8 @@ func NewHandler(bouncer security.BouncerService, demoService *demo.Service) *Han
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointRegistryAccess))).Methods(http.MethodPut)
h.Handle("/endpoints/global-key", bouncer.PublicAccess(httperror.LoggerHandler(h.endpointCreateGlobalKey))).Methods(http.MethodPost)
+ h.Handle("/endpoints/{id}/forceupdateservice",
+ bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointForceUpdateService))).Methods(http.MethodPut)
// DEPRECATED
h.Handle("/endpoints/{id}/status", bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)
diff --git a/api/http/server.go b/api/http/server.go
index b14ecaf68..bcc1d4a1e 100644
--- a/api/http/server.go
+++ b/api/http/server.go
@@ -175,6 +175,7 @@ func (server *Server) Start() error {
endpointHandler.ProxyManager = server.ProxyManager
endpointHandler.SnapshotService = server.SnapshotService
endpointHandler.K8sClientFactory = server.KubernetesClientFactory
+ endpointHandler.DockerClientFactory = server.DockerClientFactory
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
endpointHandler.ComposeStackManager = server.ComposeStackManager
endpointHandler.AuthorizationService = server.AuthorizationService
diff --git a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html
deleted file mode 100644
index e905618ca..000000000
--- a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.js b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.js
deleted file mode 100644
index 8d4b918ac..000000000
--- a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.js
+++ /dev/null
@@ -1,11 +0,0 @@
-angular.module('portainer.docker').component('servicesDatatableActions', {
- templateUrl: './servicesDatatableActions.html',
- controller: 'ServicesDatatableActionsController',
- bindings: {
- selectedItems: '=',
- selectedItemCount: '=',
- showUpdateAction: '<',
- showAddAction: '<',
- endpointId: '<',
- },
-});
diff --git a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js
deleted file mode 100644
index c7da7b6e3..000000000
--- a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import { confirmDelete } from '@@/modals/confirm';
-import { confirmServiceForceUpdate } from '@/react/docker/services/common/update-service-modal';
-import { convertServiceToConfig } from '@/react/docker/services/common/convertServiceToConfig';
-
-angular.module('portainer.docker').controller('ServicesDatatableActionsController', [
- '$q',
- '$state',
- 'ServiceService',
- 'Notifications',
- 'ImageHelper',
- 'WebhookService',
- function ($q, $state, ServiceService, Notifications, ImageHelper, WebhookService) {
- const ctrl = this;
-
- this.removeAction = function (selectedItems) {
- confirmDelete('Do you want to remove the selected service(s)? All the containers associated to the selected service(s) will be removed too.').then((confirmed) => {
- if (!confirmed) {
- return;
- }
- removeServices(selectedItems);
- });
- };
-
- this.updateAction = function (selectedItems) {
- confirmServiceForceUpdate('Do you want to force an update of the selected service(s)? All the tasks associated to the selected service(s) will be recreated.').then(
- (result) => {
- if (!result) {
- return;
- }
-
- forceUpdateServices(selectedItems, result.pullLatest);
- }
- );
- };
-
- function forceUpdateServices(services, pullImage) {
- var actionCount = services.length;
- angular.forEach(services, function (service) {
- var config = convertServiceToConfig(service.Model);
- if (pullImage) {
- config.TaskTemplate.ContainerSpec.Image = ImageHelper.removeDigestFromRepository(config.TaskTemplate.ContainerSpec.Image);
- }
-
- // As explained in https://github.com/docker/swarmkit/issues/2364 ForceUpdate can accept a random
- // value or an increment of the counter value to force an update.
- config.TaskTemplate.ForceUpdate++;
- ServiceService.update(service, config)
- .then(function success() {
- Notifications.success('Service successfully updated', service.Name);
- })
- .catch(function error(err) {
- Notifications.error('Failure', err, 'Unable to force update service' + service.Name);
- })
- .finally(function final() {
- --actionCount;
- if (actionCount === 0) {
- $state.reload();
- }
- });
- });
- }
-
- function removeServices(services) {
- var actionCount = services.length;
- angular.forEach(services, function (service) {
- ServiceService.remove(service)
- .then(function success() {
- return WebhookService.webhooks(service.Id, ctrl.endpointId);
- })
- .then(function success(data) {
- return $q.when(data.length !== 0 && WebhookService.deleteWebhook(data[0].Id));
- })
- .then(function success() {
- Notifications.success('Service successfully removed', service.Name);
- })
- .catch(function error(err) {
- Notifications.error('Failure', err, 'Unable to remove service');
- })
- .finally(function final() {
- --actionCount;
- if (actionCount === 0) {
- $state.reload();
- }
- });
- });
- }
- },
-]);
diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.html b/app/docker/components/datatables/services-datatable/servicesDatatable.html
deleted file mode 100644
index fb014777c..000000000
--- a/app/docker/components/datatables/services-datatable/servicesDatatable.html
+++ /dev/null
@@ -1,245 +0,0 @@
-
diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.js b/app/docker/components/datatables/services-datatable/servicesDatatable.js
deleted file mode 100644
index d2449064e..000000000
--- a/app/docker/components/datatables/services-datatable/servicesDatatable.js
+++ /dev/null
@@ -1,22 +0,0 @@
-angular.module('portainer.docker').component('servicesDatatable', {
- templateUrl: './servicesDatatable.html',
- controller: 'ServicesDatatableController',
- bindings: {
- titleText: '@',
- titleIcon: '@',
- dataset: '<',
- tableKey: '@',
- orderBy: '@',
- reverseOrder: '<',
- nodes: '<',
- agentProxy: '<',
- showUpdateAction: '<',
- showAddAction: '<',
- showStackColumn: '<',
- showTaskLogsButton: '<',
- refreshCallback: '<',
- notAutoFocus: '<',
- endpointPublicUrl: '<',
- endpointId: '<',
- },
-});
diff --git a/app/docker/components/datatables/services-datatable/servicesDatatableController.js b/app/docker/components/datatables/services-datatable/servicesDatatableController.js
deleted file mode 100644
index af47eb885..000000000
--- a/app/docker/components/datatables/services-datatable/servicesDatatableController.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import _ from 'lodash-es';
-
-angular.module('portainer.docker').controller('ServicesDatatableController', [
- '$scope',
- '$controller',
- 'DatatableService',
- function ($scope, $controller, DatatableService) {
- angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
-
- var ctrl = this;
-
- this.state = Object.assign(this.state, {
- expandAll: false,
- expandedItems: [],
- });
-
- this.columnVisibility = {
- columns: {
- image: {
- label: 'Image',
- display: true,
- },
- ownership: {
- label: 'OwnerShip',
- display: true,
- },
- ports: {
- label: 'Published Ports',
- display: true,
- },
- updated: {
- label: 'Last Update',
- display: true,
- },
- },
- };
-
- this.onColumnVisibilityChange = onColumnVisibilityChange.bind(this);
- function onColumnVisibilityChange(columns) {
- this.columnVisibility.columns = columns;
- DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
- }
-
- this.expandAll = function () {
- this.state.expandAll = !this.state.expandAll;
- for (var i = 0; i < this.state.filteredDataSet.length; i++) {
- var item = this.state.filteredDataSet[i];
- this.expandItem(item, this.state.expandAll);
- }
- };
-
- this.expandItem = function (item, expanded) {
- item.Expanded = expanded;
- if (item.Expanded) {
- if (this.state.expandedItems.indexOf(item.Id) === -1) {
- this.state.expandedItems.push(item.Id);
- }
- } else {
- var index = this.state.expandedItems.indexOf(item.Id);
- if (index > -1) {
- this.state.expandedItems.splice(index, 1);
- }
- }
- DatatableService.setDataTableExpandedItems(this.tableKey, this.state.expandedItems);
- };
-
- function expandPreviouslyExpandedItem(item, storedExpandedItems) {
- var expandedItem = _.find(storedExpandedItems, function (storedId) {
- return item.Id === storedId;
- });
-
- if (expandedItem) {
- ctrl.expandItem(item, true);
- }
- }
-
- this.expandItems = function (storedExpandedItems) {
- var expandedItemCount = 0;
- this.state.expandedItems = storedExpandedItems;
-
- for (var i = 0; i < this.dataset.length; i++) {
- var item = this.dataset[i];
- expandPreviouslyExpandedItem(item, storedExpandedItems);
- if (item.Expanded) {
- ++expandedItemCount;
- }
- }
-
- if (expandedItemCount === this.dataset.length) {
- this.state.expandAll = true;
- }
- };
-
- this.onDataRefresh = function () {
- var storedExpandedItems = DatatableService.getDataTableExpandedItems(this.tableKey);
- if (storedExpandedItems !== null) {
- this.expandItems(storedExpandedItems);
- }
- };
-
- this.$onInit = function () {
- this.setDefaults();
- this.prepareTableFromDataset();
-
- this.state.orderBy = this.orderBy;
- var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
- if (storedOrder !== null) {
- this.state.reverseOrder = storedOrder.reverse;
- this.state.orderBy = storedOrder.orderBy;
- }
-
- var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
- if (textFilter !== null) {
- this.state.textFilter = textFilter;
- this.onTextFilterChange();
- }
-
- var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
- if (storedFilters !== null) {
- this.filters = storedFilters;
- }
- if (this.filters && this.filters.state) {
- this.filters.state.open = false;
- }
-
- var storedExpandedItems = DatatableService.getDataTableExpandedItems(this.tableKey);
- if (storedExpandedItems !== null) {
- this.expandItems(storedExpandedItems);
- }
-
- var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
- if (storedSettings !== null) {
- this.settings = storedSettings;
- this.settings.open = false;
- }
- this.onSettingsRepeaterChange();
- var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
- if (storedColumnVisibility !== null) {
- this.columnVisibility = storedColumnVisibility;
- }
- };
- },
-]);
diff --git a/app/docker/filters/filters.js b/app/docker/filters/filters.js
index 9839394d9..e956dc9ff 100644
--- a/app/docker/filters/filters.js
+++ b/app/docker/filters/filters.js
@@ -1,5 +1,5 @@
import _ from 'lodash-es';
-import { joinCommand, taskStatusBadge, nodeStatusBadge, trimSHA, dockerNodeAvailabilityBadge } from './utils';
+import { hideShaSum, joinCommand, nodeStatusBadge, taskStatusBadge, trimSHA } from './utils';
function includeString(text, values) {
return values.some(function (val) {
@@ -76,7 +76,6 @@ angular
};
})
.filter('nodestatusbadge', () => nodeStatusBadge)
- .filter('dockerNodeAvailabilityBadge', () => dockerNodeAvailabilityBadge)
.filter('trimcontainername', function () {
'use strict';
return function (name) {
@@ -159,44 +158,7 @@ angular
.filter('command', function () {
return joinCommand;
})
- .filter('hideshasum', function () {
- 'use strict';
- return function (imageName) {
- if (imageName) {
- return imageName.split('@sha')[0];
- }
- return '';
- };
- })
- .filter('availablenodecount', [
- 'ConstraintsHelper',
- function (ConstraintsHelper) {
- 'use strict';
- return function (nodes, service) {
- var availableNodes = 0;
- for (var i = 0; i < nodes.length; i++) {
- var node = nodes[i];
- if (node.Availability === 'active' && node.Status === 'ready' && ConstraintsHelper.matchesServiceConstraints(service, node)) {
- availableNodes++;
- }
- }
- return availableNodes;
- };
- },
- ])
- .filter('runningtaskscount', function () {
- 'use strict';
- return function (tasks) {
- var runningTasks = 0;
- for (var i = 0; i < tasks.length; i++) {
- var task = tasks[i];
- if (task.Status.State === 'running' && task.DesiredState === 'running') {
- runningTasks++;
- }
- }
- return runningTasks;
- };
- })
+ .filter('hideshasum', () => hideShaSum)
.filter('tasknodename', function () {
'use strict';
return function (nodeId, nodes) {
diff --git a/app/docker/filters/utils.ts b/app/docker/filters/utils.ts
index 05723b239..8c3e633de 100644
--- a/app/docker/filters/utils.ts
+++ b/app/docker/filters/utils.ts
@@ -1,4 +1,4 @@
-import { NodeStatus, TaskState, NodeSpec } from 'docker-types/generated/1.41';
+import { NodeStatus, TaskState } from 'docker-types/generated/1.41';
import _ from 'lodash';
export function trimSHA(imageName: string) {
@@ -62,14 +62,6 @@ export function nodeStatusBadge(text: NodeStatus['State']) {
return 'success';
}
-export function dockerNodeAvailabilityBadge(text: NodeSpec['Availability']) {
- if (text === 'pause') {
- return 'warning';
- }
-
- if (text === 'drain') {
- return 'danger';
- }
-
- return 'success';
+export function hideShaSum(imageName = '') {
+ return imageName.split('@sha')[0];
}
diff --git a/app/docker/helpers/constraintsHelper.js b/app/docker/helpers/constraintsHelper.js
deleted file mode 100644
index c2288f71b..000000000
--- a/app/docker/helpers/constraintsHelper.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import _ from 'lodash-es';
-
-function ConstraintModel(op, key, value) {
- this.op = op;
- this.value = value;
- this.key = key;
-}
-
-var patterns = {
- id: {
- nodeId: 'node.id',
- nodeHostname: 'node.hostname',
- nodeRole: 'node.role',
- nodeLabels: 'node.labels.',
- engineLabels: 'engine.labels.',
- },
- op: {
- eq: '==',
- neq: '!=',
- },
-};
-
-function matchesConstraint(value, constraint) {
- if (!constraint || (constraint.op === patterns.op.eq && value === constraint.value) || (constraint.op === patterns.op.neq && value !== constraint.value)) {
- return true;
- }
- return false;
-}
-
-function matchesLabel(labels, constraint) {
- if (!constraint) {
- return true;
- }
- var found = _.find(labels, function (label) {
- return label.key === constraint.key && label.value === constraint.value;
- });
- return found !== undefined;
-}
-
-function extractValue(constraint, op) {
- return constraint.split(op).pop().trim();
-}
-
-function extractCustomLabelKey(constraint, op, baseLabelKey) {
- return constraint.split(op).shift().trim().replace(baseLabelKey, '');
-}
-
-angular.module('portainer.docker').factory('ConstraintsHelper', [
- function ConstraintsHelperFactory() {
- 'use strict';
- return {
- transformConstraints: function (constraints) {
- var transform = {};
- for (var i = 0; i < constraints.length; i++) {
- var constraint = constraints[i];
-
- var op;
- if (constraint.includes(patterns.op.eq)) {
- op = patterns.op.eq;
- } else if (constraint.includes(patterns.op.neq)) {
- op = patterns.op.neq;
- }
-
- var value = extractValue(constraint, op);
- var key = '';
- switch (true) {
- case constraint.includes(patterns.id.nodeId):
- transform.nodeId = new ConstraintModel(op, key, value);
- break;
- case constraint.includes(patterns.id.nodeHostname):
- transform.nodeHostname = new ConstraintModel(op, key, value);
- break;
- case constraint.includes(patterns.id.nodeRole):
- transform.nodeRole = new ConstraintModel(op, key, value);
- break;
- case constraint.includes(patterns.id.nodeLabels):
- key = extractCustomLabelKey(constraint, op, patterns.id.nodeLabels);
- transform.nodeLabels = new ConstraintModel(op, key, value);
- break;
- case constraint.includes(patterns.id.engineLabels):
- key = extractCustomLabelKey(constraint, op, patterns.id.engineLabels);
- transform.engineLabels = new ConstraintModel(op, key, value);
- break;
- default:
- break;
- }
- }
- return transform;
- },
- matchesServiceConstraints: function (service, node) {
- if (service.Constraints === undefined || service.Constraints.length === 0) {
- return true;
- }
- var constraints = this.transformConstraints(angular.copy(service.Constraints));
- if (
- matchesConstraint(node.Id, constraints.nodeId) &&
- matchesConstraint(node.Hostname, constraints.nodeHostname) &&
- matchesConstraint(node.Role, constraints.nodeRole) &&
- matchesLabel(node.Labels, constraints.nodeLabels) &&
- matchesLabel(node.EngineLabels, constraints.engineLabels)
- ) {
- return true;
- }
- return false;
- },
- };
- },
-]);
diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts
index 759b4e635..cfcdbbcb3 100644
--- a/app/docker/react/components/index.ts
+++ b/app/docker/react/components/index.ts
@@ -20,7 +20,6 @@ import { ConfigsDatatable } from '@/react/docker/configs/ListView/ConfigsDatatab
import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowser';
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
import { ProcessesDatatable } from '@/react/docker/containers/StatsView/ProcessesDatatable';
-import { ScaleServiceButton } from '@/react/docker/services/ListView/ServicesDatatable/columns/schedulingMode/ScaleServiceButton';
import { SecretsDatatable } from '@/react/docker/secrets/ListView/SecretsDatatable';
import { StacksDatatable } from '@/react/docker/stacks/ListView/StacksDatatable';
@@ -124,10 +123,6 @@ const ngModule = angular
'dockerContainerProcessesDatatable',
r2a(ProcessesDatatable, ['dataset', 'headers'])
)
- .component(
- 'dockerServicesDatatableScaleServiceButton',
- r2a(withUIRouter(withCurrentUser(ScaleServiceButton)), ['service'])
- )
.component('dockerEventsDatatable', r2a(EventsDatatable, ['dataset']))
.component(
'dockerSecretsDatatable',
diff --git a/app/docker/react/components/services.ts b/app/docker/react/components/services.ts
index ec4634823..046095038 100644
--- a/app/docker/react/components/services.ts
+++ b/app/docker/react/components/services.ts
@@ -5,6 +5,7 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
import { TasksDatatable } from '@/react/docker/services/ListView/ServicesDatatable/TasksDatatable';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { TaskTableQuickActions } from '@/react/docker/services/common/TaskTableQuickActions';
+import { ServicesDatatable } from '@/react/docker/services/ListView/ServicesDatatable';
export const servicesModule = angular
.module('portainer.docker.react.components.services', [])
@@ -18,4 +19,14 @@ export const servicesModule = angular
'state',
'taskId',
])
+ )
+ .component(
+ 'dockerServicesDatatable',
+ r2a(withUIRouter(withCurrentUser(ServicesDatatable)), [
+ 'dataset',
+ 'isAddActionVisible',
+ 'isStackColumnVisible',
+ 'onRefresh',
+ 'titleIcon',
+ ])
).name;
diff --git a/app/docker/views/services/services.html b/app/docker/views/services/services.html
index 0c2403f0d..fc1dcb9fd 100644
--- a/app/docker/views/services/services.html
+++ b/app/docker/views/services/services.html
@@ -1,22 +1,9 @@
-
+
diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html
index d278e43d4..a69b07abd 100644
--- a/app/portainer/views/stacks/edit/stack.html
+++ b/app/portainer/views/stacks/edit/stack.html
@@ -239,27 +239,7 @@
environment="endpoint"
>
-
+
)}
- initialTableState={{
- columnVisibility: Object.fromEntries(
- tableState.hiddenColumns.map((col) => [col, false])
- ),
- }}
- renderTableSettings={(tableInstance) => {
- const columnsToHide = tableInstance
- .getAllColumns()
- .filter((col) => col.getCanHide());
-
- return (
- <>
-
- columns={columnsToHide}
- onChange={(hiddenColumns) => {
- tableState.setHiddenColumns(hiddenColumns);
- tableInstance.setColumnVisibility(
- Object.fromEntries(
- hiddenColumns.map((col) => [col, false])
- )
- );
- }}
- value={tableState.hiddenColumns}
- />
- }
- >
-
-
- >
- );
- }}
+ initialTableState={getColumnVisibilityState(tableState.hiddenColumns)}
+ renderTableSettings={(tableInstance) => (
+ <>
+
+ table={tableInstance}
+ onChange={(hiddenColumns) => {
+ tableState.setHiddenColumns(hiddenColumns);
+ }}
+ value={tableState.hiddenColumns}
+ />
+ }
+ >
+
+
+ >
+ )}
dataset={containersQuery.data || []}
isLoading={containersQuery.isLoading}
emptyContentLabel="No containers found"
diff --git a/app/react/components/buttons/ButtonGroup.tsx b/app/react/components/buttons/ButtonGroup.tsx
index 9b459b0e3..eb197f745 100644
--- a/app/react/components/buttons/ButtonGroup.tsx
+++ b/app/react/components/buttons/ButtonGroup.tsx
@@ -9,7 +9,7 @@ export interface Props {
}
export function ButtonGroup({
- size = 'small',
+ size,
children,
className,
'aria-label': ariaLabel,
diff --git a/app/react/components/datatables/ColumnVisibilityMenu.tsx b/app/react/components/datatables/ColumnVisibilityMenu.tsx
index ca362cdfa..cb4daaa5c 100644
--- a/app/react/components/datatables/ColumnVisibilityMenu.tsx
+++ b/app/react/components/datatables/ColumnVisibilityMenu.tsx
@@ -2,21 +2,26 @@ import _ from 'lodash';
import clsx from 'clsx';
import { Menu, MenuButton, MenuList } from '@reach/menu-button';
import { Columns } from 'lucide-react';
-import { Column } from '@tanstack/react-table';
+import { Table } from '@tanstack/react-table';
import { Checkbox } from '@@/form-components/Checkbox';
interface Props {
- columns: Column[];
onChange: (value: string[]) => void;
value: string[];
+ table: Table;
}
export function ColumnVisibilityMenu({
- columns,
onChange,
value,
+ table,
}: Props) {
+ const columnsToHide = table.getAllColumns().filter((col) => col.getCanHide());
+ if (!columnsToHide.length) {
+ return null;
+ }
+
return (