Skip to content

Commit

Permalink
fix: Application list page filter counts are confusing (#6625) (#6626)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Matyushentsev <[email protected]>
  • Loading branch information
Alexander Matyushentsev authored Jul 3, 2021
1 parent ba4c655 commit f12650c
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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<string, number>();
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,
Expand All @@ -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 => (
<ComparisonStatusIcon status={s as SyncStatusCode} noSpin={true} />
))}
/>
Expand All @@ -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 => (
<HealthStatusIcon state={{status: s as HealthStatusCode, message: ''}} noSpin={true} />
))}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -142,18 +140,12 @@ const ViewPref = ({children}: {children: (pref: AppsListPreferences & {page: num
</ObservableQuery>
);

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) {
Expand Down Expand Up @@ -384,7 +376,7 @@ export const ApplicationsList = (props: RouteComponentProps<{}>) => {
<div className='applications-list'>
<ViewPref>
{pref => {
const filteredApps = filterApps(applications, pref, pref.search);
const {filteredApps, filterResults} = filterApps(applications, pref, pref.search);
return applications.length === 0 && (pref.labelsFilter || []).length === 0 ? (
<EmptyState icon='argo-icon-application'>
<h4>No applications yet</h4>
Expand All @@ -399,7 +391,7 @@ export const ApplicationsList = (props: RouteComponentProps<{}>) => {
) : (
<div className='row'>
<div className='columns small-12 xxlarge-2'>
<ApplicationsFilter apps={applications} onChange={newPrefs => onFilterPrefChanged(ctx, newPrefs)} pref={pref} />
<ApplicationsFilter apps={filterResults} onChange={newPrefs => onFilterPrefChanged(ctx, newPrefs)} pref={pref} />
{syncAppsInput && (
<ApplicationsSyncPanel
key='syncsPanel'
Expand Down

0 comments on commit f12650c

Please sign in to comment.