mirror of https://github.com/portainer/portainer
feat(ui): sort search bar icon [EE-3663] (#7205)
parent
88c4a43a19
commit
ce840997bf
|
@ -193,7 +193,7 @@ input:checked + .slider:before {
|
||||||
flex-wrap: nowarp;
|
flex-wrap: nowarp;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolBar > .searchBar {
|
.toolBar .searchBar {
|
||||||
flex: right;
|
flex: right;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
|
|
|
@ -40,10 +40,16 @@ angular.module('portainer.app').controller('GenericDatatableController', [
|
||||||
_.map(this.state.filteredDataSet, (item) => (item.Checked = false));
|
_.map(this.state.filteredDataSet, (item) => (item.Checked = false));
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onTextFilterChange = function () {
|
this.onTextFilterChangeGeneric = onTextFilterChangeGeneric;
|
||||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
|
||||||
|
this.onTextFilterChange = function onTextFilterChange() {
|
||||||
|
return this.onTextFilterChangeGeneric();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function onTextFilterChangeGeneric() {
|
||||||
|
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
||||||
|
}
|
||||||
|
|
||||||
this.changeOrderBy = function changeOrderBy(orderField) {
|
this.changeOrderBy = function changeOrderBy(orderField) {
|
||||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||||
this.state.orderBy = orderField;
|
this.state.orderBy = orderField;
|
||||||
|
|
|
@ -2,7 +2,6 @@ import angular from 'angular';
|
||||||
import 'angular-utils-pagination';
|
import 'angular-utils-pagination';
|
||||||
|
|
||||||
import { datatableTitlebar } from './titlebar';
|
import { datatableTitlebar } from './titlebar';
|
||||||
import { datatableSearchbar } from './searchbar';
|
|
||||||
import { datatableSortIcon } from './sort-icon';
|
import { datatableSortIcon } from './sort-icon';
|
||||||
import { datatablePagination } from './pagination';
|
import { datatablePagination } from './pagination';
|
||||||
import { datatableFilter } from './filter';
|
import { datatableFilter } from './filter';
|
||||||
|
@ -10,7 +9,6 @@ import { datatableFilter } from './filter';
|
||||||
export default angular
|
export default angular
|
||||||
.module('portainer.shared.datatable', ['angularUtils.directives.dirPagination'])
|
.module('portainer.shared.datatable', ['angularUtils.directives.dirPagination'])
|
||||||
.component('datatableTitlebar', datatableTitlebar)
|
.component('datatableTitlebar', datatableTitlebar)
|
||||||
.component('datatableSearchbar', datatableSearchbar)
|
|
||||||
.component('datatableSortIcon', datatableSortIcon)
|
.component('datatableSortIcon', datatableSortIcon)
|
||||||
.component('datatablePagination', datatablePagination)
|
.component('datatablePagination', datatablePagination)
|
||||||
.component('datatableFilter', datatableFilter).name;
|
.component('datatableFilter', datatableFilter).name;
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
<div class="searchBar">
|
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
|
||||||
<input type="text" class="searchInput" ng-model="$ctrl.filter" ng-change="$ctrl.onChange($ctrl.filter)" placeholder="Search..." ng-model-options="{ debounce: 300 }" />
|
|
||||||
</div>
|
|
|
@ -1,7 +0,0 @@
|
||||||
export const datatableSearchbar = {
|
|
||||||
bindings: {
|
|
||||||
onChange: '<',
|
|
||||||
ngModel: '<',
|
|
||||||
},
|
|
||||||
templateUrl: './datatable-searchbar.html',
|
|
||||||
};
|
|
|
@ -3,19 +3,9 @@
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
<div class="toolBar">
|
<div class="toolBar">
|
||||||
<div class="toolBarTitle"><i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px"></i> {{ $ctrl.titleText }} </div>
|
<div class="toolBarTitle"><i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px"></i> {{ $ctrl.titleText }} </div>
|
||||||
<div class="searchBar">
|
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
<datatable-searchbar value="$ctrl.state.textFilter" placeholder="'Search...'" on-change="($ctrl.onTextFilterChange)" data-cy="stack-searchInput"></datatable-searchbar>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="searchInput"
|
|
||||||
ng-model="$ctrl.state.textFilter"
|
|
||||||
ng-change="$ctrl.onTextFilterChange()"
|
|
||||||
placeholder="Search..."
|
|
||||||
auto-focus
|
|
||||||
ng-model-options="{ debounce: 300 }"
|
|
||||||
data-cy="stack-searchInput"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="PortainerStackCreate, PortainerStackDelete">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="PortainerStackCreate, PortainerStackDelete">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -33,6 +33,13 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { PasswordCheckHint } from '@@/PasswordCheckHint';
|
||||||
import { ViewLoading } from '@@/ViewLoading';
|
import { ViewLoading } from '@@/ViewLoading';
|
||||||
import { Tooltip } from '@@/Tip/Tooltip';
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
import { DashboardItem } from '@@/DashboardItem';
|
import { DashboardItem } from '@@/DashboardItem';
|
||||||
|
import { SearchBar } from '@@/datatables/SearchBar';
|
||||||
|
|
||||||
import { fileUploadField } from './file-upload-field';
|
import { fileUploadField } from './file-upload-field';
|
||||||
import { switchField } from './switch-field';
|
import { switchField } from './switch-field';
|
||||||
|
@ -43,4 +44,8 @@ export const componentsModule = angular
|
||||||
.component(
|
.component(
|
||||||
'dashboardItem',
|
'dashboardItem',
|
||||||
r2a(DashboardItem, ['featherIcon', 'icon', 'type', 'value', 'children'])
|
r2a(DashboardItem, ['featherIcon', 'icon', 'type', 'value', 'children'])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'datatableSearchbar',
|
||||||
|
r2a(SearchBar, ['data-cy', 'onChange', 'value', 'placeholder'])
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
<datatable-titlebar title="Activity Logs" icon="fa-history" feature="{{::$ctrl.feature}}"></datatable-titlebar>
|
<datatable-titlebar title="Activity Logs" icon="fa-history" feature="{{::$ctrl.feature}}"></datatable-titlebar>
|
||||||
<datatable-searchbar on-change="($ctrl.onChangeKeyword)" ng-model="$ctrl.keyword"></datatable-searchbar>
|
<datatable-searchbar on-change="($ctrl.onChangeKeyword)" value="$ctrl.keyword"></datatable-searchbar>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
|
@ -3,8 +3,9 @@ import moment from 'moment';
|
||||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
export default class ActivityLogsViewController {
|
export default class ActivityLogsViewController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, Notifications) {
|
constructor($async, $scope, Notifications) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
|
this.$scope = $scope;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
|
|
||||||
this.limitedFeature = FeatureId.ACTIVITY_AUDIT;
|
this.limitedFeature = FeatureId.ACTIVITY_AUDIT;
|
||||||
|
@ -54,9 +55,11 @@ export default class ActivityLogsViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeKeyword(keyword) {
|
onChangeKeyword(keyword) {
|
||||||
this.state.page = 1;
|
return this.$scope.$evalAsync(() => {
|
||||||
this.state.keyword = keyword;
|
this.state.page = 1;
|
||||||
this.loadLogs();
|
this.state.keyword = keyword;
|
||||||
|
this.loadLogs();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeDate({ startDate, endDate }) {
|
onChangeDate({ startDate, endDate }) {
|
||||||
|
@ -65,16 +68,6 @@ export default class ActivityLogsViewController {
|
||||||
this.loadLogs();
|
this.loadLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
async export() {
|
|
||||||
return this.$async(async () => {
|
|
||||||
try {
|
|
||||||
await this.UserActivityService.saveLogsAsCSV(this.state.sort, this.state.keyword, this.state.date, this.state.contextFilter);
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Failed loading user activity logs csv');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadLogs() {
|
async loadLogs() {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
this.state.logs = null;
|
this.state.logs = null;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
<datatable-titlebar title="Authentication Events" icon="fa-history" feature="{{::$ctrl.feature}}"></datatable-titlebar>
|
<datatable-titlebar title="Authentication Events" icon="fa-history" feature="{{::$ctrl.feature}}"></datatable-titlebar>
|
||||||
<datatable-searchbar on-change="($ctrl.onChangeKeyword)" ng-model="$ctrl.keyword"></datatable-searchbar>
|
<datatable-searchbar on-change="($ctrl.onChangeKeyword)" value="$ctrl.keyword"></datatable-searchbar>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover nowrap-cells">
|
<table class="table table-hover nowrap-cells">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
|
@ -68,9 +68,11 @@ export default class AuthLogsViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeKeyword(keyword) {
|
onChangeKeyword(keyword) {
|
||||||
this.state.page = 1;
|
return this.$scope.$evalAsync(() => {
|
||||||
this.state.keyword = keyword;
|
this.state.page = 1;
|
||||||
this.loadLogs();
|
this.state.keyword = keyword;
|
||||||
|
this.loadLogs();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeDate({ startDate, endDate }) {
|
onChangeDate({ startDate, endDate }) {
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
|
import { Search } from 'react-feather';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
interface Props {
|
import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
|
||||||
|
import { AutomationTestingProps } from '@/types';
|
||||||
|
|
||||||
|
interface Props extends AutomationTestingProps {
|
||||||
value: string;
|
value: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onChange(value: string): void;
|
onChange(value: string): void;
|
||||||
|
@ -10,21 +15,45 @@ export function SearchBar({
|
||||||
value,
|
value,
|
||||||
placeholder = 'Search...',
|
placeholder = 'Search...',
|
||||||
onChange,
|
onChange,
|
||||||
|
'data-cy': dataCy,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const [searchValue, setSearchValue] = useDebounce(value, onChange);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="searchBar">
|
<div className="searchBar items-center flex">
|
||||||
<i className="fa fa-search searchIcon" aria-hidden="true" />
|
<Search className="searchIcon feather" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="searchInput"
|
className="searchInput"
|
||||||
value={value}
|
value={searchValue}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
data-cy={dataCy}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useDebounce(defaultValue: string, onChange: (value: string) => void) {
|
||||||
|
const [searchValue, setSearchValue] = useState(defaultValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchValue(defaultValue);
|
||||||
|
}, [defaultValue]);
|
||||||
|
|
||||||
|
const onChangeDebounces = useMemo(
|
||||||
|
() => _.debounce(onChange, 300),
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [searchValue, handleChange] as const;
|
||||||
|
|
||||||
|
function handleChange(value: string) {
|
||||||
|
setSearchValue(value);
|
||||||
|
onChangeDebounces(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useSearchBarState(
|
export function useSearchBarState(
|
||||||
key: string
|
key: string
|
||||||
): [string, (value: string) => void] {
|
): [string, (value: string) => void] {
|
||||||
|
|
Loading…
Reference in New Issue