From a9887d4a31315566c2cd78114f3d4d7bba2824ca Mon Sep 17 00:00:00 2001
From: Richard Wei <54336863+WaysonWei@users.noreply.github.com>
Date: Wed, 20 Apr 2022 17:04:05 +1200
Subject: [PATCH] Homepage Filter Component (#6762)
---
.../components/FilterSearchBar.module.css | 10 +
.../datatables/components/FilterSearchBar.tsx | 45 +++
.../components/SortbySelector.module.css | 10 +
.../datatables/components/SortbySelector.tsx | 62 ++++
.../environments/environment.service/index.ts | 1 -
app/portainer/environments/queries.ts | 1 -
.../EnvironmentList.module.css | 28 ++
.../home/EnvironmentList/EnvironmentList.tsx | 323 +++++++++++++++++-
app/portainer/home/HomepageFilter.tsx | 44 +++
app/portainer/home/types.ts | 5 +
10 files changed, 511 insertions(+), 18 deletions(-)
create mode 100644 app/portainer/components/datatables/components/FilterSearchBar.module.css
create mode 100644 app/portainer/components/datatables/components/FilterSearchBar.tsx
create mode 100644 app/portainer/components/datatables/components/SortbySelector.module.css
create mode 100644 app/portainer/components/datatables/components/SortbySelector.tsx
create mode 100644 app/portainer/home/HomepageFilter.tsx
diff --git a/app/portainer/components/datatables/components/FilterSearchBar.module.css b/app/portainer/components/datatables/components/FilterSearchBar.module.css
new file mode 100644
index 000000000..c7ba2815a
--- /dev/null
+++ b/app/portainer/components/datatables/components/FilterSearchBar.module.css
@@ -0,0 +1,10 @@
+.searchBar {
+ padding: 8px;
+ border: 1px solid var(--border-color);
+ border-radius: 5px;
+}
+
+.searchBar .iconSpan {
+ display: inline-block;
+ margin-right: 5px;
+}
diff --git a/app/portainer/components/datatables/components/FilterSearchBar.tsx b/app/portainer/components/datatables/components/FilterSearchBar.tsx
new file mode 100644
index 000000000..44f0b1903
--- /dev/null
+++ b/app/portainer/components/datatables/components/FilterSearchBar.tsx
@@ -0,0 +1,45 @@
+import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
+
+import styles from './FilterSearchBar.module.css';
+
+interface Props {
+ value: string;
+ placeholder?: string;
+ onChange(value: string): void;
+}
+
+export function FilterSearchBar({
+ value,
+ placeholder = 'Search...',
+ onChange,
+}: Props) {
+ return (
+
+
+
+
+
+ onChange(e.target.value)}
+ placeholder={placeholder}
+ />
+
+
+ );
+}
+
+export function useSearchBarState(
+ key: string
+): [string, (value: string) => void] {
+ const filterKey = keyBuilder(key);
+ const [value, setValue] = useLocalStorage(filterKey, '', sessionStorage);
+
+ return [value, setValue];
+
+ function keyBuilder(key: string) {
+ return `datatable_text_filter_${key}`;
+ }
+}
diff --git a/app/portainer/components/datatables/components/SortbySelector.module.css b/app/portainer/components/datatables/components/SortbySelector.module.css
new file mode 100644
index 000000000..9937fd391
--- /dev/null
+++ b/app/portainer/components/datatables/components/SortbySelector.module.css
@@ -0,0 +1,10 @@
+.sort-by-container {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.sort-by-element {
+ display: inline-block;
+}
diff --git a/app/portainer/components/datatables/components/SortbySelector.tsx b/app/portainer/components/datatables/components/SortbySelector.tsx
new file mode 100644
index 000000000..c3cf393e5
--- /dev/null
+++ b/app/portainer/components/datatables/components/SortbySelector.tsx
@@ -0,0 +1,62 @@
+import { useEffect, useState } from 'react';
+
+import { Select } from '@/portainer/components/form-components/ReactSelect';
+import { Button } from '@/portainer/components/Button';
+import { Filter } from '@/portainer/home/types';
+
+import styles from './SortbySelector.module.css';
+
+interface Props {
+ filterOptions: Filter[];
+ onChange: (filterOptions: Filter) => void;
+ onDescending: () => void;
+ placeHolder: string;
+ sortByDescending: boolean;
+ sortByButton: boolean;
+}
+
+export function SortbySelector({
+ filterOptions,
+ onChange,
+ onDescending,
+ placeHolder,
+ sortByDescending,
+ sortByButton,
+}: Props) {
+ const upIcon = 'fa fa-sort-alpha-up';
+ const downIcon = 'fa fa-sort-alpha-down';
+ const [iconStyle, setIconStyle] = useState(upIcon);
+
+ useEffect(() => {
+ if (sortByDescending) {
+ setIconStyle(downIcon);
+ } else {
+ setIconStyle(upIcon);
+ }
+ }, [sortByDescending]);
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/portainer/environments/environment.service/index.ts b/app/portainer/environments/environment.service/index.ts
index 16d676040..3bb17d9c5 100644
--- a/app/portainer/environments/environment.service/index.ts
+++ b/app/portainer/environments/environment.service/index.ts
@@ -50,7 +50,6 @@ export async function getEndpoints(
try {
const response = await axios.get(url, { params });
-
const totalCount = response.headers['x-total-count'];
const totalAvailable = response.headers['x-total-available'];
diff --git a/app/portainer/environments/queries.ts b/app/portainer/environments/queries.ts
index 2a3eca82a..977e402b1 100644
--- a/app/portainer/environments/queries.ts
+++ b/app/portainer/environments/queries.ts
@@ -33,7 +33,6 @@ export function useEnvironmentList(query: Query = {}, refetchOffline = false) {
const hasOfflineEnvironment = data.value.some(
(env) => env.Status === EnvironmentStatus.Down
);
-
return hasOfflineEnvironment && ENVIRONMENTS_POLLING_INTERVAL;
},
onError(error) {
diff --git a/app/portainer/home/EnvironmentList/EnvironmentList.module.css b/app/portainer/home/EnvironmentList/EnvironmentList.module.css
index 196c25fbb..5a43f2540 100644
--- a/app/portainer/home/EnvironmentList/EnvironmentList.module.css
+++ b/app/portainer/home/EnvironmentList/EnvironmentList.module.css
@@ -5,3 +5,31 @@
.refresh-environments-button {
margin-left: 0 !important;
}
+
+.filter-container {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+}
+
+.filter-left {
+ margin-left: 10px;
+ padding: 10px 0px;
+ width: 15%;
+ display: inline-block;
+}
+
+.filter-right {
+ padding: 10px;
+ width: 15%;
+ right: 0;
+ display: inline-block;
+ margin-left: auto;
+}
+
+.filter-button {
+ margin-left: 10px;
+ padding: 10px 0px;
+ width: 5%;
+ display: inline-block;
+}
diff --git a/app/portainer/home/EnvironmentList/EnvironmentList.tsx b/app/portainer/home/EnvironmentList/EnvironmentList.tsx
index 566a3ee43..3bcb6a234 100644
--- a/app/portainer/home/EnvironmentList/EnvironmentList.tsx
+++ b/app/portainer/home/EnvironmentList/EnvironmentList.tsx
@@ -3,13 +3,19 @@ import clsx from 'clsx';
import { PaginationControls } from '@/portainer/components/pagination-controls';
import { usePaginationLimitState } from '@/portainer/hooks/usePaginationLimitState';
-import { Environment } from '@/portainer/environments/types';
+import {
+ Environment,
+ EnvironmentType,
+ EnvironmentStatus,
+} from '@/portainer/environments/types';
import { Button } from '@/portainer/components/Button';
import { useIsAdmin } from '@/portainer/hooks/useUser';
import {
- SearchBar,
+ FilterSearchBar,
useSearchBarState,
-} from '@/portainer/components/datatables/components/SearchBar';
+} from '@/portainer/components/datatables/components/FilterSearchBar';
+import { SortbySelector } from '@/portainer/components/datatables/components/SortbySelector';
+import { HomepageFilter } from '@/portainer/home/HomepageFilter';
import {
TableActions,
TableContainer,
@@ -19,6 +25,8 @@ import { TableFooter } from '@/portainer/components/datatables/components/TableF
import { useDebounce } from '@/portainer/hooks/useDebounce';
import { useEnvironmentList } from '@/portainer/environments/queries';
import { useGroups } from '@/portainer/environment-groups/queries';
+import { useTags } from '@/portainer/tags/queries';
+import { Filter } from '@/portainer/home/types';
import { EnvironmentItem } from './EnvironmentItem';
import { KubeconfigButton } from './KubeconfigButton';
@@ -33,21 +41,255 @@ interface Props {
export function EnvironmentList({ onClickItem, onRefresh }: Props) {
const isAdmin = useIsAdmin();
const storageKey = 'home_endpoints';
+ const allEnvironmentType = [
+ EnvironmentType.Docker,
+ EnvironmentType.AgentOnDocker,
+ EnvironmentType.Azure,
+ EnvironmentType.EdgeAgentOnDocker,
+ EnvironmentType.KubernetesLocal,
+ EnvironmentType.AgentOnKubernetes,
+ EnvironmentType.EdgeAgentOnKubernetes,
+ ];
+ const [platformType, setPlatformType] = useState(allEnvironmentType);
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
const [pageLimit, setPageLimit] = usePaginationLimitState(storageKey);
const [page, setPage] = useState(1);
-
const debouncedTextFilter = useDebounce(searchBarValue);
+ const [statusFilter, setStatusFilter] = useState([]);
+ const [tagFilter, setTagFilter] = useState([]);
+ const [groupFilter, setGroupFilter] = useState([]);
+ const [sortByFilter, setSortByFilter] = useState('');
+ const [sortByDescending, setSortByDescending] = useState(false);
+ const [sortByButton, setSortByButton] = useState(false);
+
+ const [platformState, setPlatformState] = useState([]);
+ const [statusState, setStatusState] = useState([]);
+ const [tagState, setTagState] = useState([]);
+ const [groupState, setGroupState] = useState([]);
+
+ const groupsQuery = useGroups();
+
+ const { isLoading, environments, totalCount, totalAvailable } =
+ useEnvironmentList(
+ { page, pageLimit, types: platformType, search: debouncedTextFilter },
+ true
+ );
+
useEffect(() => {
setPage(1);
}, [searchBarValue]);
- const groupsQuery = useGroups();
+ interface Collection {
+ Status: number[];
+ TagIds: number[];
+ GroupId: number[];
+ }
- const { isLoading, environments, totalCount, totalAvailable } =
- useEnvironmentList({ page, pageLimit, search: debouncedTextFilter }, true);
+ const PlatformOptions = [
+ { value: EnvironmentType.Docker, label: 'Docker' },
+ { value: EnvironmentType.Azure, label: 'Azure' },
+ { value: EnvironmentType.KubernetesLocal, label: 'Kubernetes' },
+ ];
+
+ const status = [
+ { value: EnvironmentStatus.Up, label: 'Up' },
+ { value: EnvironmentStatus.Down, label: 'Down' },
+ ];
+
+ const SortByOptions = [
+ { value: 1, label: 'Name' },
+ { value: 2, label: 'Group' },
+ { value: 3, label: 'Status' },
+ ];
+
+ const groupOptions = [...(groupsQuery.data || [])];
+ const uniqueGroup = [
+ ...new Map(groupOptions.map((item) => [item.Id, item])).values(),
+ ].map(({ Id: value, Name: label }) => ({
+ value,
+ label,
+ }));
+
+ const alltags = useTags();
+ const tagOptions = [...(alltags.tags || [])];
+ const uniqueTag = [
+ ...new Map(tagOptions.map((item) => [item.ID, item])).values(),
+ ].map(({ ID: value, Name: label }) => ({
+ value,
+ label,
+ }));
+
+ const collection = {
+ Status: statusFilter,
+ TagIds: tagFilter,
+ GroupId: groupFilter,
+ };
+
+ function multiPropsFilter(
+ environments: Environment[],
+ collection: Collection,
+ sortByFilter: string,
+ sortByDescending: boolean
+ ) {
+ const filterKeys = Object.keys(collection);
+ const filterResult = environments.filter((environment: Environment) =>
+ filterKeys.every((key) => {
+ if (!collection[key as keyof Collection].length) return true;
+ if (Array.isArray(environment[key as keyof Collection])) {
+ return (environment[key as keyof Collection] as number[]).some(
+ (keyEle) => collection[key as keyof Collection].includes(keyEle)
+ );
+ }
+ return collection[key as keyof Collection].includes(
+ environment[key as keyof Collection] as number
+ );
+ })
+ );
+
+ switch (sortByFilter) {
+ case 'Name':
+ return sortByDescending
+ ? filterResult.sort((a, b) =>
+ b.Name.toUpperCase() > a.Name.toUpperCase() ? 1 : -1
+ )
+ : filterResult.sort((a, b) =>
+ a.Name.toUpperCase() > b.Name.toUpperCase() ? 1 : -1
+ );
+ case 'Group':
+ return sortByDescending
+ ? filterResult.sort((a, b) => b.GroupId - a.GroupId)
+ : filterResult.sort((a, b) => a.GroupId - b.GroupId);
+ case 'Status':
+ return sortByDescending
+ ? filterResult.sort((a, b) => b.Status - a.Status)
+ : filterResult.sort((a, b) => a.Status - b.Status);
+ case 'None':
+ return filterResult;
+ default:
+ return filterResult;
+ }
+ }
+
+ const filteredEnvironments: Environment[] = multiPropsFilter(
+ environments,
+ collection,
+ sortByFilter,
+ sortByDescending
+ );
+
+ function platformOnChange(filterOptions: Filter[]) {
+ setPlatformState(filterOptions);
+ const dockerBaseType = EnvironmentType.Docker;
+ const kubernetesBaseType = EnvironmentType.KubernetesLocal;
+ const dockerRelateType = [
+ EnvironmentType.AgentOnDocker,
+ EnvironmentType.EdgeAgentOnDocker,
+ ];
+ const kubernetesRelateType = [
+ EnvironmentType.AgentOnKubernetes,
+ EnvironmentType.EdgeAgentOnKubernetes,
+ ];
+
+ let finalFilterEnvironment: number[] = [];
+
+ if (filterOptions.length === 0) {
+ setPlatformType(allEnvironmentType);
+ } else {
+ const filteredEnvironment = [
+ ...new Set(
+ filterOptions.map(
+ (filterOptions: { value: number }) => filterOptions.value
+ )
+ ),
+ ];
+ if (filteredEnvironment.includes(dockerBaseType)) {
+ finalFilterEnvironment = [...filteredEnvironment, ...dockerRelateType];
+ }
+ if (filteredEnvironment.includes(kubernetesBaseType)) {
+ finalFilterEnvironment = [
+ ...filteredEnvironment,
+ ...kubernetesRelateType,
+ ];
+ }
+ setPlatformType(finalFilterEnvironment);
+ }
+ }
+
+ function statusOnChange(filterOptions: Filter[]) {
+ setStatusState(filterOptions);
+ if (filterOptions.length === 0) {
+ setStatusFilter([]);
+ } else {
+ const filteredStatus = [
+ ...new Set(
+ filterOptions.map(
+ (filterOptions: { value: number }) => filterOptions.value
+ )
+ ),
+ ];
+ setStatusFilter(filteredStatus);
+ }
+ }
+
+ function groupOnChange(filterOptions: Filter[]) {
+ setGroupState(filterOptions);
+ if (filterOptions.length === 0) {
+ setGroupFilter([]);
+ } else {
+ const filteredGroups = [
+ ...new Set(
+ filterOptions.map(
+ (filterOptions: { value: number }) => filterOptions.value
+ )
+ ),
+ ];
+ setGroupFilter(filteredGroups);
+ }
+ }
+
+ function tagOnChange(filterOptions: Filter[]) {
+ setTagState(filterOptions);
+ if (filterOptions.length === 0) {
+ setTagFilter([]);
+ } else {
+ const filteredTags = [
+ ...new Set(
+ filterOptions.map(
+ (filterOptions: { value: number }) => filterOptions.value
+ )
+ ),
+ ];
+ setTagFilter(filteredTags);
+ }
+ }
+
+ function clearFilter() {
+ setSearchBarValue('');
+ setPlatformState([]);
+ setPlatformType(allEnvironmentType);
+ setStatusState([]);
+ setStatusFilter([]);
+ setTagState([]);
+ setTagFilter([]);
+ setGroupState([]);
+ setGroupFilter([]);
+ }
+
+ function sortOnchange(filterOptions: Filter) {
+ if (filterOptions !== null) {
+ setSortByFilter(filterOptions.label);
+ setSortByButton(true);
+ } else {
+ setSortByFilter('None');
+ setSortByButton(false);
+ }
+ }
+
+ function sortOndescending() {
+ setSortByDescending(!sortByDescending);
+ }
return (
<>
@@ -76,19 +318,68 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+