Skip to content

Commit 9675487

Browse files
authored
feat(ui): integrate Progressive Sync feature (#22781)
Signed-off-by: Atif Ali <[email protected]>
1 parent db6ece9 commit 9675487

File tree

4 files changed

+184
-3
lines changed

4 files changed

+184
-3
lines changed

ui/src/app/applications/components/application-status-panel/application-status-panel.tsx

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import {
1212
getAppDefaultSyncRevisionExtra,
1313
getAppOperationState,
1414
HydrateOperationPhaseIcon,
15-
hydrationStatusMessage
15+
hydrationStatusMessage,
16+
getProgressiveSyncStatusColor,
17+
getProgressiveSyncStatusIcon
1618
} from '../utils';
1719
import {getConditionCategory, HealthStatusIcon, OperationState, syncStatusMessage, getAppDefaultSyncRevision, getAppDefaultOperationSyncRevision} from '../utils';
1820
import {RevisionMetadataPanel} from './revision-metadata-panel';
1921
import * as utils from '../utils';
22+
import {COLORS} from '../../../shared/components/colors';
2023

2124
import './application-status-panel.scss';
2225

@@ -55,7 +58,74 @@ const sectionHeader = (info: SectionInfo, onClick?: () => any) => {
5558
);
5659
};
5760

61+
const hasRollingSyncEnabled = (application: models.Application): boolean => {
62+
return application.metadata.ownerReferences?.some(ref => ref.kind === 'ApplicationSet') || false;
63+
};
64+
65+
const ProgressiveSyncStatus = ({application}: {application: models.Application}) => {
66+
if (!hasRollingSyncEnabled(application)) {
67+
return null;
68+
}
69+
70+
const appSetRef = application.metadata.ownerReferences.find(ref => ref.kind === 'ApplicationSet');
71+
if (!appSetRef) {
72+
return null;
73+
}
74+
75+
return (
76+
<DataLoader
77+
input={application}
78+
load={async () => {
79+
const appSet = await services.applications.getApplicationSet(appSetRef.name, application.metadata.namespace);
80+
return appSet?.spec?.strategy?.type === 'RollingSync' ? appSet : null;
81+
}}>
82+
{(appSet: models.ApplicationSet) => {
83+
if (!appSet) {
84+
return (
85+
<div className='application-status-panel__item'>
86+
{sectionHeader({
87+
title: 'PROGRESSIVE SYNC',
88+
helpContent: 'Shows the current status of progressive sync for applications managed by an ApplicationSet with RollingSync strategy.'
89+
})}
90+
<div className='application-status-panel__item-value'>
91+
<i className='fa fa-question-circle' style={{color: COLORS.sync.unknown}} /> Unknown
92+
</div>
93+
</div>
94+
);
95+
}
96+
97+
// Get the current application's status from the ApplicationSet resources
98+
const appResource = appSet.status?.applicationStatus?.find(status => status.application === application.metadata.name);
99+
100+
return (
101+
<div className='application-status-panel__item'>
102+
{sectionHeader({
103+
title: 'PROGRESSIVE SYNC',
104+
helpContent: 'Shows the current status of progressive sync for applications managed by an ApplicationSet with RollingSync strategy.'
105+
})}
106+
<div className='application-status-panel__item-value' style={{color: getProgressiveSyncStatusColor(appResource.status)}}>
107+
{getProgressiveSyncStatusIcon({status: appResource.status})}&nbsp;{appResource.status}
108+
</div>
109+
<div className='application-status-panel__item-value'>Wave: {appResource.step}</div>
110+
<div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}>
111+
Last Transition: <br />
112+
<Timestamp date={appResource.lastTransitionTime} />
113+
</div>
114+
{appResource.message && <div className='application-status-panel__item-name'>{appResource.message}</div>}
115+
</div>
116+
);
117+
}}
118+
</DataLoader>
119+
);
120+
};
121+
58122
export const ApplicationStatusPanel = ({application, showDiff, showOperation, showHydrateOperation, showConditions, showExtension, showMetadataInfo}: Props) => {
123+
const [showProgressiveSync, setShowProgressiveSync] = React.useState(false);
124+
125+
React.useEffect(() => {
126+
setShowProgressiveSync(hasRollingSyncEnabled(application));
127+
}, [application]);
128+
59129
const today = new Date();
60130

61131
let daysSinceLastSynchronized = 0;
@@ -82,6 +152,7 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh
82152
const errors = cntByCategory.get('error');
83153
const source = getAppDefaultSource(application);
84154
const hasMultipleSources = application.spec.sources?.length > 0;
155+
85156
return (
86157
<div className='application-status-panel row'>
87158
<div className='application-status-panel__item'>
@@ -261,6 +332,7 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh
261332
</React.Fragment>
262333
)}
263334
</DataLoader>
335+
{showProgressiveSync && <ProgressiveSyncStatus application={application} />}
264336
{statusExtensions && statusExtensions.map(ext => <ext.component key={ext.title} application={application} openFlyout={() => showExtension && showExtension(ext.id)} />)}
265337
</div>
266338
);

ui/src/app/applications/components/utils.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,3 +1685,43 @@ export function getAppUrl(app: appModels.Application): string {
16851685
}
16861686
return `applications/${app.metadata.namespace}/${app.metadata.name}`;
16871687
}
1688+
1689+
export const getProgressiveSyncStatusIcon = ({status, isButton}: {status: string; isButton?: boolean}) => {
1690+
const getIconProps = () => {
1691+
switch (status) {
1692+
case 'Healthy':
1693+
return {icon: 'fa-check-circle', color: COLORS.health.healthy};
1694+
case 'Progressing':
1695+
return {icon: 'fa-circle-notch fa-spin', color: COLORS.health.progressing};
1696+
case 'Pending':
1697+
return {icon: 'fa-clock', color: COLORS.health.degraded};
1698+
case 'Waiting':
1699+
return {icon: 'fa-clock', color: COLORS.sync.out_of_sync};
1700+
case 'Error':
1701+
return {icon: 'fa-times-circle', color: COLORS.health.degraded};
1702+
default:
1703+
return {icon: 'fa-question-circle', color: COLORS.sync.unknown};
1704+
}
1705+
};
1706+
1707+
const {icon, color} = getIconProps();
1708+
const className = `fa ${icon}${isButton ? ' application-status-panel__item-value__status-button' : ''}`;
1709+
return <i className={className} style={{color}} />;
1710+
};
1711+
1712+
export const getProgressiveSyncStatusColor = (status: string): string => {
1713+
switch (status) {
1714+
case 'Waiting':
1715+
return COLORS.sync.out_of_sync;
1716+
case 'Pending':
1717+
return COLORS.health.degraded;
1718+
case 'Progressing':
1719+
return COLORS.health.progressing;
1720+
case 'Healthy':
1721+
return COLORS.health.healthy;
1722+
case 'Error':
1723+
return COLORS.health.degraded;
1724+
default:
1725+
return COLORS.sync.unknown;
1726+
}
1727+
};

ui/src/app/shared/models.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,17 @@ export interface Operation {
6262
initiatedBy: OperationInitiator;
6363
}
6464

65-
export type OperationPhase = 'Running' | 'Error' | 'Failed' | 'Succeeded' | 'Terminating';
65+
export type OperationPhase = 'Running' | 'Error' | 'Failed' | 'Succeeded' | 'Terminating' | 'Progressing' | 'Pending' | 'Waiting';
6666

6767
export const OperationPhases = {
6868
Running: 'Running' as OperationPhase,
6969
Failed: 'Failed' as OperationPhase,
7070
Error: 'Error' as OperationPhase,
7171
Succeeded: 'Succeeded' as OperationPhase,
72-
Terminating: 'Terminating' as OperationPhase
72+
Terminating: 'Terminating' as OperationPhase,
73+
Progressing: 'Progressing' as OperationPhase,
74+
Pending: 'Pending' as OperationPhase,
75+
Waiting: 'Waiting' as OperationPhase
7376
};
7477

7578
/**
@@ -1050,3 +1053,60 @@ export interface UserMessages {
10501053
}
10511054

10521055
export const AppDeletionConfirmedAnnotation = 'argocd.argoproj.io/deletion-approved';
1056+
1057+
export interface ApplicationSetSpec {
1058+
strategy?: {
1059+
type: 'AllAtOnce' | 'RollingSync';
1060+
rollingSync?: {
1061+
steps: Array<{
1062+
matchExpressions: Array<{
1063+
key: string;
1064+
operator: string;
1065+
values: string[];
1066+
}>;
1067+
maxUpdate: number;
1068+
}>;
1069+
};
1070+
};
1071+
}
1072+
1073+
export interface ApplicationSetCondition {
1074+
type: string;
1075+
status: string;
1076+
message: string;
1077+
lastTransitionTime: string;
1078+
reason: string;
1079+
}
1080+
1081+
export interface ApplicationSetResource {
1082+
group: string;
1083+
version: string;
1084+
kind: string;
1085+
name: string;
1086+
namespace: string;
1087+
status: string;
1088+
health?: {
1089+
status: string;
1090+
lastTransitionTime: models.Time;
1091+
};
1092+
labels?: {[key: string]: string};
1093+
}
1094+
1095+
export interface ApplicationSet {
1096+
apiVersion?: string;
1097+
kind?: string;
1098+
metadata: models.ObjectMeta;
1099+
spec: ApplicationSetSpec;
1100+
status?: {
1101+
conditions?: ApplicationSetCondition[];
1102+
applicationStatus?: Array<{
1103+
application: string;
1104+
status: 'Waiting' | 'Pending' | 'Progressing' | 'Healthy';
1105+
message?: string;
1106+
lastTransitionTime?: string;
1107+
step?: string;
1108+
targetRevisions?: string[];
1109+
}>;
1110+
resources?: ApplicationSetResource[];
1111+
};
1112+
}

ui/src/app/shared/services/applications-service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ function optionsToSearch(options?: QueryOptions) {
2121
}
2222

2323
export class ApplicationsService {
24+
constructor() {}
25+
2426
public list(projects: string[], options?: QueryOptions): Promise<models.ApplicationList> {
2527
return requests
2628
.get('/applications')
@@ -530,4 +532,11 @@ export class ApplicationsService {
530532

531533
return data as models.Application;
532534
}
535+
536+
public async getApplicationSet(name: string, namespace: string): Promise<models.ApplicationSet> {
537+
return requests
538+
.get(`/applicationsets/${name}`)
539+
.query({namespace})
540+
.then(res => res.body as models.ApplicationSet);
541+
}
533542
}

0 commit comments

Comments
 (0)