mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
feat: Add application filter for operation status to UI (#25636)
Signed-off-by: Dylan Schlager <dylan.schlager@lattice.com>
This commit is contained in:
parent
216611ff3b
commit
b414432ddb
6 changed files with 108 additions and 14 deletions
1
USERS.md
1
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/)
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue