From 14129632a3251af0429862fb8bfb4ec6c183bb9f Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 23 Oct 2023 19:04:18 +0300 Subject: [PATCH] refactor(app-templates): convert list to react [EE-6205] (#10439) --- app/docker/react/components/index.ts | 4 - .../template-list/template-list-controller.js | 149 ------------------ .../components/template-list/template-list.js | 13 -- .../template-list/templateList.html | 76 --------- .../components/custom-templates/index.ts | 21 +-- app/portainer/services/api/templateService.js | 2 +- app/portainer/views/templates/templates.html | 21 +-- .../views/templates/templatesController.js | 6 +- .../TemplateListDropdown.tsx | 32 ---- .../TemplateListDropdown/index.ts | 11 -- .../app-templates/TemplateListSort/index.ts | 14 -- .../app-templates/AppTemplatesList.tsx | 110 +++++++++++++ .../app-templates/AppTemplatesListItem.tsx | 2 +- .../templates/app-templates/Filters.tsx | 69 ++++++++ .../TemplateListSort.module.css | 0 .../TemplateListSort/TemplateListSort.tsx | 35 ++-- .../app-templates/TemplateListSort/index.ts | 1 + .../templates/app-templates/types.ts | 11 +- .../useFetchTemplateInfoMutation.ts | 52 ++++++ .../app-templates/useSortAndFilter.tsx | 111 +++++++++++++ .../{template.ts => view-model.ts} | 0 webpack/webpack.common.js | 4 + 22 files changed, 393 insertions(+), 351 deletions(-) delete mode 100644 app/portainer/components/template-list/template-list-controller.js delete mode 100644 app/portainer/components/template-list/template-list.js delete mode 100644 app/portainer/components/template-list/templateList.html delete mode 100644 app/react/docker/app-templates/TemplateListDropdown/TemplateListDropdown.tsx delete mode 100644 app/react/docker/app-templates/TemplateListDropdown/index.ts delete mode 100644 app/react/docker/app-templates/TemplateListSort/index.ts create mode 100644 app/react/portainer/templates/app-templates/AppTemplatesList.tsx create mode 100644 app/react/portainer/templates/app-templates/Filters.tsx rename app/react/{docker => portainer/templates}/app-templates/TemplateListSort/TemplateListSort.module.css (100%) rename app/react/{docker => portainer/templates}/app-templates/TemplateListSort/TemplateListSort.tsx (52%) create mode 100644 app/react/portainer/templates/app-templates/TemplateListSort/index.ts create mode 100644 app/react/portainer/templates/app-templates/useFetchTemplateInfoMutation.ts create mode 100644 app/react/portainer/templates/app-templates/useSortAndFilter.tsx rename app/react/portainer/templates/app-templates/{template.ts => view-model.ts} (100%) diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts index 34e2a4d25..def6b6761 100644 --- a/app/docker/react/components/index.ts +++ b/app/docker/react/components/index.ts @@ -3,8 +3,6 @@ import angular from 'angular'; import { r2a } from '@/react-tools/react2angular'; import { withControlledInput } from '@/react-tools/withControlledInput'; import { StackContainersDatatable } from '@/react/common/stacks/ItemView/StackContainersDatatable'; -import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown'; -import { TemplateListSortAngular } from '@/react/docker/app-templates/TemplateListSort'; import { withCurrentUser } from '@/react-tools/withCurrentUser'; import { withReactQuery } from '@/react-tools/withReactQuery'; import { withUIRouter } from '@/react-tools/withUIRouter'; @@ -39,8 +37,6 @@ const ngModule = angular ]) .component('dockerfileDetails', r2a(DockerfileDetails, ['image'])) .component('dockerHealthStatus', r2a(HealthStatus, ['health'])) - .component('templateListDropdown', TemplateListDropdownAngular) - .component('templateListSort', TemplateListSortAngular) .component( 'stackContainersDatatable', r2a( diff --git a/app/portainer/components/template-list/template-list-controller.js b/app/portainer/components/template-list/template-list-controller.js deleted file mode 100644 index 7185250b3..000000000 --- a/app/portainer/components/template-list/template-list-controller.js +++ /dev/null @@ -1,149 +0,0 @@ -import _ from 'lodash-es'; - -angular.module('portainer.app').controller('TemplateListController', TemplateListController); - -function TemplateListController($scope, $async, $state, DatatableService, Notifications, TemplateService) { - var ctrl = this; - - this.state = { - textFilter: '', - selectedCategory: null, - categories: [], - typeFilters: [], - filterByType: null, - showContainerTemplates: true, - selectedOrderBy: null, - orderByFields: [], - orderDesc: false, - }; - - this.onTextFilterChange = function () { - DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter); - }; - - ctrl.filterByTemplateType = function (item) { - switch (item.Type) { - case 1: // container - return ctrl.state.showContainerTemplates; - case 2: // swarm stack - return ctrl.showSwarmStacks && !ctrl.state.showContainerTemplates; - case 3: // docker compose - return !ctrl.state.showContainerTemplates || null === ctrl.state.filterByType; - case 4: // Edge stack templates - return false; - } - return false; - }; - - ctrl.updateCategories = function () { - var availableCategories = []; - - for (var i = 0; i < ctrl.templates.length; i++) { - var template = ctrl.templates[i]; - if (ctrl.filterByTemplateType(template)) { - availableCategories = availableCategories.concat(template.Categories); - } - } - - ctrl.state.categories = _.sortBy(_.uniq(availableCategories)); - }; - - ctrl.filterByCategory = function (item) { - if (!ctrl.state.selectedCategory) { - return true; - } - - return _.includes(item.Categories, ctrl.state.selectedCategory); - }; - - this.duplicateTemplate = duplicateTemplate.bind(this); - this.duplicateTemplateAsync = duplicateTemplateAsync.bind(this); - function duplicateTemplate(template) { - return $async(this.duplicateTemplateAsync, template); - } - - async function duplicateTemplateAsync(template) { - try { - const { FileContent: fileContent } = await TemplateService.templateFile(template.Repository.url, template.Repository.stackfile); - let type = 0; - if (template.Type === 2) { - type = 1; - } - if (template.Type === 3) { - type = 2; - } - $state.go('docker.templates.custom.new', { fileContent, type }); - } catch (err) { - Notifications.error('Failure', err, 'Failed to duplicate template'); - } - } - - ctrl.changeOrderBy = function (orderField) { - $scope.$evalAsync(() => { - if (null === orderField) { - ctrl.state.selectedOrderBy = null; - ctrl.templates = ctrl.initalTemplates; - } - - ctrl.state.selectedOrderBy = orderField; - ctrl.templates = _.orderBy(ctrl.templates, [getSorter(ctrl.state.selectedOrderBy)], [ctrl.state.orderDesc ? 'desc' : 'asc']); - }); - }; - - ctrl.applyTypeFilter = function (type) { - $scope.$evalAsync(() => { - ctrl.state.filterByType = type; - ctrl.state.showContainerTemplates = 'Container' === type || null === type; - ctrl.updateCategories(); - }); - }; - - ctrl.invertOrder = function () { - $scope.$evalAsync(() => { - ctrl.state.orderDesc = !ctrl.state.orderDesc; - ctrl.templates = _.orderBy(ctrl.templates, [getSorter(ctrl.state.selectedOrderBy)], [ctrl.state.orderDesc ? 'desc' : 'asc']); - }); - }; - - ctrl.applyCategoriesFilter = function (category) { - $scope.$evalAsync(() => { - ctrl.state.selectedCategory = category; - ctrl.updateCategories(); - }); - }; - - this.$onInit = function () { - if (this.showSwarmStacks) { - this.state.showContainerTemplates = false; - } - this.updateCategories(); - - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; - } - - this.initalTemplates = this.templates; - this.state.orderByFields = ['Title', 'Categories', 'Description']; - this.state.typeFilters = ['Container', 'Stack']; - }; - - function categorySorter(template) { - if (template.Categories && template.Categories.length > 0 && template.Categories[0] && template.Categories[0].length > 0) { - return template.Categories[0].toLowerCase(); - } - } - - function getSorter(orderBy) { - let sorter; - switch (orderBy) { - case 'Categories': - sorter = categorySorter; - break; - default: - sorter = orderBy; - } - - return sorter; - } -} diff --git a/app/portainer/components/template-list/template-list.js b/app/portainer/components/template-list/template-list.js deleted file mode 100644 index ec61e2a01..000000000 --- a/app/portainer/components/template-list/template-list.js +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('portainer.app').component('templateList', { - templateUrl: './templateList.html', - controller: 'TemplateListController', - bindings: { - titleText: '@', - titleIcon: '@', - templates: '<', - tableKey: '@', - selectAction: '<', - showSwarmStacks: '<', - isSelected: '<', - }, -}); diff --git a/app/portainer/components/template-list/templateList.html b/app/portainer/components/template-list/templateList.html deleted file mode 100644 index 68c8ef110..000000000 --- a/app/portainer/components/template-list/templateList.html +++ /dev/null @@ -1,76 +0,0 @@ -
- -
-
-
- -
- {{ $ctrl.titleText }} -
- -
- -
-
- -
-
-
- -
-
- -
-
- -
-
-
-
-
- - -
-
Loading...
-
- No templates available. -
-
-
-
-
diff --git a/app/portainer/react/components/custom-templates/index.ts b/app/portainer/react/components/custom-templates/index.ts index 488bf752d..ca7aca80f 100644 --- a/app/portainer/react/components/custom-templates/index.ts +++ b/app/portainer/react/components/custom-templates/index.ts @@ -7,7 +7,6 @@ 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 { CommonFields, validation as commonFieldsValidation, @@ -15,6 +14,7 @@ import { import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector'; import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector'; import { withFormValidation } from '@/react-tools/withFormValidation'; +import { AppTemplatesList } from '@/react/portainer/templates/app-templates/AppTemplatesList'; import { VariablesFieldAngular } from './variables-field'; @@ -47,15 +47,7 @@ export const ngModule = angular 'isSelected', ]) ) - .component( - 'appTemplatesListItem', - r2a(withUIRouter(withCurrentUser(AppTemplatesListItem)), [ - 'onSelect', - 'template', - 'isSelected', - 'onDuplicate', - ]) - ) + .component( 'customTemplatesPlatformSelector', r2a(PlatformField, ['onChange', 'value']) @@ -63,6 +55,15 @@ export const ngModule = angular .component( 'customTemplatesTypeSelector', r2a(TemplateTypeSelector, ['onChange', 'value']) + ) + .component( + 'appTemplatesList', + r2a(withUIRouter(withCurrentUser(AppTemplatesList)), [ + 'onSelect', + 'templates', + 'selectedId', + 'showSwarmStacks', + ]) ); withFormValidation( diff --git a/app/portainer/services/api/templateService.js b/app/portainer/services/api/templateService.js index 7259305f7..ec96917ca 100644 --- a/app/portainer/services/api/templateService.js +++ b/app/portainer/services/api/templateService.js @@ -1,5 +1,5 @@ import { commandStringToArray } from '@/docker/helpers/containers'; -import { TemplateViewModel } from '@/react/portainer/templates/app-templates/template'; +import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model'; import { DockerHubViewModel } from 'Portainer/models/dockerhub'; angular.module('portainer.app').factory('TemplateService', TemplateServiceFactory); diff --git a/app/portainer/views/templates/templates.html b/app/portainer/views/templates/templates.html index a32a5f655..69ea724d8 100644 --- a/app/portainer/views/templates/templates.html +++ b/app/portainer/views/templates/templates.html @@ -270,18 +270,9 @@ -
-
- - -
-
+ diff --git a/app/portainer/views/templates/templatesController.js b/app/portainer/views/templates/templatesController.js index 6edde966c..5e86e34d7 100644 --- a/app/portainer/views/templates/templatesController.js +++ b/app/portainer/views/templates/templatesController.js @@ -224,10 +224,6 @@ angular.module('portainer.app').controller('TemplatesController', [ } }; - $scope.isSelected = function (template) { - return $scope.state.selectedTemplate && $scope.state.selectedTemplate.Id === template.Id; - }; - $scope.unselectTemplate = function () { return $async(async () => { $scope.state.selectedTemplate = null; @@ -237,7 +233,7 @@ angular.module('portainer.app').controller('TemplatesController', [ $scope.selectTemplate = function (template) { return $async(async () => { if ($scope.state.selectedTemplate) { - $scope.unselectTemplate($scope.state.selectedTemplate); + await $scope.unselectTemplate($scope.state.selectedTemplate); } if (template.Network) { diff --git a/app/react/docker/app-templates/TemplateListDropdown/TemplateListDropdown.tsx b/app/react/docker/app-templates/TemplateListDropdown/TemplateListDropdown.tsx deleted file mode 100644 index c48fab24e..000000000 --- a/app/react/docker/app-templates/TemplateListDropdown/TemplateListDropdown.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Select } from '@@/form-components/ReactSelect'; - -interface Filter { - label?: string; -} - -interface Props { - options: string[]; - onChange: (value: string | null) => void; - placeholder?: string; - value: string; -} - -export function TemplateListDropdown({ - options, - onChange, - placeholder, - value, -}: Props) { - const filterOptions: Filter[] = options.map((value) => ({ label: value })); - const filterValue: Filter | null = value ? { label: value } : null; - - return ( -