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.validateForm = this.validateForm.bind(this);
|
||||||
this.confirmDelete = this.confirmDelete.bind(this);
|
this.confirmDelete = this.confirmDelete.bind(this);
|
||||||
this.selectTemplate = this.selectTemplate.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) {
|
selectTemplate(template) {
|
||||||
|
@ -70,7 +65,7 @@ export default class KubeCustomTemplatesViewController {
|
||||||
var template = _.find(this.templates, { Id: templateId });
|
var template = _.find(this.templates, { Id: templateId });
|
||||||
await this.CustomTemplateService.remove(templateId);
|
await this.CustomTemplateService.remove(templateId);
|
||||||
this.Notifications.success('Template successfully deleted', template && template.Title);
|
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) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Failed to delete template');
|
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>
|
<page-header title="'Custom Templates'" breadcrumbs="['Custom Templates']" reload="true"></page-header>
|
||||||
|
|
||||||
<div class="row">
|
<custom-templates-list
|
||||||
<div class="col-sm-12">
|
templates="$ctrl.templates"
|
||||||
<custom-templates-list
|
on-select="($ctrl.selectTemplate)"
|
||||||
ng-if="$ctrl.templates"
|
on-delete="($ctrl.confirmDelete)"
|
||||||
title-text="Templates"
|
selected-id="$ctrl.state.selectedTemplate.Id"
|
||||||
title-icon="edit"
|
></custom-templates-list>
|
||||||
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>
|
|
||||||
|
|
|
@ -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 { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||||
import { CustomTemplatesVariablesField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
import { CustomTemplatesVariablesField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
||||||
import { withControlledInput } from '@/react-tools/withControlledInput';
|
import { withControlledInput } from '@/react-tools/withControlledInput';
|
||||||
import { CustomTemplatesListItem } from '@/react/portainer/templates/custom-templates/ListView/CustomTemplatesListItem';
|
|
||||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import {
|
import {
|
||||||
|
@ -15,6 +14,7 @@ import { PlatformField } from '@/react/portainer/custom-templates/components/Pla
|
||||||
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
|
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
|
||||||
import { withFormValidation } from '@/react-tools/withFormValidation';
|
import { withFormValidation } from '@/react-tools/withFormValidation';
|
||||||
import { AppTemplatesList } from '@/react/portainer/templates/app-templates/AppTemplatesList';
|
import { AppTemplatesList } from '@/react/portainer/templates/app-templates/AppTemplatesList';
|
||||||
|
import { CustomTemplatesList } from '@/react/portainer/templates/custom-templates/ListView/CustomTemplatesList';
|
||||||
|
|
||||||
import { VariablesFieldAngular } from './variables-field';
|
import { VariablesFieldAngular } from './variables-field';
|
||||||
|
|
||||||
|
@ -39,15 +39,14 @@ export const ngModule = angular
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'customTemplatesListItem',
|
'customTemplatesList',
|
||||||
r2a(withUIRouter(withCurrentUser(CustomTemplatesListItem)), [
|
r2a(withUIRouter(withCurrentUser(CustomTemplatesList)), [
|
||||||
'onDelete',
|
'onDelete',
|
||||||
'onSelect',
|
'onSelect',
|
||||||
'template',
|
'templates',
|
||||||
'isSelected',
|
'selectedId',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
.component(
|
.component(
|
||||||
'customTemplatesPlatformSelector',
|
'customTemplatesPlatformSelector',
|
||||||
r2a(PlatformField, ['onChange', 'value'])
|
r2a(PlatformField, ['onChange', 'value'])
|
||||||
|
|
|
@ -62,20 +62,10 @@
|
||||||
</advanced-form>
|
</advanced-form>
|
||||||
</stack-from-template-form>
|
</stack-from-template-form>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
<custom-templates-list
|
||||||
<custom-templates-list
|
templates="$ctrl.templates"
|
||||||
ng-if="$ctrl.templates"
|
on-select="($ctrl.selectTemplate)"
|
||||||
title-text="Templates"
|
on-delete="($ctrl.confirmDelete)"
|
||||||
title-icon="edit"
|
selected-id="$ctrl.state.selectedTemplate.Id"
|
||||||
templates="$ctrl.templates"
|
></custom-templates-list>
|
||||||
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>
|
|
||||||
|
|
|
@ -82,11 +82,6 @@ class CustomTemplatesViewController {
|
||||||
this.isEditAllowed = this.isEditAllowed.bind(this);
|
this.isEditAllowed = this.isEditAllowed.bind(this);
|
||||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
||||||
this.onChangeTemplateVariables = this.onChangeTemplateVariables.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) {
|
isEditAllowed(template) {
|
||||||
|
@ -260,7 +255,7 @@ class CustomTemplatesViewController {
|
||||||
var template = _.find(this.templates, { Id: templateId });
|
var template = _.find(this.templates, { Id: templateId });
|
||||||
await this.CustomTemplateService.remove(templateId);
|
await this.CustomTemplateService.remove(templateId);
|
||||||
this.Notifications.success('Template successfully deleted', template && template.Title);
|
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) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Failed to delete template');
|
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