diff --git a/USERS.md b/USERS.md index fb965e34d2..414d75bde8 100644 --- a/USERS.md +++ b/USERS.md @@ -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/) 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 a2d930d837..544c1a1728 100644 --- a/ui/src/app/applications/components/applications-list/applications-filter.tsx +++ b/ui/src/app/applications/components/applications-list/applications-filter.tsx @@ -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; + + /** + * 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: , + count: combinedSyncingCount + }, + { + label: OperationStateTitles.SyncOK, + icon: , + count: counts.get(OperationStateTitles.SyncOK) + }, + { + label: OperationStateTitles.SyncError, + icon: , + count: counts.get(OperationStateTitles.SyncError) + }, + { + label: OperationStateTitles.SyncFailed, + icon: , + count: counts.get(OperationStateTitles.SyncFailed) + }, + { + label: OperationStateTitles.Unknown, + icon: , + count: counts.get(OperationStateTitles.Unknown) + } + ]; +} + +const OperationFilter = (props: AppFilterProps) => ( + props.onChange({...props.pref, operationFilter: s})} + options={getOperationOptions(props.apps)} + collapsed={props.collapsed || false} + /> +); + export const ApplicationsFilter = (props: AppFilterProps) => { return ( + 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 f8f41615d7..17a13119ce 100644 --- a/ui/src/app/applications/components/applications-list/applications-list.tsx +++ b/ui/src/app/applications/components/applications-list/applications-list.tsx @@ -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 & {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} ); diff --git a/ui/src/app/applications/components/utils.tsx b/ui/src/app/applications/components/utils.tsx index 84698b5a6f..719aab3540 100644 --- a/ui/src/app/applications/components/utils.tsx +++ b/ui/src/app/applications/components/utils.tsx @@ -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) { diff --git a/ui/src/app/shared/models.ts b/ui/src/app/shared/models.ts index 77866efaa8..4a8f4f542d 100644 --- a/ui/src/app/shared/models.ts +++ b/ui/src/app/shared/models.ts @@ -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; + export type HookType = 'PreSync' | 'Sync' | 'PostSync' | 'SyncFail' | 'Skip'; export interface RevisionMetadata { diff --git a/ui/src/app/shared/services/view-preferences-service.ts b/ui/src/app/shared/services/view-preferences-service.ts index b4be5136e6..cff4aeb74b 100644 --- a/ui/src/app/shared/services/view-preferences-service.ts +++ b/ui/src/app/shared/services/view-preferences-service.ts @@ -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(), autoSyncFilter: new Array(), healthFilter: new Array(), + operationFilter: new Array(), hideFilters: false, showFavorites: false, favoritesAppList: new Array(),