feat: Add application filter for operation status to UI (#25636)

Signed-off-by: Dylan Schlager <dylan.schlager@lattice.com>
This commit is contained in:
Dylan Schlager 2025-12-17 18:54:18 -05:00 committed by GitHub
parent 216611ff3b
commit b414432ddb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 108 additions and 14 deletions

View file

@ -208,6 +208,7 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Kurly](https://www.kurly.com/)
1. [Kvist](https://kvistsolutions.com)
1. [Kyriba](https://www.kyriba.com/)
1. [Lattice](https://lattice.com)
1. [LeFigaro](https://www.lefigaro.fr/)
1. [Lely](https://www.lely.com/)
1. [LexisNexis](https://www.lexisnexis.com/)

View file

@ -2,11 +2,22 @@ import {useData, Checkbox} from 'argo-ui/v2';
import * as minimatch from 'minimatch';
import * as React from 'react';
import {Context} from '../../../shared/context';
import {Application, ApplicationDestination, Cluster, HealthStatusCode, HealthStatuses, SyncPolicy, SyncStatusCode, SyncStatuses} from '../../../shared/models';
import {
Application,
ApplicationDestination,
Cluster,
HealthStatusCode,
HealthStatuses,
OperationStateTitle,
OperationStateTitles,
SyncPolicy,
SyncStatusCode,
SyncStatuses
} from '../../../shared/models';
import {AppsListPreferences, services} from '../../../shared/services';
import {Filter, FiltersGroup} from '../filter/filter';
import * as LabelSelector from '../label-selector';
import {ComparisonStatusIcon, getAppDefaultSource, HealthStatusIcon} from '../utils';
import {ComparisonStatusIcon, getAppDefaultSource, HealthStatusIcon, getOperationStateTitle} from '../utils';
import {formatClusterQueryParam} from '../../../shared/utils';
import {COLORS} from '../../../shared/components/colors';
@ -19,6 +30,7 @@ export interface FilterResult {
clusters: boolean;
favourite: boolean;
labels: boolean;
operation: boolean;
}
export interface FilteredApp extends Application {
@ -54,7 +66,8 @@ export function getFilterResults(applications: Application[], pref: AppsListPref
return (inputMatch && inputMatch[0] === app.spec.destination.server) || (app.spec.destination.name && minimatch(app.spec.destination.name, filterString));
}
}),
labels: pref.labelsFilter.length === 0 || pref.labelsFilter.every(selector => LabelSelector.match(selector, app.metadata.labels))
labels: pref.labelsFilter.length === 0 || pref.labelsFilter.every(selector => LabelSelector.match(selector, app.metadata.labels)),
operation: pref.operationFilter.length === 0 || pref.operationFilter.includes(getOperationStateTitle(app))
}
}));
}
@ -275,12 +288,64 @@ const AutoSyncFilter = (props: AppFilterProps) => (
/>
);
function getOperationOptions(apps: FilteredApp[]) {
const operationStateTitles = Object.values(OperationStateTitles);
const counts = getCounts(apps, 'operation', app => getOperationStateTitle(app), operationStateTitles) as Map<OperationStateTitle, number>;
/**
* Combine syncing, terminated (terminating), and deleting counts into a single count for syncing status.
* Deleting and Terminating are considered syncing statuses. Terminating operations will always end in a
* failed or error state.
*/
const combinedSyncingCount = counts.get(OperationStateTitles.Syncing) + counts.get(OperationStateTitles.Terminated) + counts.get(OperationStateTitles.Deleting);
return [
{
label: OperationStateTitles.Syncing,
icon: <i className='fa fa-circle-notch' style={{color: COLORS.operation.running}} />,
count: combinedSyncingCount
},
{
label: OperationStateTitles.SyncOK,
icon: <i className='fa fa-check-circle' style={{color: COLORS.operation.success}} />,
count: counts.get(OperationStateTitles.SyncOK)
},
{
label: OperationStateTitles.SyncError,
icon: <i className='fa fa-exclamation-circle' style={{color: COLORS.operation.error}} />,
count: counts.get(OperationStateTitles.SyncError)
},
{
label: OperationStateTitles.SyncFailed,
icon: <i className='fa fa-times-circle' style={{color: COLORS.operation.failed}} />,
count: counts.get(OperationStateTitles.SyncFailed)
},
{
label: OperationStateTitles.Unknown,
icon: <i className='fa fa-question-circle' style={{color: COLORS.health.unknown}} />,
count: counts.get(OperationStateTitles.Unknown)
}
];
}
const OperationFilter = (props: AppFilterProps) => (
<Filter
label='OPERATION STATUS'
selected={props.pref.operationFilter}
setSelected={s => props.onChange({...props.pref, operationFilter: s})}
options={getOperationOptions(props.apps)}
collapsed={props.collapsed || false}
/>
);
export const ApplicationsFilter = (props: AppFilterProps) => {
return (
<FiltersGroup title='Application filters' content={props.children} collapsed={props.collapsed}>
<FavoriteFilter {...props} />
<SyncFilter {...props} />
<HealthFilter {...props} />
<OperationFilter {...props} />
<LabelsFilter {...props} />
<ProjectFilter {...props} />
<ClusterFilter {...props} />

View file

@ -122,6 +122,12 @@ const ViewPref = ({children}: {children: (pref: AppsListPreferences & {page: num
.split(',')
.filter(item => !!item);
}
if (params.get('operation') != null) {
viewPref.operationFilter = params
.get('operation')
.split(',')
.filter(item => !!item);
}
if (params.get('health') != null) {
viewPref.healthFilter = params
.get('health')
@ -423,7 +429,8 @@ export const ApplicationsList = (props: RouteComponentProps<any> & {objectListKi
health: newPref.healthFilter.join(','),
namespace: newPref.namespacesFilter.join(','),
cluster: newPref.clustersFilter.join(','),
labels: newPref.labelsFilter.map(encodeURIComponent).join(',')
labels: newPref.labelsFilter.map(encodeURIComponent).join(','),
operation: newPref.operationFilter.join(',')
},
{replace: true}
);

View file

@ -1212,7 +1212,7 @@ export function getOperationType(application: appModels.Application) {
return 'Unknown';
}
const getOperationStateTitle = (app: appModels.Application) => {
export const getOperationStateTitle = (app: appModels.Application): appModels.OperationStateTitle => {
const appOperationState = getAppOperationState(app);
const operationType = getOperationType(app);
switch (operationType) {

View file

@ -90,6 +90,18 @@ export interface OperationState {
finishedAt: models.Time;
}
export type OperationStateTitle = 'Deleting' | 'Syncing' | 'Sync error' | 'Sync failed' | 'Sync OK' | 'Terminated' | 'Unknown';
export const OperationStateTitles = {
Deleting: 'Deleting',
Syncing: 'Syncing',
SyncError: 'Sync error',
SyncFailed: 'Sync failed',
SyncOK: 'Sync OK',
Terminated: 'Terminated',
Unknown: 'Unknown'
} satisfies Record<string, OperationStateTitle>;
export type HookType = 'PreSync' | 'Sync' | 'PostSync' | 'SyncFail' | 'Skip';
export interface RevisionMetadata {

View file

@ -85,15 +85,21 @@ export class AbstractAppsListPreferences {
export class AppsListPreferences extends AbstractAppsListPreferences {
public static countEnabledFilters(pref: AppsListPreferences) {
return [pref.clustersFilter, pref.healthFilter, pref.labelsFilter, pref.namespacesFilter, pref.projectsFilter, pref.reposFilter, pref.syncFilter].reduce(
(count, filter) => {
if (filter && filter.length > 0) {
return count + 1;
}
return count;
},
0
);
return [
pref.clustersFilter,
pref.healthFilter,
pref.labelsFilter,
pref.namespacesFilter,
pref.projectsFilter,
pref.reposFilter,
pref.syncFilter,
pref.operationFilter
].reduce((count, filter) => {
if (filter && filter.length > 0) {
return count + 1;
}
return count;
}, 0);
}
public static clearFilters(pref: AppsListPreferences) {
@ -105,6 +111,7 @@ export class AppsListPreferences extends AbstractAppsListPreferences {
pref.reposFilter = [];
pref.syncFilter = [];
pref.autoSyncFilter = [];
pref.operationFilter = [];
}
public projectsFilter: string[];
@ -113,6 +120,7 @@ export class AppsListPreferences extends AbstractAppsListPreferences {
public autoSyncFilter: string[];
public namespacesFilter: string[];
public clustersFilter: string[];
public operationFilter: string[];
}
export class AppSetsListPreferences extends AbstractAppsListPreferences {
@ -179,6 +187,7 @@ const DEFAULT_PREFERENCES: ViewPreferences = {
syncFilter: new Array<string>(),
autoSyncFilter: new Array<string>(),
healthFilter: new Array<string>(),
operationFilter: new Array<string>(),
hideFilters: false,
showFavorites: false,
favoritesAppList: new Array<string>(),