mirror of https://github.com/portainer/portainer
feat(ui): add sorting icon component and table header cell styling EE-3626 (#7165)
* feat(ui): add sorting icons EE-3626 feat(ui): Add react component for sorting icons feat(ui) make component usable in angular * feat(ui): update angular example EE-3626pull/7202/head
parent
712207e69f
commit
14a8b1d897
|
@ -0,0 +1,2 @@
|
||||||
|
export default 'SvgrURL';
|
||||||
|
export const ReactComponent = 'div';
|
|
@ -111,10 +111,10 @@ div.input-mask {
|
||||||
background: var(--bg-widget-table-color);
|
background: var(--bg-widget-table-color);
|
||||||
}
|
}
|
||||||
.widget .widget-body table thead * {
|
.widget .widget-body table thead * {
|
||||||
font-size: 14px !important;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.widget .widget-body table tbody * {
|
.widget .widget-body table tbody * {
|
||||||
font-size: 13px !important;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.widget .widget-body .error {
|
.widget .widget-body .error {
|
||||||
color: #ff0000;
|
color: #ff0000;
|
||||||
|
|
|
@ -264,6 +264,9 @@
|
||||||
--bg-multiselect-helpercontainer: var(--white-color);
|
--bg-multiselect-helpercontainer: var(--white-color);
|
||||||
--text-input-textarea: var(--white-color);
|
--text-input-textarea: var(--white-color);
|
||||||
|
|
||||||
|
--sort-icon-muted: var(--ui-gray-5);
|
||||||
|
--sort-icon-hover: var(--ui-gray-6);
|
||||||
|
--sort-icon: var(--ui-gray-9);
|
||||||
--border-checkbox: var(--ui-gray-5);
|
--border-checkbox: var(--ui-gray-5);
|
||||||
--bg-checkbox: var(--white-color);
|
--bg-checkbox: var(--white-color);
|
||||||
--border-searchbar: var(--ui-gray-5);
|
--border-searchbar: var(--ui-gray-5);
|
||||||
|
@ -443,6 +446,9 @@
|
||||||
--bg-multiselect-helpercontainer: var(--grey-1);
|
--bg-multiselect-helpercontainer: var(--grey-1);
|
||||||
--text-input-textarea: var(--grey-1);
|
--text-input-textarea: var(--grey-1);
|
||||||
|
|
||||||
|
--sort-icon-muted: var(--ui-gray-7);
|
||||||
|
--sort-icon-hover: var(--ui-gray-6);
|
||||||
|
--sort-icon: var(--ui-gray-3);
|
||||||
--border-checkbox: var(--ui-gray-5);
|
--border-checkbox: var(--ui-gray-5);
|
||||||
--bg-checkbox: var(--white-color);
|
--bg-checkbox: var(--white-color);
|
||||||
--border-searchbar: var(--ui-gray-5);
|
--border-searchbar: var(--ui-gray-5);
|
||||||
|
@ -614,6 +620,9 @@
|
||||||
--text-cm-string-color: var(--red-7);
|
--text-cm-string-color: var(--red-7);
|
||||||
--text-progress-bar-color: var(--black-color);
|
--text-progress-bar-color: var(--black-color);
|
||||||
|
|
||||||
|
--sort-icon-muted: var(--ui-gray-7);
|
||||||
|
--sort-icon-hover: var(--ui-gray-6);
|
||||||
|
--sort-icon: var(--ui-gray-3);
|
||||||
--border-checkbox: var(--ui-gray-5);
|
--border-checkbox: var(--ui-gray-5);
|
||||||
--bg-checkbox: var(--white-color);
|
--bg-checkbox: var(--white-color);
|
||||||
--border-searchbar: var(--ui-gray-5);
|
--border-searchbar: var(--ui-gray-5);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.isOpen }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.state.isOpen">
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.isOpen }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.state.isOpen">
|
||||||
<span uib-dropdown-toggle aria-label="Columns"><i class="fa fa-columns space-right" aria-hidden="true"></i></span>
|
<span uib-dropdown-toggle aria-label="Columns">
|
||||||
|
<pr-icon icon="'columns'" feather="true"></pr-icon>
|
||||||
|
</span>
|
||||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
<div class="tableMenu">
|
<div class="tableMenu">
|
||||||
<div class="menuHeader"> Show / Hide Columns </div>
|
<div class="menuHeader"> Show / Hide Columns </div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.datatable .toolBar {
|
.datatable .toolBar {
|
||||||
background-color: var(--bg-card-color);
|
background-color: var(--bg-card-color);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 20px 10px;
|
padding: 20px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
.datatable .toolBar .settings {
|
.datatable .toolBar .settings {
|
||||||
float: right;
|
float: right;
|
||||||
margin-right: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datatable .toolBar .setting {
|
.datatable .toolBar .setting {
|
||||||
|
@ -38,23 +38,29 @@
|
||||||
@apply text-blue-7;
|
@apply text-blue-7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.datatable tr > td a {
|
||||||
|
color: var(--ui-blue-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.datatable tr > td a:hover,
|
||||||
|
.datatable tr > td a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.toolBar .actionBar {
|
.toolBar .actionBar {
|
||||||
margin-right: 10px;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolBar .settings {
|
.toolBar .settings {
|
||||||
width: 60px;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datatable .searchBar {
|
.datatable .searchBar {
|
||||||
border: 1px solid var(--border-searchbar);
|
border: 1px solid var(--border-searchbar);
|
||||||
padding: 5px;
|
|
||||||
background: var(--bg-searchbar) !important;
|
background: var(--bg-searchbar) !important;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 4px 10px !important;
|
padding: 4px 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +73,6 @@
|
||||||
.toolBar .searchBar {
|
.toolBar .searchBar {
|
||||||
flex: right;
|
flex: right;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
width: 500px;
|
|
||||||
height: 30px;
|
height: 30px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
@ -146,6 +151,15 @@
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table > tbody > tr > td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > tbody > tr {
|
||||||
|
height: 50px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
[data-reach-menu-list],
|
[data-reach-menu-list],
|
||||||
[data-reach-menu-items] {
|
[data-reach-menu-items] {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
@ -172,7 +186,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.datatable .table-filters thead tr > th {
|
.datatable .table-filters thead tr > th {
|
||||||
height: 32px;
|
white-space: nowrap;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,16 +330,14 @@
|
||||||
width: 30px;
|
width: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table th button.sortable {
|
.table tr > th:first-child,
|
||||||
background: none;
|
.table tr > td:first-child {
|
||||||
border: none;
|
padding-left: 20px;
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--text-link-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table th button.sortable:hover .sortable-label {
|
.table tr > th:last-child,
|
||||||
text-decoration: underline;
|
.table tr > last-child {
|
||||||
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datatable .table-setting-menu-btn {
|
.datatable .table-setting-menu-btn {
|
||||||
|
|
|
@ -1,37 +1,52 @@
|
||||||
<div class="datatable">
|
<div class="datatable">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
<div class="toolBar">
|
<div class="toolBar vertical-center !gap-x-5 !gap-y-1 flex-wrap">
|
||||||
<div class="toolBarTitle"><i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px"></i> {{ $ctrl.titleText }} </div>
|
<div class="toolBarTitle vertical-center">
|
||||||
|
<pr-icon icon="'layers'" feather="true" class-name="'icon-nested-blue vertical-center'" mode="'primary'"></pr-icon>
|
||||||
<datatable-searchbar value="$ctrl.state.textFilter" placeholder="'Search...'" on-change="($ctrl.onTextFilterChange)" data-cy="stack-searchInput"></datatable-searchbar>
|
{{ $ctrl.titleText }}
|
||||||
|
</div>
|
||||||
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="PortainerStackCreate, PortainerStackDelete">
|
<div class="searchBar vertical-center">
|
||||||
|
<pr-icon icon="'search'" feather="true"></pr-icon>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="searchInput"
|
||||||
|
ng-model="$ctrl.state.textFilter"
|
||||||
|
ng-change="$ctrl.onTextFilterChange()"
|
||||||
|
placeholder="Search for a stack..."
|
||||||
|
auto-focus
|
||||||
|
ng-model-options="{ debounce: 300 }"
|
||||||
|
data-cy="stack-searchInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="actionBar !gap-3" ng-if="!$ctrl.offlineMode" authorization="PortainerStackCreate, PortainerStackDelete">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-danger"
|
class="btn btn-sm btn-dangerlight h-fit vertical-center !ml-0"
|
||||||
authorization="PortainerStackDelete"
|
authorization="PortainerStackDelete"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||||
data-cy="stack-removeStackButton"
|
data-cy="stack-removeStackButton"
|
||||||
>
|
>
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<pr-icon icon="'trash'" feather="true" mode="'danger'"></pr-icon>Remove
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
ng-disabled="!$ctrl.createEnabled"
|
ng-disabled="!$ctrl.createEnabled"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary h-fit vertical-center !ml-0"
|
||||||
ui-sref="docker.stacks.newstack"
|
ui-sref="docker.stacks.newstack"
|
||||||
authorization="PortainerStackCreate"
|
authorization="PortainerStackCreate"
|
||||||
data-cy="stack-addStackButton"
|
data-cy="stack-addStackButton"
|
||||||
>
|
>
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
|
<pr-icon icon="'plus'" feather="true"></pr-icon>Add stack
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings">
|
<div class="settings">
|
||||||
<datatable-columns-visibility columns="$ctrl.columnVisibility.columns" on-change="($ctrl.onColumnVisibilityChange)"></datatable-columns-visibility>
|
<datatable-columns-visibility columns="$ctrl.columnVisibility.columns" on-change="($ctrl.onColumnVisibilityChange)"></datatable-columns-visibility>
|
||||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
<span class="setting ml-2" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
<span uib-dropdown-toggle aria-label="Settings"><i class="fa fa-cog" aria-hidden="true"></i></span>
|
<span uib-dropdown-toggle aria-label="Settings">
|
||||||
|
<pr-icon icon="'settings'" feather="true"></pr-icon>
|
||||||
|
</span>
|
||||||
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
<div class="tableMenu">
|
<div class="tableMenu">
|
||||||
<div class="menuHeader"> Table settings </div>
|
<div class="menuHeader"> Table settings </div>
|
||||||
|
@ -55,7 +70,7 @@
|
||||||
<option value="300">5min</option>
|
<option value="300">5min</option>
|
||||||
</select>
|
</select>
|
||||||
<span>
|
<span>
|
||||||
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none"></i>
|
<pr-icon id="refreshRateChange" icon="'check'" feather="true" mode="'success'" size="'sm'"></pr-icon>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,68 +89,81 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open">
|
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open">
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="PortainerStackCreate, PortainerStackDelete">
|
<div class="flex flex-row flex-no-wrap gap-1">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="PortainerStackCreate, PortainerStackDelete">
|
||||||
<label for="select_all"></label>
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
</span>
|
<label for="select_all"></label>
|
||||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
|
||||||
Name
|
|
||||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
<span uib-dropdown-toggle ng-class="['table-filter', { 'filter-active': $ctrl.filters.state.enabled }]">
|
|
||||||
Filter
|
|
||||||
<i ng-class="['fa', { 'fa-filter': !$ctrl.filters.state.enabled, 'fa-check': $ctrl.filters.state.enabled }]" aria-hidden="true"></i>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<table-column-header
|
||||||
<div class="dropdown-menu" uib-dropdown-menu>
|
col-title="'Name'"
|
||||||
<div class="tableMenu">
|
can-sort="true"
|
||||||
<div class="menuHeader"> Filter by activity </div>
|
is-sorted="$ctrl.state.orderBy === 'Name'"
|
||||||
<div class="menuContent">
|
is-sorted-desc="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"
|
||||||
<div class="md-checkbox">
|
ng-click="$ctrl.changeOrderBy('Name')"
|
||||||
<input id="filter_usage_activeStacks" type="checkbox" ng-model="$ctrl.filters.state.showActiveStacks" ng-change="$ctrl.onFilterChange()" />
|
></table-column-header>
|
||||||
<label for="filter_usage_activeStacks">Active stacks</label>
|
<div>
|
||||||
|
<span uib-dropdown-toggle ng-class="['table-filter vertical-center !ml-1', { 'filter-active': $ctrl.filters.state.enabled }]">
|
||||||
|
Filter
|
||||||
|
<pr-icon ng-if="$ctrl.filters.state.enabled" icon="'check'" feather="true" size="'sm'"></pr-icon>
|
||||||
|
<pr-icon ng-if="!$ctrl.filters.state.enabled" icon="'filter'" feather="true" size="'sm'"></pr-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" uib-dropdown-menu>
|
||||||
|
<div class="tableMenu">
|
||||||
|
<div class="menuHeader"> Filter by activity </div>
|
||||||
|
<div class="menuContent">
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="filter_usage_activeStacks" type="checkbox" ng-model="$ctrl.filters.state.showActiveStacks" ng-change="$ctrl.onFilterChange()" />
|
||||||
|
<label for="filter_usage_activeStacks">Active stacks</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="filter_usage_unactiveStacks" type="checkbox" ng-model="$ctrl.filters.state.showUnactiveStacks" ng-change="$ctrl.onFilterChange()" />
|
||||||
|
<label for="filter_usage_unactiveStacks">Inactive stacks</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="md-checkbox">
|
<div>
|
||||||
<input id="filter_usage_unactiveStacks" type="checkbox" ng-model="$ctrl.filters.state.showUnactiveStacks" ng-change="$ctrl.onFilterChange()" />
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
|
||||||
<label for="filter_usage_unactiveStacks">Inactive stacks</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ng-click="$ctrl.changeOrderBy('Type')">
|
<table-column-header
|
||||||
Type
|
col-title="'Type'"
|
||||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && !$ctrl.state.reverseOrder"></i>
|
can-sort="true"
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
|
is-sorted="$ctrl.state.orderBy === 'Type'"
|
||||||
</a>
|
is-sorted-desc="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"
|
||||||
|
ng-click="$ctrl.changeOrderBy('Type')"
|
||||||
|
></table-column-header>
|
||||||
</th>
|
</th>
|
||||||
<th>Control</th>
|
<th><table-column-header col-title="'Control'" can-sort="false"></table-column-header></th>
|
||||||
<th>
|
<th>
|
||||||
<a ng-click="$ctrl.changeOrderBy('ResourceControl.CreationDate')">
|
<table-column-header
|
||||||
Created
|
col-title="'Created'"
|
||||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.CreationDate' && !$ctrl.state.reverseOrder"></i>
|
can-sort="true"
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.CreationDate' && $ctrl.state.reverseOrder"></i>
|
is-sorted="$ctrl.state.orderBy === 'ResourceControl.CreationDate'"
|
||||||
</a>
|
is-sorted-desc="$ctrl.state.orderBy === 'ResourceControl.CreationDate' && $ctrl.state.reverseOrder"
|
||||||
|
ng-click="$ctrl.changeOrderBy('ResourceControl.CreationDate')"
|
||||||
|
></table-column-header>
|
||||||
</th>
|
</th>
|
||||||
<th ng-if="$ctrl.columnVisibility.columns.updated.display">
|
<th ng-if="$ctrl.columnVisibility.columns.updated.display">
|
||||||
<a ng-click="$ctrl.changeOrderBy('ResourceControl.UpdateDate')">
|
<table-column-header
|
||||||
Updated
|
col-title="'Updated'"
|
||||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.UpdateDate' && !$ctrl.state.reverseOrder"></i>
|
can-sort="true"
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.UpdateDate' && $ctrl.state.reverseOrder"></i>
|
is-sorted="$ctrl.state.orderBy === 'ResourceControl.UpdateDate'"
|
||||||
</a>
|
is-sorted-desc="$ctrl.state.orderBy === 'ResourceControl.UpdateDate' && $ctrl.state.reverseOrder"
|
||||||
|
ng-click="$ctrl.changeOrderBy('ResourceControl.UpdateDate')"
|
||||||
|
></table-column-header>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
|
<table-column-header
|
||||||
Ownership
|
col-title="'Ownership'"
|
||||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && !$ctrl.state.reverseOrder"></i>
|
can-sort="true"
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && $ctrl.state.reverseOrder"></i>
|
is-sorted="$ctrl.state.orderBy === 'ResourceControl.Ownership'"
|
||||||
</a>
|
is-sorted-desc="$ctrl.state.orderBy === 'ResourceControl.Ownership' && $ctrl.state.reverseOrder"
|
||||||
|
ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')"
|
||||||
|
></table-column-header>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -155,29 +183,31 @@
|
||||||
>{{ item.Name }}</a
|
>{{ item.Name }}</a
|
||||||
>
|
>
|
||||||
<span ng-if="$ctrl.offlineMode">{{ item.Name }}</span>
|
<span ng-if="$ctrl.offlineMode">{{ item.Name }}</span>
|
||||||
<span ng-if="item.Regular && item.Status == 2" style="margin-left: 10px" class="label label-warning image-tag space-left">Inactive</span>
|
<span ng-if="item.Regular && item.Status == 2" class="label label-warning image-tag ml-2">Inactive</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.Type === 1 ? 'Swarm' : 'Compose' }}</td>
|
<td>{{ item.Type === 1 ? 'Swarm' : 'Compose' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
ng-if="item.Orphaned"
|
ng-if="item.Orphaned"
|
||||||
class="interactive"
|
class="interactive vertical-center"
|
||||||
tooltip-append-to-body="true"
|
tooltip-append-to-body="true"
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
tooltip-class="portainer-tooltip"
|
tooltip-class="portainer-tooltip"
|
||||||
uib-tooltip="This stack was created inside an environment that is no longer registered inside Portainer."
|
uib-tooltip="This stack was created inside an environment that is no longer registered inside Portainer."
|
||||||
>
|
>
|
||||||
Orphaned <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-left: 2px"></i>
|
Orphaned
|
||||||
|
<pr-icon icon="'alert-circle'" feather="true" class-name="'ml-0.5'" mode="'warning'"></pr-icon>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
ng-if="item.External"
|
ng-if="item.External"
|
||||||
class="interactive"
|
class="interactive vertical-center"
|
||||||
tooltip-append-to-body="true"
|
tooltip-append-to-body="true"
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
tooltip-class="portainer-tooltip"
|
tooltip-class="portainer-tooltip"
|
||||||
uib-tooltip="This stack was created outside of Portainer. Control over this stack is limited."
|
uib-tooltip="This stack was created outside of Portainer. Control over this stack is limited."
|
||||||
>
|
>
|
||||||
Limited <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-left: 2px"></i>
|
Limited
|
||||||
|
<pr-icon icon="'alert-circle'" feather="true" class-name="'ml-0.5'" mode="'warning'"></pr-icon>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="item.Regular">Total</span>
|
<span ng-if="item.Regular">Total</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -190,8 +220,8 @@
|
||||||
<span ng-if="!item.UpdateDate"> - </span>
|
<span ng-if="!item.UpdateDate"> - </span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span>
|
<span class="vertical-center">
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<pr-icon ng-attr-icon="item.ResourceControl.Ownership | ownershipicon" feather="true" class-name="'icon ml-0.5'"></pr-icon>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -213,7 +243,7 @@
|
||||||
<div class="paginationControls">
|
<div class="paginationControls">
|
||||||
<form class="form-inline">
|
<form class="form-inline">
|
||||||
<span class="limitSelector">
|
<span class="limitSelector">
|
||||||
<span style="margin-right: 5px"> Items per page </span>
|
<span class="mr-1"> Items per page </span>
|
||||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
|
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
|
||||||
<option value="0">All</option>
|
<option value="0">All</option>
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
|
|
|
@ -33,13 +33,6 @@ angular.module('portainer.app').controller('StacksDatatableController', [
|
||||||
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
|
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onTextFilterChange = onTextFilterChange.bind(this);
|
|
||||||
|
|
||||||
function onTextFilterChange(value) {
|
|
||||||
this.state.textFilter = value;
|
|
||||||
this.onTextFilterChangeGeneric();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do not allow external items
|
* Do not allow external items
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -106,13 +106,13 @@ export function environmentTypeIcon(type) {
|
||||||
export function ownershipIcon(ownership) {
|
export function ownershipIcon(ownership) {
|
||||||
switch (ownership) {
|
switch (ownership) {
|
||||||
case RCO.PRIVATE:
|
case RCO.PRIVATE:
|
||||||
return 'fa fa-eye-slash';
|
return 'eye-off';
|
||||||
case RCO.ADMINISTRATORS:
|
case RCO.ADMINISTRATORS:
|
||||||
return 'fa fa-eye-slash';
|
return 'eye-off';
|
||||||
case RCO.RESTRICTED:
|
case RCO.RESTRICTED:
|
||||||
return 'fa fa-users';
|
return 'users';
|
||||||
default:
|
default:
|
||||||
return 'fa fa-eye';
|
return 'eye';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Loading } from '@@/Widget/Loading';
|
||||||
import { PasswordCheckHint } from '@@/PasswordCheckHint';
|
import { PasswordCheckHint } from '@@/PasswordCheckHint';
|
||||||
import { ViewLoading } from '@@/ViewLoading';
|
import { ViewLoading } from '@@/ViewLoading';
|
||||||
import { Tooltip } from '@@/Tip/Tooltip';
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
|
import { TableColumnHeaderAngular } from '@@/datatables/TableHeaderCell';
|
||||||
import { DashboardItem } from '@@/DashboardItem';
|
import { DashboardItem } from '@@/DashboardItem';
|
||||||
import { SearchBar } from '@@/datatables/SearchBar';
|
import { SearchBar } from '@@/datatables/SearchBar';
|
||||||
|
|
||||||
|
@ -31,6 +32,15 @@ export const componentsModule = angular
|
||||||
r2a(PasswordCheckHint, ['forceChangePassword', 'passwordValid'])
|
r2a(PasswordCheckHint, ['forceChangePassword', 'passwordValid'])
|
||||||
)
|
)
|
||||||
.component('rdLoading', r2a(Loading, []))
|
.component('rdLoading', r2a(Loading, []))
|
||||||
|
.component(
|
||||||
|
'tableColumnHeader',
|
||||||
|
r2a(TableColumnHeaderAngular, [
|
||||||
|
'colTitle',
|
||||||
|
'canSort',
|
||||||
|
'isSorted',
|
||||||
|
'isSortedDesc',
|
||||||
|
])
|
||||||
|
)
|
||||||
.component('viewLoading', r2a(ViewLoading, ['message']))
|
.component('viewLoading', r2a(ViewLoading, ['message']))
|
||||||
.component(
|
.component(
|
||||||
'pageHeader',
|
'pageHeader',
|
||||||
|
|
|
@ -67,7 +67,13 @@ export function react2angular<T, U extends PropNames<T>[]>(
|
||||||
el
|
el
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
this.$onDestroy = () => ReactDOM.unmountComponentAtNode(el);
|
this.$onDestroy = () => {
|
||||||
|
// eslint-disable-next-line react/no-find-dom-node
|
||||||
|
const domNode = ReactDOM.findDOMNode(el);
|
||||||
|
if (domNode != null && domNode.parentElement != null) {
|
||||||
|
ReactDOM.unmountComponentAtNode(domNode.parentElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
.sort-icon {
|
/* highlight the sort icons for columns that aren't actively sorting */
|
||||||
width: 1em;
|
button:not(.sortingActive):hover path {
|
||||||
height: 1em;
|
fill: var(--sort-icon-hover);
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,8 @@ import clsx from 'clsx';
|
||||||
import { PropsWithChildren, ReactNode } from 'react';
|
import { PropsWithChildren, ReactNode } from 'react';
|
||||||
import { TableHeaderProps } from 'react-table';
|
import { TableHeaderProps } from 'react-table';
|
||||||
|
|
||||||
import { Button } from '@@/buttons';
|
|
||||||
|
|
||||||
import { useTableContext } from './TableContainer';
|
import { useTableContext } from './TableContainer';
|
||||||
|
import { TableHeaderSortIcons } from './TableHeaderSortIcons';
|
||||||
import styles from './TableHeaderCell.module.css';
|
import styles from './TableHeaderCell.module.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -32,15 +31,17 @@ export function TableHeaderCell({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th role={role} style={style} className={className}>
|
<th role={role} style={style} className={className}>
|
||||||
<SortWrapper
|
<div className="flex flex-row flex-nowrap h-full items-center gap-1">
|
||||||
canSort={canSort}
|
<SortWrapper
|
||||||
onClick={onSortClick}
|
canSort={canSort}
|
||||||
isSorted={isSorted}
|
onClick={onSortClick}
|
||||||
isSortedDesc={isSortedDesc}
|
isSorted={isSorted}
|
||||||
>
|
isSortedDesc={isSortedDesc}
|
||||||
{render()}
|
>
|
||||||
</SortWrapper>
|
{render()}
|
||||||
{canFilter ? renderFilter() : null}
|
</SortWrapper>
|
||||||
|
{canFilter ? renderFilter() : null}
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -49,13 +50,13 @@ interface SortWrapperProps {
|
||||||
canSort: boolean;
|
canSort: boolean;
|
||||||
isSorted: boolean;
|
isSorted: boolean;
|
||||||
isSortedDesc?: boolean;
|
isSortedDesc?: boolean;
|
||||||
onClick: (desc: boolean) => void;
|
onClick?: (desc: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SortWrapper({
|
function SortWrapper({
|
||||||
canSort,
|
canSort,
|
||||||
children,
|
children,
|
||||||
onClick,
|
onClick = () => {},
|
||||||
isSorted,
|
isSorted,
|
||||||
isSortedDesc,
|
isSortedDesc,
|
||||||
}: PropsWithChildren<SortWrapperProps>) {
|
}: PropsWithChildren<SortWrapperProps>) {
|
||||||
|
@ -64,27 +65,47 @@ function SortWrapper({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<button
|
||||||
color="link"
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onClick(!isSortedDesc)}
|
onClick={() => onClick(!isSortedDesc)}
|
||||||
className="sortable"
|
className={clsx(
|
||||||
>
|
'sortable !bg-transparent w-full h-full !ml-0 !px-0 border-none focus:border-none',
|
||||||
<span className="sortable-label">{children}</span>
|
isSorted && styles.sortingActive
|
||||||
|
|
||||||
{isSorted ? (
|
|
||||||
<i
|
|
||||||
className={clsx(
|
|
||||||
'fa',
|
|
||||||
'space-left',
|
|
||||||
isSortedDesc ? 'fa-sort-alpha-up' : 'fa-sort-alpha-down',
|
|
||||||
styles.sortIcon
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className={styles.sortIcon} />
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
>
|
||||||
|
<div className="flex flex-row justify-start items-center w-full h-full">
|
||||||
|
{children}
|
||||||
|
<TableHeaderSortIcons
|
||||||
|
sorted={isSorted}
|
||||||
|
descending={isSorted && !!isSortedDesc}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableColumnHeaderAngularProps {
|
||||||
|
colTitle: string;
|
||||||
|
canSort: boolean;
|
||||||
|
isSorted?: boolean;
|
||||||
|
isSortedDesc?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableColumnHeaderAngular({
|
||||||
|
canSort,
|
||||||
|
isSorted,
|
||||||
|
colTitle,
|
||||||
|
isSortedDesc,
|
||||||
|
}: TableColumnHeaderAngularProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row flex-nowrap h-full">
|
||||||
|
<SortWrapper
|
||||||
|
canSort={canSort}
|
||||||
|
isSorted={!!isSorted}
|
||||||
|
isSortedDesc={isSortedDesc}
|
||||||
|
>
|
||||||
|
{colTitle}
|
||||||
|
</SortWrapper>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
.sort-icon > path {
|
||||||
|
fill: var(--sort-icon-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-sort-icon > path {
|
||||||
|
fill: var(--sort-icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-icon {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import SortDownIcon from './sort-arrow-down.svg?c';
|
||||||
|
import SortUpIcon from './sort-arrow-up.svg?c';
|
||||||
|
import styles from './TableHeaderSortIcons.module.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
sorted: boolean;
|
||||||
|
descending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableHeaderSortIcons({ sorted, descending }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row no-wrap w-min-max">
|
||||||
|
<SortDownIcon
|
||||||
|
className={clsx(
|
||||||
|
'space-left',
|
||||||
|
sorted && !descending && styles.activeSortIcon,
|
||||||
|
styles.sortIcon
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<SortUpIcon
|
||||||
|
className={clsx(
|
||||||
|
'-ml-1', // shift closer to SortDownIcon to match the mockup
|
||||||
|
sorted && descending && styles.activeSortIcon,
|
||||||
|
styles.sortIcon
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.4734 0.727041C4.4734 0.399398 4.2078 0.133789 3.88015 0.133789C3.55251 0.133789 3.2869 0.399398 3.2869 0.727041H4.4734ZM3.88015 11.2737L3.47355 11.7057C3.70705 11.9255 4.07293 11.9199 4.29966 11.6931L3.88015 11.2737ZM6.93628 9.0563C7.16795 8.82464 7.16795 8.44901 6.93618 8.21734C6.70452 7.98568 6.32891 7.98568 6.09723 8.21734L6.93628 9.0563ZM1.48521 8.20479C1.24663 7.98024 0.871167 7.99161 0.646611 8.2302C0.422046 8.46878 0.433416 8.84431 0.672003 9.06886L1.48521 8.20479ZM3.2869 0.727041V11.2737H4.4734V0.727041H3.2869ZM6.09723 8.21734L3.46064 10.8542L4.29966 11.6931L6.93628 9.0563L6.09723 8.21734ZM4.28676 10.8417L1.48521 8.20479L0.672003 9.06886L3.47355 11.7057L4.28676 10.8417Z" fill="#D0D5DD"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 818 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.52627 11.2732C3.52627 11.6008 3.79185 11.8664 4.11952 11.8664C4.4472 11.8664 4.71278 11.6008 4.71278 11.2732H3.52627ZM4.11952 0.726556L4.5261 0.29456C4.29265 0.0747795 3.92672 0.0803264 3.7 0.307077L4.11952 0.726556ZM1.06338 2.94393C0.83172 3.17561 0.83172 3.55124 1.06348 3.7829C1.29515 4.01458 1.67078 4.01456 1.90244 3.78286L1.06338 2.94393ZM6.51448 3.79539C6.75307 4.01996 7.1285 4.00859 7.35304 3.77C7.57759 3.53141 7.56622 3.15595 7.32763 2.9314L6.51448 3.79539ZM4.71278 11.2732V0.726556H3.52627V11.2732H4.71278ZM1.90244 3.78286L4.53905 1.14602L3.7 0.307077L1.06338 2.94393L1.90244 3.78286ZM3.71295 1.15855L6.51448 3.79539L7.32763 2.9314L4.5261 0.29456L3.71295 1.15855Z" fill="#D0D5DD"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 807 B |
|
@ -84,6 +84,7 @@ module.exports = {
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/app/__mocks__/fileMock.js',
|
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/app/__mocks__/fileMock.js',
|
||||||
'\\.(css|less)$': '<rootDir>/app/__mocks__/styleMock.js',
|
'\\.(css|less)$': '<rootDir>/app/__mocks__/styleMock.js',
|
||||||
|
'\\.svg\\?c$': '<rootDir>/app/__mocks__/svg.js',
|
||||||
'^@@/(.*)$': '<rootDir>/app/react/components/$1',
|
'^@@/(.*)$': '<rootDir>/app/react/components/$1',
|
||||||
'^@/(.*)$': '<rootDir>/app/$1',
|
'^@/(.*)$': '<rootDir>/app/$1',
|
||||||
'^Agent/(.*)?': '<rootDir>/app/agent/$1',
|
'^Agent/(.*)?': '<rootDir>/app/agent/$1',
|
||||||
|
|
Loading…
Reference in New Issue