diff --git a/app/kubernetes/custom-templates/kube-custom-templates-view/kube-custom-templates-view.controller.js b/app/kubernetes/custom-templates/kube-custom-templates-view/kube-custom-templates-view.controller.js
index 74d345b4f..23f4b546a 100644
--- a/app/kubernetes/custom-templates/kube-custom-templates-view/kube-custom-templates-view.controller.js
+++ b/app/kubernetes/custom-templates/kube-custom-templates-view/kube-custom-templates-view.controller.js
@@ -22,6 +22,11 @@ export default class KubeCustomTemplatesViewController {
this.validateForm = this.validateForm.bind(this);
this.confirmDelete = this.confirmDelete.bind(this);
this.selectTemplate = this.selectTemplate.bind(this);
+ this.isSelected = this.isSelected.bind(this);
+ }
+
+ isSelected(templateId) {
+ return this.state.selectedTemplate && this.state.selectedTemplate.Id === templateId;
}
selectTemplate(template) {
diff --git a/app/kubernetes/custom-templates/kube-custom-templates-view/kube-custom-templates-view.html b/app/kubernetes/custom-templates/kube-custom-templates-view/kube-custom-templates-view.html
index c771dd4db..bc35c5194 100644
--- a/app/kubernetes/custom-templates/kube-custom-templates-view/kube-custom-templates-view.html
+++ b/app/kubernetes/custom-templates/kube-custom-templates-view/kube-custom-templates-view.html
@@ -13,6 +13,7 @@
on-delete-click="($ctrl.confirmDelete)"
create-path="kubernetes.templates.custom.new"
edit-path="kubernetes.templates.custom.edit"
+ is-selected="($ctrl.isSelected)"
>
diff --git a/app/portainer/components/custom-templates-list/customTemplatesList.html b/app/portainer/components/custom-templates-list/customTemplatesList.html
index 96bd2ea82..97fd126f2 100644
--- a/app/portainer/components/custom-templates-list/customTemplatesList.html
+++ b/app/portainer/components/custom-templates-list/customTemplatesList.html
@@ -29,25 +29,15 @@
-
-
-
-
-
+
Loading...
1 ? hostAndContainerPort[0] : undefined,
- containerPort: hostAndContainerPort.length > 1 ? hostAndContainerPort[1] : hostAndContainerPort[0],
- protocol: portAndProtocol[1],
- };
- });
- }
-
- return ports;
-}
-
-function templateVolumes(data) {
- var volumes = [];
-
- if (data.volumes) {
- volumes = data.volumes.map(function (v) {
- return {
- container: v.container,
- readonly: v.readonly || false,
- type: v.bind ? 'bind' : 'auto',
- bind: v.bind ? v.bind : null,
- };
- });
- }
-
- return volumes;
-}
-
-function templateEnv(data) {
- var env = [];
-
- if (data.env) {
- env = data.env.map(function (envvar) {
- envvar.type = 2;
- envvar.value = envvar.default ? envvar.default : '';
-
- if (envvar.preset) {
- envvar.type = 1;
- }
-
- if (envvar.select) {
- envvar.type = 3;
- for (var i = 0; i < envvar.select.length; i++) {
- var allowedValue = envvar.select[i];
- if (allowedValue.default) {
- envvar.value = allowedValue.value;
- break;
- }
- }
- }
- return envvar;
- });
- }
-
- return env;
-}
diff --git a/app/portainer/react/components/custom-templates/index.ts b/app/portainer/react/components/custom-templates/index.ts
index 6a682b881..18c1cdfcb 100644
--- a/app/portainer/react/components/custom-templates/index.ts
+++ b/app/portainer/react/components/custom-templates/index.ts
@@ -4,6 +4,10 @@ import { r2a } from '@/react-tools/react2angular';
import { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
import { CustomTemplatesVariablesField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
import { withControlledInput } from '@/react-tools/withControlledInput';
+import { CustomTemplatesListItem } from '@/react/portainer/templates/custom-templates/ListView/CustomTemplatesListItem';
+import { withCurrentUser } from '@/react-tools/withCurrentUser';
+import { withUIRouter } from '@/react-tools/withUIRouter';
+import { AppTemplatesListItem } from '@/react/portainer/templates/app-templates/AppTemplatesListItem';
import { VariablesFieldAngular } from './variables-field';
@@ -26,4 +30,22 @@ export const customTemplatesModule = angular
'errors',
'isVariablesNamesFromParent',
])
+ )
+ .component(
+ 'customTemplatesListItem',
+ r2a(withUIRouter(withCurrentUser(CustomTemplatesListItem)), [
+ 'onDelete',
+ 'onSelect',
+ 'template',
+ 'isSelected',
+ ])
+ )
+ .component(
+ 'appTemplatesListItem',
+ r2a(withUIRouter(withCurrentUser(AppTemplatesListItem)), [
+ 'onSelect',
+ 'template',
+ 'isSelected',
+ 'onDuplicate',
+ ])
).name;
diff --git a/app/portainer/services/api/templateService.js b/app/portainer/services/api/templateService.js
index 52f0f8cf6..7259305f7 100644
--- a/app/portainer/services/api/templateService.js
+++ b/app/portainer/services/api/templateService.js
@@ -1,6 +1,6 @@
import { commandStringToArray } from '@/docker/helpers/containers';
+import { TemplateViewModel } from '@/react/portainer/templates/app-templates/template';
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
-import { TemplateViewModel } from '../../models/template';
angular.module('portainer.app').factory('TemplateService', TemplateServiceFactory);
diff --git a/app/portainer/views/custom-templates/custom-templates-view/customTemplatesView.html b/app/portainer/views/custom-templates/custom-templates-view/customTemplatesView.html
index d7882eb5d..cf7108a0a 100644
--- a/app/portainer/views/custom-templates/custom-templates-view/customTemplatesView.html
+++ b/app/portainer/views/custom-templates/custom-templates-view/customTemplatesView.html
@@ -75,6 +75,7 @@
is-edit-allowed="$ctrl.isEditAllowed"
on-select-click="($ctrl.selectTemplate)"
on-delete-click="($ctrl.confirmDelete)"
+ is-selected="($ctrl.isSelected)"
>
diff --git a/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js b/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js
index 4aeac7ac9..52b0d65a5 100644
--- a/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js
+++ b/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js
@@ -82,6 +82,11 @@ class CustomTemplatesViewController {
this.isEditAllowed = this.isEditAllowed.bind(this);
this.onChangeFormValues = this.onChangeFormValues.bind(this);
this.onChangeTemplateVariables = this.onChangeTemplateVariables.bind(this);
+ this.isSelected = this.isSelected.bind(this);
+ }
+
+ isSelected(templateId) {
+ return this.state.selectedTemplate && this.state.selectedTemplate.Id === templateId;
}
isEditAllowed(template) {
@@ -177,12 +182,11 @@ class CustomTemplatesViewController {
}
}
- unselectTemplate(template) {
+ unselectTemplate() {
// wrapping unselect with async to make a digest cycle run between unselect to select
- return this.$async(this.unselectTemplateAsync, template);
+ return this.$async(this.unselectTemplateAsync);
}
- async unselectTemplateAsync(template) {
- template.Selected = false;
+ async unselectTemplateAsync() {
this.state.selectedTemplate = null;
this.formValues = {
@@ -194,15 +198,15 @@ class CustomTemplatesViewController {
};
}
- selectTemplate(template) {
- return this.$async(this.selectTemplateAsync, template);
+ selectTemplate(templateId) {
+ return this.$async(this.selectTemplateAsync, templateId);
}
- async selectTemplateAsync(template) {
+ async selectTemplateAsync(templateId) {
if (this.state.selectedTemplate) {
await this.unselectTemplate(this.state.selectedTemplate);
}
- template.Selected = true;
+ const template = _.find(this.templates, { Id: templateId });
try {
this.state.templateContent = this.formValues.fileContent = await this.CustomTemplateService.customTemplateFile(template.Id, template.GitConfig !== null);
diff --git a/app/portainer/views/templates/templates.html b/app/portainer/views/templates/templates.html
index 5fc6c9ccf..a32a5f655 100644
--- a/app/portainer/views/templates/templates.html
+++ b/app/portainer/views/templates/templates.html
@@ -279,6 +279,7 @@
templates="templates"
table-key="templates"
select-action="selectTemplate"
+ is-selected="isSelected"
show-swarm-stacks="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER' && applicationState.endpoint.apiVersion >= 1.25"
>
diff --git a/app/portainer/views/templates/templatesController.js b/app/portainer/views/templates/templatesController.js
index a19124a7e..6edde966c 100644
--- a/app/portainer/views/templates/templatesController.js
+++ b/app/portainer/views/templates/templatesController.js
@@ -18,6 +18,7 @@ angular.module('portainer.app').controller('TemplatesController', [
'FormValidator',
'StackService',
'endpoint',
+ '$async',
function (
$scope,
$q,
@@ -34,7 +35,8 @@ angular.module('portainer.app').controller('TemplatesController', [
Authentication,
FormValidator,
StackService,
- endpoint
+ endpoint,
+ $async
) {
const DOCKER_STANDALONE = 'DOCKER_STANDALONE';
const DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE';
@@ -222,31 +224,37 @@ angular.module('portainer.app').controller('TemplatesController', [
}
};
- $scope.unselectTemplate = function (template) {
- template.Selected = false;
- $scope.state.selectedTemplate = null;
+ $scope.isSelected = function (template) {
+ return $scope.state.selectedTemplate && $scope.state.selectedTemplate.Id === template.Id;
+ };
+
+ $scope.unselectTemplate = function () {
+ return $async(async () => {
+ $scope.state.selectedTemplate = null;
+ });
};
$scope.selectTemplate = function (template) {
- if ($scope.state.selectedTemplate) {
- $scope.unselectTemplate($scope.state.selectedTemplate);
- }
+ return $async(async () => {
+ if ($scope.state.selectedTemplate) {
+ $scope.unselectTemplate($scope.state.selectedTemplate);
+ }
- template.Selected = true;
- if (template.Network) {
- $scope.formValues.network = _.find($scope.availableNetworks, function (o) {
- return o.Name === template.Network;
- });
- } else {
- $scope.formValues.network = _.find($scope.availableNetworks, function (o) {
- return o.Name === 'bridge';
- });
- }
+ if (template.Network) {
+ $scope.formValues.network = _.find($scope.availableNetworks, function (o) {
+ return o.Name === template.Network;
+ });
+ } else {
+ $scope.formValues.network = _.find($scope.availableNetworks, function (o) {
+ return o.Name === 'bridge';
+ });
+ }
- $scope.formValues.name = template.Name ? template.Name : '';
- $scope.state.selectedTemplate = template;
- $scope.state.deployable = isDeployable($scope.applicationState.endpoint, template.Type);
- $anchorScroll('view-top');
+ $scope.formValues.name = template.Name ? template.Name : '';
+ $scope.state.selectedTemplate = template;
+ $scope.state.deployable = isDeployable($scope.applicationState.endpoint, template.Type);
+ $anchorScroll('view-top');
+ });
};
function isDeployable(endpoint, templateType) {
diff --git a/app/react/components/Blocklist/BlocklistItem.tsx b/app/react/components/Blocklist/BlocklistItem.tsx
new file mode 100644
index 000000000..d3f2a4e26
--- /dev/null
+++ b/app/react/components/Blocklist/BlocklistItem.tsx
@@ -0,0 +1,37 @@
+import clsx from 'clsx';
+import { ComponentProps, ComponentType, ElementType } from 'react';
+
+export type AsComponentProps