diff --git a/ui/src/app/applications/components/applications-list/applications-filter.tsx b/ui/src/app/applications/components/applications-list/applications-filter.tsx index 0bdcfe33f2fda..98e8342f77fdf 100644 --- a/ui/src/app/applications/components/applications-list/applications-filter.tsx +++ b/ui/src/app/applications/components/applications-list/applications-filter.tsx @@ -1,10 +1,41 @@ import {ActionButton, debounce, useData} from 'argo-ui/v2'; +import * as minimatch from 'minimatch'; import * as React from 'react'; import {Application, ApplicationDestination, Cluster, HealthStatusCode, HealthStatuses, SyncStatusCode, SyncStatuses} from '../../../shared/models'; import {AppsListPreferences, services} from '../../../shared/services'; import {Filter} from '../filter/filter'; +import * as LabelSelector from '../label-selector'; import {ComparisonStatusIcon, HealthStatusIcon} from '../utils'; +export interface FilterResult { + projects: boolean; + repos: boolean; + sync: boolean; + health: boolean; + namespaces: boolean; + clusters: boolean; + labels: boolean; +} + +export interface FilteredApp extends Application { + filterResult: FilterResult; +} + +export function getFilterResults(applications: Application[], pref: AppsListPreferences): FilteredApp[] { + return applications.map(app => ({ + ...app, + filterResult: { + projects: pref.projectsFilter.length === 0 || pref.projectsFilter.includes(app.spec.project), + repos: pref.reposFilter.length === 0 || pref.reposFilter.includes(app.spec.source.repoURL), + sync: pref.syncFilter.length === 0 || pref.syncFilter.includes(app.status.sync.status), + health: pref.healthFilter.length === 0 || pref.healthFilter.includes(app.status.health.status), + namespaces: pref.namespacesFilter.length === 0 || pref.namespacesFilter.some(ns => app.spec.destination.namespace && minimatch(app.spec.destination.namespace, ns)), + clusters: pref.clustersFilter.length === 0 || pref.clustersFilter.some(server => server === (app.spec.destination.server || app.spec.destination.name)), + labels: pref.labelsFilter.length === 0 || pref.labelsFilter.every(selector => LabelSelector.match(selector, app.metadata.labels)) + } + })); +} + const optionsFrom = (options: string[], filter: string[]) => { return options .filter(s => filter.indexOf(s) === -1) @@ -14,22 +45,25 @@ const optionsFrom = (options: string[], filter: string[]) => { }; interface AppFilterProps { - apps: Application[]; + apps: FilteredApp[]; pref: AppsListPreferences; onChange: (newPrefs: AppsListPreferences) => void; } -const getCounts = (apps: Application[], filter: (app: Application) => string, init?: string[]) => { +const getCounts = (apps: FilteredApp[], filterType: keyof FilterResult, filter: (app: Application) => string, init?: string[]) => { const map = new Map(); if (init) { init.forEach(key => map.set(key, 0)); } - apps.filter(filter).forEach(app => map.set(filter(app), (map.get(filter(app)) || 0) + 1)); + // filter out all apps that does not match other filters and ignore this filter result + apps.filter(app => filter(app) && Object.keys(app.filterResult).every((key: keyof FilterResult) => key === filterType || app.filterResult[key])).forEach(app => + map.set(filter(app), (map.get(filter(app)) || 0) + 1) + ); return map; }; -const getOptions = (apps: Application[], filter: (app: Application) => string, keys: string[], getIcon?: (k: string) => React.ReactNode) => { - const counts = getCounts(apps, filter, keys); +const getOptions = (apps: FilteredApp[], filterType: keyof FilterResult, filter: (app: Application) => string, keys: string[], getIcon?: (k: string) => React.ReactNode) => { + const counts = getCounts(apps, filterType, filter, keys); return keys.map(k => { return { label: k, @@ -44,7 +78,7 @@ const SyncFilter = (props: AppFilterProps) => ( label='SYNC STATUS' selected={props.pref.syncFilter} setSelected={s => props.onChange({...props.pref, syncFilter: s})} - options={getOptions(props.apps, app => app.status.sync.status, Object.keys(SyncStatuses), s => ( + options={getOptions(props.apps, 'sync', app => app.status.sync.status, Object.keys(SyncStatuses), s => ( ))} /> @@ -55,7 +89,7 @@ const HealthFilter = (props: AppFilterProps) => ( label='HEALTH STATUS' selected={props.pref.healthFilter} setSelected={s => props.onChange({...props.pref, healthFilter: s})} - options={getOptions(props.apps, app => app.status.health.status, Object.keys(HealthStatuses), s => ( + options={getOptions(props.apps, 'health', app => app.status.health.status, Object.keys(HealthStatuses), s => ( ))} /> diff --git a/ui/src/app/applications/components/applications-list/applications-list.tsx b/ui/src/app/applications/components/applications-list/applications-list.tsx index ee8e49b4feb31..6f71a2cf0dcc8 100644 --- a/ui/src/app/applications/components/applications-list/applications-list.tsx +++ b/ui/src/app/applications/components/applications-list/applications-list.tsx @@ -1,6 +1,5 @@ import {Autocomplete, ErrorNotification, MockupList, NotificationType, SlidingPanel, Toolbar} from 'argo-ui'; import * as classNames from 'classnames'; -import * as minimatch from 'minimatch'; import * as React from 'react'; import {Key, KeybindingContext, KeybindingProvider} from 'react-keyhooks'; import {RouteComponentProps} from 'react-router'; @@ -13,9 +12,8 @@ import {AppsListPreferences, AppsListViewType, services} from '../../../shared/s import {ApplicationCreatePanel} from '../application-create-panel/application-create-panel'; import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-panel'; import {ApplicationsSyncPanel} from '../applications-sync-panel/applications-sync-panel'; -import * as LabelSelector from '../label-selector'; import * as AppUtils from '../utils'; -import {ApplicationsFilter} from './applications-filter'; +import {ApplicationsFilter, FilteredApp, getFilterResults} from './applications-filter'; import {ApplicationsSummary} from './applications-summary'; import {ApplicationsTable} from './applications-table'; import {ApplicationTiles} from './applications-tiles'; @@ -142,18 +140,12 @@ const ViewPref = ({children}: {children: (pref: AppsListPreferences & {page: num ); -function filterApps(applications: models.Application[], pref: AppsListPreferences, search: string) { - return applications.filter( - app => - (search === '' || app.metadata.name.includes(search)) && - (pref.projectsFilter.length === 0 || pref.projectsFilter.includes(app.spec.project)) && - (pref.reposFilter.length === 0 || pref.reposFilter.includes(app.spec.source.repoURL)) && - (pref.syncFilter.length === 0 || pref.syncFilter.includes(app.status.sync.status)) && - (pref.healthFilter.length === 0 || pref.healthFilter.includes(app.status.health.status)) && - (pref.namespacesFilter.length === 0 || pref.namespacesFilter.some(ns => app.spec.destination.namespace && minimatch(app.spec.destination.namespace, ns))) && - (pref.clustersFilter.length === 0 || pref.clustersFilter.some(server => server === (app.spec.destination.server || app.spec.destination.name))) && - (pref.labelsFilter.length === 0 || pref.labelsFilter.every(selector => LabelSelector.match(selector, app.metadata.labels))) - ); +function filterApps(applications: models.Application[], pref: AppsListPreferences, search: string): {filteredApps: models.Application[]; filterResults: FilteredApp[]} { + const filterResults = getFilterResults(applications, pref); + return { + filterResults, + filteredApps: filterResults.filter(app => (search === '' || app.metadata.name.includes(search)) && Object.values(app.filterResult).every(val => val)) + }; } function tryJsonParse(input: string) { @@ -384,7 +376,7 @@ export const ApplicationsList = (props: RouteComponentProps<{}>) => {
{pref => { - const filteredApps = filterApps(applications, pref, pref.search); + const {filteredApps, filterResults} = filterApps(applications, pref, pref.search); return applications.length === 0 && (pref.labelsFilter || []).length === 0 ? (

No applications yet

@@ -399,7 +391,7 @@ export const ApplicationsList = (props: RouteComponentProps<{}>) => { ) : (
- onFilterPrefChanged(ctx, newPrefs)} pref={pref} /> + onFilterPrefChanged(ctx, newPrefs)} pref={pref} /> {syncAppsInput && (