Skip to content

Commit 7e17f29

Browse files
committed
Add Application Detail Pages and Tabs (#7267)
Signed-off-by: Keith Chong <kykchong@redhat.com>
1 parent b858b0b commit 7e17f29

34 files changed

+3284
-22
lines changed

console-extensions.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,24 @@
333333
}
334334
}
335335
},
336+
{
337+
"type": "console.page/resource/details",
338+
"flags": {
339+
"required": [
340+
"APPLICATION"
341+
]
342+
},
343+
"properties": {
344+
"model": {
345+
"group": "argoproj.io",
346+
"kind": "Application",
347+
"version": "v1alpha1"
348+
},
349+
"component": {
350+
"$codeRef": "ApplicationDetails"
351+
}
352+
}
353+
},
336354
{
337355
"type": "console.page/resource/list",
338356
"flags": {

plugin-metadata.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const metadata: ConsolePluginBuildMetadata = {
1414
"gitopsFlags": "./components/utils/flags",
1515
"topology": "./components/topology",
1616
ApplicationList: "./gitops/components/application/ApplicationListTab.tsx",
17+
ApplicationDetails: "./gitops/components/application/ApplicationNavPage.tsx",
1718
ApplicationSetList: "./gitops/components/application/ApplicationSetListTab.tsx",
1819
yamlApplicationTemplates: "./gitops/components/application/templates/index.ts"
1920
}

src/gitops/Revision/Revision.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@ interface RevisionProps {
77
repoURL: string;
88
revision: string;
99
helm: boolean;
10+
revisionExtra?: string;
1011
}
1112

12-
const Revision: React.FC<RevisionProps> = ({ repoURL, revision, helm }) => {
13+
const Revision: React.FC<RevisionProps> = ({ repoURL, revision, helm, revisionExtra }) => {
1314
if (revision) {
1415
return (
1516
<>
1617
{!helm && (
17-
<ExternalLink href={createRevisionURL(repoURL, revision)}>
18-
({revision.substring(0, 7) || ''})
19-
</ExternalLink>
18+
<span>
19+
<ExternalLink href={createRevisionURL(repoURL, revision)}>
20+
({revision.substring(0, 7) || ''})
21+
</ExternalLink>
22+
{revisionExtra && revisionExtra}
23+
</span>
2024
)}
21-
{helm && <span>{revision}</span>}
25+
{helm && <span>({revision.substring(0, 7) || ''})</span>}
2226
</>
2327
);
2428
} else {

src/gitops/Statuses/HealthStatus.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {
66
HealthProgressingIcon,
77
HealthSuspendedIcon,
88
HealthUnknownIcon,
9+
SpinningIcon,
910
} from 'src/gitops/utils/components/Icons/Icons';
1011
import { HealthStatus as HS } from 'src/gitops/utils/constants';
1112

13+
import { COLORS } from '@gitops/components/shared/colors';
1214
import { Button, Popover } from '@patternfly/react-core';
1315

1416
interface HealthProps {
@@ -64,4 +66,50 @@ const HealthStatus: React.FC<HealthProps> = ({ status, message }) => {
6466
return <div>{showStatus}</div>;
6567
};
6668

69+
export type HealthStatusCode =
70+
| 'Unknown'
71+
| 'Progressing'
72+
| 'Healthy'
73+
| 'Suspended'
74+
| 'Degraded'
75+
| 'Missing';
76+
77+
export interface HealthStatusModel {
78+
status: HealthStatusCode;
79+
message: string;
80+
}
81+
82+
export const HealthStatusIcon = ({ status }: { status: HealthStatusCode }) => {
83+
let color = COLORS.health.unknown;
84+
let icon = 'fa-question-circle';
85+
86+
switch (status) {
87+
case HS.HEALTHY:
88+
color = COLORS.health.healthy;
89+
icon = 'fa-heart';
90+
break;
91+
case HS.SUSPENDED:
92+
color = COLORS.health.suspended;
93+
icon = 'fa-pause-circle';
94+
break;
95+
case HS.DEGRADED:
96+
color = COLORS.health.degraded;
97+
icon = 'fa-heart-broken';
98+
break;
99+
case HS.PROGRESSING:
100+
color = COLORS.health.progressing;
101+
icon = `fa fa-circle-notch fa-spin`;
102+
break;
103+
case HS.MISSING:
104+
color = COLORS.health.missing;
105+
icon = 'fa-ghost';
106+
break;
107+
}
108+
return icon.includes('fa-spin') ? (
109+
<SpinningIcon color={color} title={status} />
110+
) : (
111+
<i title={status} className={'fa ' + icon + ' utils-health-status-icon'} style={{ color }} />
112+
);
113+
};
114+
67115
export default HealthStatus;
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import * as React from 'react';
2+
import { RouteComponentProps } from 'react-router';
3+
import classNames from 'classnames';
4+
5+
import {
6+
ApplicationKind,
7+
ApplicationModel,
8+
ApplicationSource,
9+
} from '@gitops/models/ApplicationModel';
10+
import Revision from '@gitops/Revision/Revision';
11+
import HealthStatus from '@gitops/Statuses/HealthStatus';
12+
import { OperationState } from '@gitops/Statuses/OperationState';
13+
import SyncStatus from '@gitops/Statuses/SyncStatus';
14+
import { ArgoServer, getArgoServer, getFriendlyClusterName } from '@gitops/utils/gitops';
15+
import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation';
16+
import { useObjectModifyPermissions } from '@gitops/utils/utils';
17+
import { k8sUpdate, ResourceLink, useK8sModel } from '@openshift-console/dynamic-plugin-sdk';
18+
import { Label as PfLabel, ToggleGroup, ToggleGroupItem } from '@patternfly/react-core';
19+
import {
20+
DescriptionList,
21+
Flex,
22+
FlexItem,
23+
PageSection,
24+
PageSectionVariants,
25+
Title,
26+
} from '@patternfly/react-core';
27+
28+
import { ArgoCDLink } from '../shared/ArgoCDLink/ArgoCDLink';
29+
import {
30+
BaseDetailsSummary,
31+
DetailsDescriptionGroup,
32+
} from '../shared/BaseDetailsSummary/BaseDetailsSummary';
33+
34+
import { ConditionsPopover } from './Conditions/ConditionsPopover';
35+
36+
type ApplicationDetailsTabProps = RouteComponentProps<{
37+
ns: string;
38+
name: string;
39+
}> & {
40+
obj?: ApplicationKind;
41+
};
42+
43+
const ApplicationDetailsTab: React.FC<ApplicationDetailsTabProps> = ({ obj }) => {
44+
const { t } = useGitOpsTranslation();
45+
const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' });
46+
47+
const [canPatch, canUpdate] = useObjectModifyPermissions(obj, ApplicationModel);
48+
49+
const [argoServer, setArgoServer] = React.useState<ArgoServer>({ host: '', protocol: '' });
50+
51+
React.useEffect(() => {
52+
(async () => {
53+
getArgoServer(model, obj)
54+
.then((server) => {
55+
setArgoServer(server);
56+
})
57+
.catch((err) => {
58+
console.error('Error:', err);
59+
});
60+
})();
61+
}, [model, obj]);
62+
63+
const onChangeAutomated = (event: React.MouseEvent<any> | React.KeyboardEvent | MouseEvent) => {
64+
const id = event.currentTarget.id;
65+
66+
switch (id) {
67+
case 'automated': {
68+
if (obj.spec.syncPolicy?.automated) {
69+
obj.spec.syncPolicy = {};
70+
} else {
71+
obj.spec.syncPolicy = { automated: {} };
72+
}
73+
break;
74+
}
75+
case 'self-heal': {
76+
if (obj.spec.syncPolicy.automated.selfHeal) {
77+
obj.spec.syncPolicy.automated.selfHeal = false;
78+
} else {
79+
obj.spec.syncPolicy.automated = {
80+
...obj.spec.syncPolicy.automated,
81+
...{ selfHeal: true },
82+
};
83+
}
84+
break;
85+
}
86+
case 'prune': {
87+
if (obj.spec.syncPolicy.automated.prune) {
88+
obj.spec.syncPolicy.automated.prune = false;
89+
} else {
90+
obj.spec.syncPolicy.automated = { ...obj.spec.syncPolicy.automated, ...{ prune: true } };
91+
}
92+
break;
93+
}
94+
}
95+
k8sUpdate({
96+
model: ApplicationModel,
97+
data: obj,
98+
});
99+
};
100+
101+
let sources: ApplicationSource[];
102+
let revisions: string[];
103+
if (obj?.spec?.source) {
104+
sources = [obj?.spec?.source];
105+
revisions = [obj.status?.sync?.revision];
106+
} else if (obj?.spec?.sources) {
107+
sources = obj.spec.sources;
108+
revisions = obj.status?.sync?.revisions;
109+
} else {
110+
sources = [];
111+
revisions = [];
112+
}
113+
return (
114+
<div>
115+
<PageSection
116+
variant={PageSectionVariants.default}
117+
className={classNames('co-m-pane__body', { 'co-m-pane__body--section-heading': true })}
118+
>
119+
<Title headingLevel="h2" className="co-section-heading">
120+
{t('Application details')}
121+
</Title>
122+
<div className="row">
123+
<div className="col-sm-6">
124+
<BaseDetailsSummary
125+
obj={obj}
126+
model={ApplicationModel}
127+
nameLink={
128+
<>
129+
<ArgoCDLink
130+
href={
131+
argoServer.protocol +
132+
'://' +
133+
argoServer.host +
134+
'/applications/' +
135+
obj?.metadata?.namespace +
136+
'/' +
137+
obj?.metadata?.name
138+
}
139+
/>
140+
</>
141+
}
142+
/>
143+
</div>
144+
145+
<div className="col-sm-6">
146+
<DescriptionList className="pf-c-description-list">
147+
<DetailsDescriptionGroup
148+
title={t('Health Status')}
149+
help={t('Health status represents the overall health of the application.')}
150+
>
151+
<HealthStatus status={obj.status?.health?.status || ''} />
152+
</DetailsDescriptionGroup>
153+
154+
<DetailsDescriptionGroup
155+
title={t('Current Sync Status')}
156+
help={t(
157+
'Sync status represents the current synchronized state for the application.',
158+
)}
159+
>
160+
<Flex>
161+
<FlexItem>
162+
<SyncStatus status={obj.status?.sync?.status || ''} />
163+
</FlexItem>
164+
<FlexItem>
165+
<PfLabel>
166+
<Revision
167+
revision={revisions[0] || ''}
168+
repoURL={sources[0].repoURL}
169+
helm={obj.status?.sourceType == 'Helm' && sources[0].chart ? true : false}
170+
revisionExtra={
171+
revisions.length > 1 && ' and ' + (revisions.length - 1) + ' more'
172+
}
173+
/>
174+
</PfLabel>
175+
</FlexItem>
176+
</Flex>
177+
</DetailsDescriptionGroup>
178+
179+
<DetailsDescriptionGroup
180+
title={t('Last Sync Status')}
181+
help={t('The result of the last sync status.')}
182+
>
183+
<Flex>
184+
{obj?.status?.operationState && (
185+
<FlexItem>
186+
<OperationState app={obj} />
187+
</FlexItem>
188+
)}
189+
{obj?.status?.conditions && (
190+
<FlexItem>
191+
<ConditionsPopover conditions={obj.status?.conditions} />
192+
</FlexItem>
193+
)}
194+
</Flex>
195+
</DetailsDescriptionGroup>
196+
197+
<DetailsDescriptionGroup
198+
title={t('Target Revision')}
199+
help={t('The specified revision for the Application.')}
200+
>
201+
{sources[0].targetRevision ? sources[0].targetRevision : 'HEAD'}
202+
</DetailsDescriptionGroup>
203+
204+
<DetailsDescriptionGroup
205+
title={t('Project')}
206+
help={t('The Argo CD Project that this application belongs to.')}
207+
>
208+
{/* TODO - Update to handle App in Any Namespace when controller namespace is in status */}
209+
<ResourceLink
210+
namespace={obj?.metadata?.namespace}
211+
groupVersionKind={{
212+
group: 'argoproj.io',
213+
version: 'v1alpha1',
214+
kind: 'AppProject',
215+
}}
216+
name={obj?.spec?.project}
217+
/>
218+
</DetailsDescriptionGroup>
219+
220+
<DetailsDescriptionGroup
221+
title={t('Destination')}
222+
help={t('The cluster and namespace where the application is targeted')}
223+
>
224+
{getFriendlyClusterName(obj?.spec?.destination.server)}/
225+
{obj?.spec?.destination.namespace}
226+
</DetailsDescriptionGroup>
227+
228+
<DetailsDescriptionGroup
229+
title={t('Sync Policy')}
230+
help={t('Provides options to determine application synchronization behavior')}
231+
>
232+
<ToggleGroup isCompact areAllGroupsDisabled={!canPatch || !canUpdate}>
233+
<ToggleGroupItem
234+
text={t('Automated')}
235+
buttonId="automated"
236+
onChange={onChangeAutomated}
237+
isSelected={obj?.spec?.syncPolicy?.automated ? true : false}
238+
/>
239+
<ToggleGroupItem
240+
text={t('Prune')}
241+
buttonId="prune"
242+
onChange={onChangeAutomated}
243+
isSelected={
244+
obj?.spec?.syncPolicy?.automated && obj?.spec?.syncPolicy?.automated.prune
245+
}
246+
isDisabled={obj?.spec?.syncPolicy?.automated ? false : true}
247+
/>
248+
<ToggleGroupItem
249+
text={t('Self Heal')}
250+
buttonId="self-heal"
251+
onChange={onChangeAutomated}
252+
isSelected={
253+
obj?.spec?.syncPolicy?.automated && obj?.spec?.syncPolicy?.automated.selfHeal
254+
}
255+
isDisabled={obj?.spec?.syncPolicy?.automated ? false : true}
256+
/>
257+
</ToggleGroup>
258+
</DetailsDescriptionGroup>
259+
</DescriptionList>
260+
</div>
261+
</div>
262+
</PageSection>
263+
</div>
264+
);
265+
};
266+
267+
export default ApplicationDetailsTab;

0 commit comments

Comments
 (0)