mirror of https://github.com/portainer/portainer
feat(templates): remove toggle and add sorting for app templates EE-2522 (#6884)
parent
9223c0226a
commit
df381b6a33
|
@ -3,6 +3,8 @@ import angular from 'angular';
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
import { ContainersDatatableContainer } from '@/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableContainer';
|
import { ContainersDatatableContainer } from '@/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableContainer';
|
||||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||||
|
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
|
||||||
|
import { TemplateListSortAngular } from '@/react/docker/app-templates/TemplateListSort';
|
||||||
|
|
||||||
export const componentsModule = angular
|
export const componentsModule = angular
|
||||||
.module('portainer.docker.react.components', [])
|
.module('portainer.docker.react.components', [])
|
||||||
|
@ -26,4 +28,6 @@ export const componentsModule = angular
|
||||||
'status',
|
'status',
|
||||||
'taskId',
|
'taskId',
|
||||||
])
|
])
|
||||||
).name;
|
)
|
||||||
|
.component('templateListDropdown', TemplateListDropdownAngular)
|
||||||
|
.component('templateListSort', TemplateListSortAngular).name;
|
||||||
|
|
|
@ -2,48 +2,53 @@ import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer.app').controller('TemplateListController', TemplateListController);
|
angular.module('portainer.app').controller('TemplateListController', TemplateListController);
|
||||||
|
|
||||||
function TemplateListController($async, $state, DatatableService, Notifications, TemplateService) {
|
function TemplateListController($scope, $async, $state, DatatableService, Notifications, TemplateService) {
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
textFilter: '',
|
textFilter: '',
|
||||||
selectedCategory: '',
|
selectedCategory: null,
|
||||||
categories: [],
|
categories: [],
|
||||||
|
typeFilters: [],
|
||||||
|
filterByType: null,
|
||||||
showContainerTemplates: true,
|
showContainerTemplates: true,
|
||||||
|
selectedOrderBy: null,
|
||||||
|
orderByFields: [],
|
||||||
|
orderDesc: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onTextFilterChange = function () {
|
this.onTextFilterChange = function () {
|
||||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.filterByTemplateType = function (item) {
|
ctrl.filterByTemplateType = function (item) {
|
||||||
switch (item.Type) {
|
switch (item.Type) {
|
||||||
case 1: // container
|
case 1: // container
|
||||||
return ctrl.state.showContainerTemplates;
|
return ctrl.state.showContainerTemplates;
|
||||||
case 2: // swarm stack
|
case 2: // swarm stack
|
||||||
return ctrl.showSwarmStacks;
|
return ctrl.showSwarmStacks && !ctrl.state.showContainerTemplates;
|
||||||
case 3: // docker compose
|
case 3: // docker compose
|
||||||
return !ctrl.showSwarmStacks || (ctrl.showSwarmStacks && ctrl.state.showContainerTemplates);
|
return !ctrl.state.showContainerTemplates || null === ctrl.state.filterByType;
|
||||||
case 4: // Edge stack templates
|
case 4: // Edge stack templates
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateCategories = function () {
|
ctrl.updateCategories = function () {
|
||||||
var availableCategories = [];
|
var availableCategories = [];
|
||||||
|
|
||||||
for (var i = 0; i < ctrl.templates.length; i++) {
|
for (var i = 0; i < ctrl.templates.length; i++) {
|
||||||
var template = ctrl.templates[i];
|
var template = ctrl.templates[i];
|
||||||
if (this.filterByTemplateType(template)) {
|
if (ctrl.filterByTemplateType(template)) {
|
||||||
availableCategories = availableCategories.concat(template.Categories);
|
availableCategories = availableCategories.concat(template.Categories);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.categories = _.sortBy(_.uniq(availableCategories));
|
ctrl.state.categories = _.sortBy(_.uniq(availableCategories));
|
||||||
};
|
};
|
||||||
|
|
||||||
this.filterByCategory = function (item) {
|
ctrl.filterByCategory = function (item) {
|
||||||
if (!ctrl.state.selectedCategory) {
|
if (!ctrl.state.selectedCategory) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -73,6 +78,40 @@ function TemplateListController($async, $state, DatatableService, Notifications,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 () {
|
this.$onInit = function () {
|
||||||
if (this.showSwarmStacks) {
|
if (this.showSwarmStacks) {
|
||||||
this.state.showContainerTemplates = false;
|
this.state.showContainerTemplates = false;
|
||||||
|
@ -83,5 +122,28 @@ function TemplateListController($async, $state, DatatableService, Notifications,
|
||||||
if (textFilter !== null) {
|
if (textFilter !== null) {
|
||||||
this.state.textFilter = textFilter;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,45 +2,45 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="{{ $ctrl.titleIcon }}" feather-icon="true" title-text="{{ $ctrl.titleText }}"></rd-widget-header>
|
<rd-widget-header icon="{{ $ctrl.titleIcon }}" feather-icon="true" title-text="{{ $ctrl.titleText }}"></rd-widget-header>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
<div class="mx-3">
|
<div class="actionBar">
|
||||||
<div class="actionBar">
|
<div class="row">
|
||||||
<div>
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.templates.new" ng-if="$ctrl.showAddAction">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.templates.new" ng-if="$ctrl.showAddAction">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add template
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add template
|
||||||
</button>
|
</button>
|
||||||
<span ng-class="{ 'pull-right': $ctrl.showAddAction }">
|
|
||||||
<ui-select ng-model="$ctrl.state.selectedCategory">
|
|
||||||
<ui-select-match placeholder="Select a category" allow-clear="true">
|
|
||||||
<span>{{ $select.selected }}</span>
|
|
||||||
</ui-select-match>
|
|
||||||
<ui-select-choices repeat="category in ($ctrl.state.categories | filter: $select.search)">
|
|
||||||
<span>{{ category }}</span>
|
|
||||||
</ui-select-choices>
|
|
||||||
</ui-select>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="small text-muted mt-3">
|
|
||||||
<label for="show_stacks" class="control-label text-left"> Show container templates </label>
|
|
||||||
<label class="switch space-left">
|
|
||||||
<input type="checkbox" name="show_stacks" ng-model="$ctrl.state.showContainerTemplates" ng-change="$ctrl.updateCategories()" /><i></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
<div class="searchBar">
|
<div class="col-sm-3">
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
<template-list-dropdown
|
||||||
<input
|
options="$ctrl.state.categories"
|
||||||
type="text"
|
on-change="($ctrl.applyCategoriesFilter)"
|
||||||
class="searchInput"
|
placeholder="'Category'"
|
||||||
ng-model="$ctrl.state.textFilter"
|
value="$ctrl.state.selectedCategory"
|
||||||
ng-change="$ctrl.onTextFilterChange()"
|
></template-list-dropdown>
|
||||||
placeholder="Search..."
|
</div>
|
||||||
auto-focus
|
<div class="col-sm-3">
|
||||||
ng-model-options="{ debounce: 300 }"
|
<template-list-dropdown
|
||||||
/>
|
options="$ctrl.state.typeFilters"
|
||||||
|
on-change="($ctrl.applyTypeFilter)"
|
||||||
|
placeholder="'Type'"
|
||||||
|
value="$ctrl.state.filterByType"
|
||||||
|
></template-list-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3 col-sm-offset-3">
|
||||||
|
<template-list-sort
|
||||||
|
on-change="($ctrl.changeOrderBy)"
|
||||||
|
on-descending="($ctrl.invertOrder)"
|
||||||
|
options="$ctrl.state.orderByFields"
|
||||||
|
sort-by-button="true"
|
||||||
|
sort-by-descending="$ctrl.state.orderDesc"
|
||||||
|
placeholder="'Sort By'"
|
||||||
|
value="$ctrl.state.selectedOrderBy"
|
||||||
|
></template-list-sort>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="blocklist">
|
<div class="blocklist">
|
||||||
<template-item
|
<template-item
|
||||||
ng-repeat="template in $ctrl.templates | filter: $ctrl.filterByTemplateType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter"
|
ng-repeat="template in $ctrl.templates | filter: $ctrl.filterByTemplateType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter"
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
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 (
|
||||||
|
<Select
|
||||||
|
placeholder={placeholder}
|
||||||
|
options={filterOptions}
|
||||||
|
value={filterValue}
|
||||||
|
isClearable
|
||||||
|
onChange={(option) => onChange(option?.label ?? null)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { react2angular } from '@/react-tools/react2angular';
|
||||||
|
|
||||||
|
import { TemplateListDropdown } from './TemplateListDropdown';
|
||||||
|
|
||||||
|
const TemplateListDropdownAngular = react2angular(TemplateListDropdown, [
|
||||||
|
'options',
|
||||||
|
'onChange',
|
||||||
|
'placeholder',
|
||||||
|
'value',
|
||||||
|
]);
|
||||||
|
export { TemplateListDropdown, TemplateListDropdownAngular };
|
|
@ -0,0 +1,18 @@
|
||||||
|
.sort-by-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-by-element {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-button {
|
||||||
|
background-color: var(--bg-sortbutton-color);
|
||||||
|
color: var(--text-ui-select-color);
|
||||||
|
border: 1px solid var(--border-sortbutton);
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { TemplateListDropdown } from '../TemplateListDropdown';
|
||||||
|
|
||||||
|
import styles from './TemplateListSort.module.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: string[];
|
||||||
|
onChange: (value: string | null) => void;
|
||||||
|
onDescending: () => void;
|
||||||
|
placeholder?: string;
|
||||||
|
sortByDescending: boolean;
|
||||||
|
sortByButton: boolean;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TemplateListSort({
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
onDescending,
|
||||||
|
placeholder,
|
||||||
|
sortByDescending,
|
||||||
|
sortByButton,
|
||||||
|
value,
|
||||||
|
}: Props) {
|
||||||
|
const upIcon = 'fa fa-sort-alpha-up';
|
||||||
|
const downIcon = 'fa fa-sort-alpha-down';
|
||||||
|
const iconStyle = sortByDescending ? upIcon : downIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.sortByContainer}>
|
||||||
|
<div className={styles.sortByElement}>
|
||||||
|
<TemplateListDropdown
|
||||||
|
placeholder={placeholder}
|
||||||
|
options={options}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.sortByElement}>
|
||||||
|
<button
|
||||||
|
className={styles.sortButton}
|
||||||
|
type="button"
|
||||||
|
disabled={!sortByButton || !value}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onDescending();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className={iconStyle} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { react2angular } from '@/react-tools/react2angular';
|
||||||
|
|
||||||
|
import { TemplateListSort } from './TemplateListSort';
|
||||||
|
|
||||||
|
const TemplateListSortAngular = react2angular(TemplateListSort, [
|
||||||
|
'options',
|
||||||
|
'onChange',
|
||||||
|
'onDescending',
|
||||||
|
'placeholder',
|
||||||
|
'sortByDescending',
|
||||||
|
'sortByButton',
|
||||||
|
'value',
|
||||||
|
]);
|
||||||
|
export { TemplateListSort, TemplateListSortAngular };
|
Loading…
Reference in New Issue