mirror of https://github.com/portainer/portainer
				
				
				
			refactor(custom-templates): migrate list component to react [EE-6206] (#10440)
							parent
							
								
									14129632a3
								
							
						
					
					
						commit
						10c3ed42f0
					
				| 
						 | 
				
			
			@ -22,11 +22,6 @@ 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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +65,7 @@ export default class KubeCustomTemplatesViewController {
 | 
			
		|||
        var template = _.find(this.templates, { Id: templateId });
 | 
			
		||||
        await this.CustomTemplateService.remove(templateId);
 | 
			
		||||
        this.Notifications.success('Template successfully deleted', template && template.Title);
 | 
			
		||||
        _.remove(this.templates, { Id: templateId });
 | 
			
		||||
        this.templates = this.templates.filter((template) => template.Id !== templateId);
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        this.Notifications.error('Failure', err, 'Failed to delete template');
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,8 @@
 | 
			
		|||
<page-header title="'Custom Templates'" breadcrumbs="['Custom Templates']" reload="true"></page-header>
 | 
			
		||||
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col-sm-12">
 | 
			
		||||
    <custom-templates-list
 | 
			
		||||
      ng-if="$ctrl.templates"
 | 
			
		||||
      title-text="Templates"
 | 
			
		||||
      title-icon="edit"
 | 
			
		||||
      templates="$ctrl.templates"
 | 
			
		||||
      table-key="customTemplates"
 | 
			
		||||
      is-edit-allowed="$ctrl.isEditAllowed"
 | 
			
		||||
      on-select-click="($ctrl.selectTemplate)"
 | 
			
		||||
      on-delete-click="($ctrl.confirmDelete)"
 | 
			
		||||
      create-path="kubernetes.templates.custom.new"
 | 
			
		||||
      edit-path="kubernetes.templates.custom.edit"
 | 
			
		||||
      is-selected="($ctrl.isSelected)"
 | 
			
		||||
    ></custom-templates-list>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
<custom-templates-list
 | 
			
		||||
  templates="$ctrl.templates"
 | 
			
		||||
  on-select="($ctrl.selectTemplate)"
 | 
			
		||||
  on-delete="($ctrl.confirmDelete)"
 | 
			
		||||
  selected-id="$ctrl.state.selectedTemplate.Id"
 | 
			
		||||
></custom-templates-list>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,46 +0,0 @@
 | 
			
		|||
<div class="datatable">
 | 
			
		||||
  <rd-widget>
 | 
			
		||||
    <rd-widget-body classes="no-padding">
 | 
			
		||||
      <div class="toolBar vertical-center flex-wrap !gap-x-5 !gap-y-1">
 | 
			
		||||
        <div class="toolBarTitle vertical-center">
 | 
			
		||||
          <div class="widget-icon space-right">
 | 
			
		||||
            <pr-icon icon="$ctrl.titleIcon"></pr-icon>
 | 
			
		||||
          </div>
 | 
			
		||||
          Custom Templates
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="searchBar vertical-center !mr-0">
 | 
			
		||||
          <pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
 | 
			
		||||
          <input
 | 
			
		||||
            type="text"
 | 
			
		||||
            class="searchInput"
 | 
			
		||||
            ng-model="$ctrl.state.textFilter"
 | 
			
		||||
            ng-change="$ctrl.onTextFilterChange()"
 | 
			
		||||
            placeholder="Search for a template..."
 | 
			
		||||
            auto-focus
 | 
			
		||||
            ng-model-options="{ debounce: 300 }"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="actionBar">
 | 
			
		||||
          <button type="button" class="btn btn-sm btn-primary" ui-state="$ctrl.createPath">
 | 
			
		||||
            <pr-icon icon="'plus'"></pr-icon>
 | 
			
		||||
            Add Custom Template
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="blocklist gap-y-2 !px-[20px] !pb-[20px]">
 | 
			
		||||
        <custom-templates-list-item
 | 
			
		||||
          ng-repeat="template in $ctrl.templates | filter:$ctrl.state.textFilter"
 | 
			
		||||
          template="template"
 | 
			
		||||
          on-select="($ctrl.onSelectClick)"
 | 
			
		||||
          on-delete="($ctrl.onDeleteClick)"
 | 
			
		||||
          is-selected="$ctrl.isSelected(template)"
 | 
			
		||||
        >
 | 
			
		||||
        </custom-templates-list-item>
 | 
			
		||||
 | 
			
		||||
        <div ng-if="!$ctrl.templates" class="text-muted text-center"> Loading... </div>
 | 
			
		||||
        <div ng-if="($ctrl.templates | filter: $ctrl.state.textFilter).length === 0" class="text-muted text-center"> No templates available. </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </rd-widget-body>
 | 
			
		||||
  </rd-widget>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,57 +0,0 @@
 | 
			
		|||
const CUSTOM_TEMPLATES_TYPES = {
 | 
			
		||||
  SWARM: 1,
 | 
			
		||||
  STANDALONE: 2,
 | 
			
		||||
  KUBERNETES: 3,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
angular.module('portainer.docker').controller('CustomTemplatesListController', function ($scope, $controller, DatatableService) {
 | 
			
		||||
  angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
 | 
			
		||||
 | 
			
		||||
  this.typeLabel = typeLabel;
 | 
			
		||||
  this.$onInit = $onInit;
 | 
			
		||||
 | 
			
		||||
  function typeLabel(type) {
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case CUSTOM_TEMPLATES_TYPES.SWARM:
 | 
			
		||||
        return 'swarm';
 | 
			
		||||
      case CUSTOM_TEMPLATES_TYPES.KUBERNETES:
 | 
			
		||||
        return 'manifest';
 | 
			
		||||
      case CUSTOM_TEMPLATES_TYPES.STANDALONE:
 | 
			
		||||
      default:
 | 
			
		||||
        return 'standalone';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function $onInit() {
 | 
			
		||||
    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 storedSettings = DatatableService.getDataTableSettings(this.tableKey);
 | 
			
		||||
    if (storedSettings !== null) {
 | 
			
		||||
      this.settings = storedSettings;
 | 
			
		||||
      this.settings.open = false;
 | 
			
		||||
    }
 | 
			
		||||
    this.onSettingsRepeaterChange();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,19 +0,0 @@
 | 
			
		|||
import angular from 'angular';
 | 
			
		||||
 | 
			
		||||
angular.module('portainer.app').component('customTemplatesList', {
 | 
			
		||||
  templateUrl: './customTemplatesList.html',
 | 
			
		||||
  controller: 'CustomTemplatesListController',
 | 
			
		||||
  bindings: {
 | 
			
		||||
    titleText: '@',
 | 
			
		||||
    titleIcon: '@',
 | 
			
		||||
    templates: '<',
 | 
			
		||||
    tableKey: '@',
 | 
			
		||||
    onSelectClick: '<',
 | 
			
		||||
    showSwarmStacks: '<',
 | 
			
		||||
    onDeleteClick: '<',
 | 
			
		||||
    isEditAllowed: '<',
 | 
			
		||||
    createPath: '@',
 | 
			
		||||
    editPath: '@',
 | 
			
		||||
    isSelected: '<',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -4,7 +4,6 @@ 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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +14,7 @@ import { PlatformField } from '@/react/portainer/custom-templates/components/Pla
 | 
			
		|||
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 { CustomTemplatesList } from '@/react/portainer/templates/custom-templates/ListView/CustomTemplatesList';
 | 
			
		||||
 | 
			
		||||
import { VariablesFieldAngular } from './variables-field';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -39,15 +39,14 @@ export const ngModule = angular
 | 
			
		|||
    ])
 | 
			
		||||
  )
 | 
			
		||||
  .component(
 | 
			
		||||
    'customTemplatesListItem',
 | 
			
		||||
    r2a(withUIRouter(withCurrentUser(CustomTemplatesListItem)), [
 | 
			
		||||
    'customTemplatesList',
 | 
			
		||||
    r2a(withUIRouter(withCurrentUser(CustomTemplatesList)), [
 | 
			
		||||
      'onDelete',
 | 
			
		||||
      'onSelect',
 | 
			
		||||
      'template',
 | 
			
		||||
      'isSelected',
 | 
			
		||||
      'templates',
 | 
			
		||||
      'selectedId',
 | 
			
		||||
    ])
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  .component(
 | 
			
		||||
    'customTemplatesPlatformSelector',
 | 
			
		||||
    r2a(PlatformField, ['onChange', 'value'])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,20 +62,10 @@
 | 
			
		|||
    </advanced-form>
 | 
			
		||||
  </stack-from-template-form>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col-sm-12">
 | 
			
		||||
    <custom-templates-list
 | 
			
		||||
      ng-if="$ctrl.templates"
 | 
			
		||||
      title-text="Templates"
 | 
			
		||||
      title-icon="edit"
 | 
			
		||||
      templates="$ctrl.templates"
 | 
			
		||||
      table-key="customTemplates"
 | 
			
		||||
      create-path="docker.templates.custom.new"
 | 
			
		||||
      edit-path="docker.templates.custom.edit"
 | 
			
		||||
      is-edit-allowed="$ctrl.isEditAllowed"
 | 
			
		||||
      on-select-click="($ctrl.selectTemplate)"
 | 
			
		||||
      on-delete-click="($ctrl.confirmDelete)"
 | 
			
		||||
      is-selected="($ctrl.isSelected)"
 | 
			
		||||
    ></custom-templates-list>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<custom-templates-list
 | 
			
		||||
  templates="$ctrl.templates"
 | 
			
		||||
  on-select="($ctrl.selectTemplate)"
 | 
			
		||||
  on-delete="($ctrl.confirmDelete)"
 | 
			
		||||
  selected-id="$ctrl.state.selectedTemplate.Id"
 | 
			
		||||
></custom-templates-list>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,11 +82,6 @@ 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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -260,7 +255,7 @@ class CustomTemplatesViewController {
 | 
			
		|||
      var template = _.find(this.templates, { Id: templateId });
 | 
			
		||||
      await this.CustomTemplateService.remove(templateId);
 | 
			
		||||
      this.Notifications.success('Template successfully deleted', template && template.Title);
 | 
			
		||||
      _.remove(this.templates, { Id: templateId });
 | 
			
		||||
      this.templates = this.templates.filter((template) => template.Id !== templateId);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      this.Notifications.error('Failure', err, 'Failed to delete template');
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,88 @@
 | 
			
		|||
import { Edit, Plus } from 'lucide-react';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { useCallback, useState } from 'react';
 | 
			
		||||
 | 
			
		||||
import { CustomTemplate } from '@/react/portainer/custom-templates/types';
 | 
			
		||||
 | 
			
		||||
import { DatatableHeader } from '@@/datatables/DatatableHeader';
 | 
			
		||||
import { Table } from '@@/datatables';
 | 
			
		||||
import { useTableState } from '@@/datatables/useTableState';
 | 
			
		||||
import { createPersistedStore } from '@@/datatables/types';
 | 
			
		||||
import { DatatableFooter } from '@@/datatables/DatatableFooter';
 | 
			
		||||
import { Button } from '@@/buttons';
 | 
			
		||||
import { Link } from '@@/Link';
 | 
			
		||||
 | 
			
		||||
import { CustomTemplatesListItem } from './CustomTemplatesListItem';
 | 
			
		||||
 | 
			
		||||
const tableKey = 'custom-templates-list';
 | 
			
		||||
const store = createPersistedStore(tableKey);
 | 
			
		||||
 | 
			
		||||
export function CustomTemplatesList({
 | 
			
		||||
  templates,
 | 
			
		||||
  onSelect,
 | 
			
		||||
  onDelete,
 | 
			
		||||
  selectedId,
 | 
			
		||||
}: {
 | 
			
		||||
  templates?: CustomTemplate[];
 | 
			
		||||
  onSelect: (template: CustomTemplate['Id']) => void;
 | 
			
		||||
  onDelete: (template: CustomTemplate['Id']) => void;
 | 
			
		||||
  selectedId: CustomTemplate['Id'];
 | 
			
		||||
}) {
 | 
			
		||||
  const [page, setPage] = useState(0);
 | 
			
		||||
 | 
			
		||||
  const listState = useTableState(store, tableKey);
 | 
			
		||||
 | 
			
		||||
  const filterBySearch = useCallback(
 | 
			
		||||
    (item: CustomTemplate) =>
 | 
			
		||||
      item.Title.includes(listState.search) ||
 | 
			
		||||
      item.Description.includes(listState.search) ||
 | 
			
		||||
      item.Note?.includes(listState.search),
 | 
			
		||||
    [listState.search]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const filteredTemplates = templates?.filter(filterBySearch) || [];
 | 
			
		||||
 | 
			
		||||
  const pagedTemplates =
 | 
			
		||||
    _.chunk(filteredTemplates, listState.pageSize)[page] || [];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Table.Container>
 | 
			
		||||
      <DatatableHeader
 | 
			
		||||
        onSearchChange={listState.setSearch}
 | 
			
		||||
        searchValue={listState.search}
 | 
			
		||||
        title="Custom Templates"
 | 
			
		||||
        titleIcon={Edit}
 | 
			
		||||
        renderTableActions={() => (
 | 
			
		||||
          <Button as={Link} props={{ to: '.new' }} icon={Plus}>
 | 
			
		||||
            Add Custom Template
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <div className="blocklist gap-y-2 !px-[20px] !pb-[20px]">
 | 
			
		||||
        {pagedTemplates.map((template) => (
 | 
			
		||||
          <CustomTemplatesListItem
 | 
			
		||||
            key={template.Id}
 | 
			
		||||
            template={template}
 | 
			
		||||
            onSelect={onSelect}
 | 
			
		||||
            isSelected={template.Id === selectedId}
 | 
			
		||||
            onDelete={onDelete}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
        {!templates && <div className="text-muted text-center">Loading...</div>}
 | 
			
		||||
        {filteredTemplates.length === 0 && (
 | 
			
		||||
          <div className="text-muted text-center">No templates available.</div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <DatatableFooter
 | 
			
		||||
        onPageChange={setPage}
 | 
			
		||||
        page={page}
 | 
			
		||||
        onPageSizeChange={listState.setPageSize}
 | 
			
		||||
        pageSize={listState.pageSize}
 | 
			
		||||
        pageCount={Math.ceil(filteredTemplates.length / listState.pageSize)}
 | 
			
		||||
        totalSelected={0}
 | 
			
		||||
      />
 | 
			
		||||
    </Table.Container>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue