From a465e59868f4dfe1fb081a5019a318eb11e851d9 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 27 Aug 2025 17:57:24 -0400 Subject: [PATCH 01/16] Show an Argo CD Application Set as Details page in Dev Console Signed-off-by: Atif Ali --- console-extensions.json | 18 ++ plugin-metadata.ts | 1 + .../application/ApplicationDetailsTitle.tsx | 80 +++++ .../application/ApplicationSetDetailsPage.tsx | 305 ++++++++++++++++++ .../application-details-title.scss | 14 + 5 files changed, 418 insertions(+) create mode 100644 src/gitops/components/application/ApplicationDetailsTitle.tsx create mode 100644 src/gitops/components/application/ApplicationSetDetailsPage.tsx diff --git a/console-extensions.json b/console-extensions.json index 6d599193..d97fddf1 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -406,5 +406,23 @@ "$codeRef": "yamlApplicationTemplates.defaultApplicationSetYamlTemplate" } } + }, + { + "type": "console.page/resource/details", + "flags": { + "required": [ + "APPLICATIONSET" + ] + }, + "properties": { + "model": { + "group": "argoproj.io", + "kind": "ApplicationSet", + "version": "v1alpha1" + }, + "component": { + "$codeRef": "ApplicationSetDetailsPage" + } + } } ] diff --git a/plugin-metadata.ts b/plugin-metadata.ts index 7bf50d72..8993c161 100644 --- a/plugin-metadata.ts +++ b/plugin-metadata.ts @@ -16,6 +16,7 @@ const metadata: ConsolePluginBuildMetadata = { ApplicationList: "./gitops/components/application/ApplicationListTab.tsx", ApplicationDetails: "./gitops/components/application/ApplicationNavPage.tsx", ApplicationSetList: "./gitops/components/application/ApplicationSetListTab.tsx", + ApplicationSetDetailsPage: "./gitops/components/application/ApplicationSetDetailsPage.tsx", yamlApplicationTemplates: "./gitops/components/application/templates/index.ts" } }; diff --git a/src/gitops/components/application/ApplicationDetailsTitle.tsx b/src/gitops/components/application/ApplicationDetailsTitle.tsx new file mode 100644 index 00000000..e18f148d --- /dev/null +++ b/src/gitops/components/application/ApplicationDetailsTitle.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom-v5-compat'; +import DevPreviewBadge from '../../../components/import/badges/DevPreviewBadge'; +import { DEFAULT_NAMESPACE } from '../../utils/constants'; +import { isApplicationRefreshing } from '../../utils/gitops'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import { Action, K8sModel, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; +import { Breadcrumb, BreadcrumbItem, Spinner, Title } from '@patternfly/react-core'; +import ActionsDropdown from '../../utils/components/ActionDropDown/ActionDropDown'; +import DetailsPageTitle, { PaneHeading } from '../../utils/components/DetailsPageTitle/DetailsPageTitle'; +import './application-details-title.scss'; + +type ApplicationPageTitleProps = { + obj: K8sResourceCommon; + model: K8sModel; + name: string; + namespace: string; + actions: Action[]; +}; + +const ApplicationDetailsTitle: React.FC = ({ + obj, + model, + name, + namespace, + actions, +}) => { + const { t } = useGitOpsTranslation(); + + // Determine if this is an ApplicationSet based on the model kind + const isApplicationSet = model.kind === 'ApplicationSet'; + const iconText = isApplicationSet ? 'AS' : 'A'; + const iconTitle = isApplicationSet ? 'Argo CD ApplicationSet' : 'Argo CD Application'; + + return ( + <> +
+ + + + Argo CD {t(model.labelPlural)} + + + Argo CD {t(model.labelPlural + ' Details')} + + } + > + + + <span + className="argocd-application-icon co-m-resource-icon co-m-resource-icon--lg" + title={iconTitle} + > + {iconText} + </span> + <span className="co-resource-item__resource-name"> + {name ?? obj?.metadata?.name}{' '} + {isApplicationRefreshing(obj) ? <Spinner size="md" /> : <span></span>} + </span> + <span style={{ marginLeft: '10px', marginBottom: '3px' }}> + <DevPreviewBadge /> + </span> + +
+ +
+
+
+
+ + ); +}; + +export default ApplicationDetailsTitle; diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx new file mode 100644 index 00000000..b2d8746d --- /dev/null +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -0,0 +1,305 @@ +import * as React from 'react'; +import { useK8sWatchResource, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { useParams } from 'react-router-dom-v5-compat'; +import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; +import { + Card, + CardBody, + CardTitle, + CardHeader, + Spinner, + Badge, + Label, + LabelGroup, + DescriptionList, + Tabs, + Tab, + TabTitleText, + Button, + ButtonVariant +} from '@patternfly/react-core'; +import { PencilAltIcon } from '@patternfly/react-icons'; +import * as _ from 'lodash'; +import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; +import ApplicationDetailsTitle from './ApplicationDetailsTitle'; + +const ApplicationSetDetailsPage: React.FC = () => { + const { name, ns } = useParams<{ name: string; ns: string }>(); + const [activeTabKey, setActiveTabKey] = React.useState(0); + + const [appSet, loaded, loadError] = useK8sWatchResource({ + groupVersionKind: { + group: 'argoproj.io', + version: 'v1alpha1', + kind: 'ApplicationSet', + }, + name, + namespace: ns, + }); + + const [actions] = useApplicationSetActionsProvider(appSet); + + if (loadError) return
Error loading ApplicationSet details.
; + if (!loaded || !appSet) return ; + + const metadata = appSet.metadata || {}; + const status = appSet.status || {}; + + const handleTabClick = (event: React.MouseEvent, tabIndex: string | number) => { + setActiveTabKey(tabIndex); + }; + + const labelItems = metadata.labels || {}; + + return ( +
+ + + {/* Main Content */} +
+ {/* Tabs Section */} +
+ + Details} className="pf-v6-c-tab-content"> +
+
+
+ + + ApplicationSet details + + + +
+
+
+
Name
+
+
+
+
+
{metadata.name}
+
+
+
+ +
+
+
+
Namespace
+
+
+
+
+
+ NS {metadata.namespace} +
+
+
+
+ +
+
+
+
Labels
+
+
+
+
+
+ {_.isEmpty(labelItems) ? ( + No labels + ) : ( +
+ + {Object.entries(labelItems).map(([key, value]) => ( + + ))} + + +
+ )} +
+
+
+
+ +
+
+
+
Created at
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
Status
+
+
+
+
+
+ Healthy +
+
+
+
+ +
+
+
+
Generated Apps
+
+
+
+
+
+ 3 applications +
+
+
+
+ +
+
+
+
Generators
+
+
+
+
+
+ 1 generators +
+
+
+
+ +
+
+
+
App Project
+
+
+
+
+
+ AP default +
+
+
+
+ +
+
+
+
Repository
+
+
+
+ +
+
+
+ + {/* Conditions Section */} + {status.conditions && status.conditions.length > 0 && ( +
+

+ Conditions +

+ + + + + + + + + + + + {status.conditions.map((condition: any, index: number) => ( + + + + + + + + ))} + +
TypeStatusUpdatedReasonMessage
{condition.type} + + {condition.status} + + + + {condition.reason || ''}{condition.message || ''}
+
+ )} +
+
+
+
+
+
+ + YAML} className="pf-v6-c-tab-content"> +
+
+
+
+ + + +
+
+ Shortcuts +
+
+
+
{JSON.stringify(appSet, null, 2)}
+
+
+
+
+
+
+
+
+ ); +}; + +export default ApplicationSetDetailsPage; diff --git a/src/gitops/components/application/application-details-title.scss b/src/gitops/components/application/application-details-title.scss index 9f26ff35..f3acee6f 100644 --- a/src/gitops/components/application/application-details-title.scss +++ b/src/gitops/components/application/application-details-title.scss @@ -1,4 +1,18 @@ // Application Details Title Styles +.argocd-application-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 4px; + background-color: #E9654B; + color: white; + font-size: 16px; + font-weight: bold; + margin-right: var(--pf-v6-global--spacer--md); +} + .co-resource-item__resource-name { font-size: var(--pf-v6-global--font-size--2xl); font-weight: var(--pf-v6-global--font-weight--bold); From 888a52f60a11c72a6c410fc0401ee8eafb69f805 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 27 Aug 2025 18:12:35 -0400 Subject: [PATCH 02/16] fix AS icon color and style Signed-off-by: Atif Ali --- src/gitops/components/application/ApplicationDetailsTitle.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gitops/components/application/ApplicationDetailsTitle.tsx b/src/gitops/components/application/ApplicationDetailsTitle.tsx index e18f148d..21127168 100644 --- a/src/gitops/components/application/ApplicationDetailsTitle.tsx +++ b/src/gitops/components/application/ApplicationDetailsTitle.tsx @@ -27,7 +27,7 @@ const ApplicationDetailsTitle: React.FC = ({ }) => { const { t } = useGitOpsTranslation(); - // Determine if this is an ApplicationSet based on the model kind + // Determine the correct icon text and styling based on the model const isApplicationSet = model.kind === 'ApplicationSet'; const iconText = isApplicationSet ? 'AS' : 'A'; const iconTitle = isApplicationSet ? 'Argo CD ApplicationSet' : 'Argo CD Application'; @@ -54,7 +54,7 @@ const ApplicationDetailsTitle: React.FC = ({ <span - className="argocd-application-icon co-m-resource-icon co-m-resource-icon--lg" + className="co-m-resource-icon co-m-resource-icon--lg" title={iconTitle} > {iconText} From f8260f494785db28f728142b925e22656e80f98e Mon Sep 17 00:00:00 2001 From: Atif Ali <atali@redhat.com> Date: Wed, 27 Aug 2025 19:08:14 -0400 Subject: [PATCH 03/16] label styling Signed-off-by: Atif Ali <atali@redhat.com> more label styling Signed-off-by: Atif Ali <atali@redhat.com> add annotation && Generators && more formatting Signed-off-by: Atif Ali <atali@redhat.com> --- .../application/ApplicationSetDetailsPage.tsx | 201 +++++++++++++----- 1 file changed, 143 insertions(+), 58 deletions(-) diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index b2d8746d..c904cecb 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -2,12 +2,12 @@ import * as React from 'react'; import { useK8sWatchResource, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; import { useParams } from 'react-router-dom-v5-compat'; import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; -import { - Card, - CardBody, +import { + Card, + CardBody, CardTitle, CardHeader, - Spinner, + Spinner, Badge, Label, LabelGroup, @@ -16,12 +16,14 @@ import { Tab, TabTitleText, Button, - ButtonVariant } from '@patternfly/react-core'; import { PencilAltIcon } from '@patternfly/react-icons'; import * as _ from 'lodash'; import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; import ApplicationDetailsTitle from './ApplicationDetailsTitle'; +import { useLabelsModal, useAnnotationsModal } from '@openshift-console/dynamic-plugin-sdk'; + +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; const ApplicationSetDetailsPage: React.FC = () => { const { name, ns } = useParams<{ name: string; ns: string }>(); @@ -38,6 +40,8 @@ const ApplicationSetDetailsPage: React.FC = () => { }); const [actions] = useApplicationSetActionsProvider(appSet); + const launchLabelsModal = useLabelsModal(appSet); + const launchAnnotationsModal = useAnnotationsModal(appSet); if (loadError) return <div>Error loading ApplicationSet details.</div>; if (!loaded || !appSet) return <Spinner />; @@ -50,6 +54,9 @@ const ApplicationSetDetailsPage: React.FC = () => { }; const labelItems = metadata.labels || {}; + const annotationItems = metadata.annotations || {}; + // Helper to count object keys + const countAnnotations = Object.keys(annotationItems).length; return ( <div className="pf-v6-c-page__main-section pf-m-no-padding pf-m-fill pf-v6-c-page__main-section--no-gap pf-v6-u-flex-shrink-1"> @@ -98,45 +105,124 @@ const ApplicationSetDetailsPage: React.FC = () => { <dd className="pf-v6-c-description-list__description"> <div className="pf-v6-l-split pf-v6-u-w-100"> <div className="pf-v6-l-split__item pf-m-fill"> - <Badge isRead color="green">NS</Badge> {metadata.namespace} + <ResourceLink kind="Namespace" name={metadata.namespace} /> </div> </div> </dd> </div> <div className="pf-v6-c-description-list__group"> - <dt className="pf-v6-c-description-list__term" data-test-selector="details-item-label_Labels"> - <div className="pf-v6-l-split pf-v6-u-w-100"> - <div className="pf-v6-l-split__item pf-m-fill">Labels</div> - </div> + <dt className="pf-v6-c-description-list__term" data-test-selector="details-item-label_Labels" style={{ margin: 0 }}> + <span>Labels</span> </dt> - <dd className="pf-v6-c-description-list__description"> - <div className="pf-v6-l-split pf-v6-u-w-100"> - <div className="pf-v6-l-split__item pf-m-fill"> + <dd className="pf-v6-c-description-list__description" style={{ padding: 0, marginTop: 0 }}> + <div style={{ display: 'inline-block' }}> + <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 4, width: '100%' }}> + <a + style={{ + fontSize: 13, + color: '#73bcf7', + textDecoration: 'underline', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + }} + tabIndex={0} + role="button" + onClick={launchLabelsModal} + onKeyPress={e => { if (e.key === 'Enter' || e.key === ' ') launchLabelsModal(); }} + aria-label="Edit labels" + > + Edit <PencilAltIcon style={{ marginLeft: 4, fontSize: 13, color: '#73bcf7' }} /> + </a> + </div> + <div + style={{ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + border: '1px solid #8a8d90', + borderRadius: 8, + padding: '6px 10px', + background: 'none', + boxSizing: 'border-box', + width: 'fit-content', + maxWidth: '100%', + gap: 8, + }} + > {_.isEmpty(labelItems) ? ( <span className="text-muted">No labels</span> ) : ( - <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> - <LabelGroup> - {Object.entries(labelItems).map(([key, value]) => ( - <Label key={key} color="grey"> - {key}={value} - </Label> - ))} - </LabelGroup> - <Button variant={ButtonVariant.link} icon={<PencilAltIcon />}> - Edit - </Button> - </div> + <LabelGroup + style={{ + display: 'flex', + flexDirection: 'row', + gap: '8px', + margin: 0, + }} + > + {Object.entries(labelItems).map(([key, value]) => ( + <Label key={key} color="grey"> + {key}={value} + </Label> + ))} + </LabelGroup> )} </div> </div> </dd> </div> + {/* Annotations Section - matches Console style */} + <div className="pf-v6-c-description-list__group"> + <dt className="pf-v6-c-description-list__term" data-test-selector="details-item-label_Annotations" style={{ margin: 0 }}> + <span>Annotations</span> + </dt> + <dd className="pf-v6-c-description-list__description" style={{ padding: 0, marginTop: 0 }}> + <div style={{ display: 'inline-block' }}> + <div style={{ display: 'flex', alignItems: 'center', marginBottom: 4, width: '100%' }}> + <a + style={{ + fontSize: 15, + color: '#73bcf7', + textDecoration: 'underline', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + }} + tabIndex={0} + role="button" + onClick={launchAnnotationsModal} + onKeyPress={e => { if (e.key === 'Enter' || e.key === ' ') launchAnnotationsModal(); }} + aria-label="Edit annotations" + > + {countAnnotations} annotation{countAnnotations !== 1 ? 's' : ''} + <PencilAltIcon style={{ marginLeft: 6, fontSize: 15, color: '#73bcf7' }} /> + </a> + </div> + </div> + </dd> + </div> + <div className="pf-v6-c-description-list__group"> <dt className="pf-v6-c-description-list__term" data-test-selector="details-item-label_Created"> <div className="pf-v6-l-split pf-v6-u-w-100"> + <Button + variant="link" + icon={<PencilAltIcon />} + onClick={launchLabelsModal} + style={{ + padding: 0, + position: 'absolute', + top: -24, + right: 0, + fontSize: 13, + }} + aria-label="Edit labels" + > + Edit + </Button> <div className="pf-v6-l-split__item pf-m-fill">Created at</div> </div> </dt> @@ -179,6 +265,7 @@ const ApplicationSetDetailsPage: React.FC = () => { </dd> </div> + {/* Generators Section */} <div className="pf-v6-c-description-list__group"> <dt className="pf-v6-c-description-list__term" data-test-selector="details-item-label_Generators"> <div className="pf-v6-l-split pf-v6-u-w-100"> @@ -188,12 +275,13 @@ const ApplicationSetDetailsPage: React.FC = () => { <dd className="pf-v6-c-description-list__description"> <div className="pf-v6-l-split pf-v6-u-w-100"> <div className="pf-v6-l-split__item pf-m-fill"> - <Badge isRead color="blue">1 generators</Badge> + <Badge isRead color="grey">1 generators</Badge> </div> </div> </dd> </div> + {/* App Project Section (blue badge, no extra Created at) */} <div className="pf-v6-c-description-list__group"> <dt className="pf-v6-c-description-list__term" data-test-selector="details-item-label_AppProject"> <div className="pf-v6-l-split pf-v6-u-w-100"> @@ -203,7 +291,7 @@ const ApplicationSetDetailsPage: React.FC = () => { <dd className="pf-v6-c-description-list__description"> <div className="pf-v6-l-split pf-v6-u-w-100"> <div className="pf-v6-l-split__item pf-m-fill"> - <Badge isRead color="blue">AP</Badge> default + <Badge isRead color="blue" style={{ backgroundColor: '#73bcf7', color: '#003a70' }}>AP</Badge> default </div> </div> </dd> @@ -229,38 +317,35 @@ const ApplicationSetDetailsPage: React.FC = () => { {/* Conditions Section */} {status.conditions && status.conditions.length > 0 && ( - <div className="co-m-pane__body"> - <h2 data-test-section-heading="Conditions" className="pf-v6-c-title pf-m-h2 co-section-heading"> - <span>Conditions</span> - </h2> - <table role="grid" className="pf-v6-c-table"> - <thead> - <tr> - <th>Type</th> - <th>Status</th> - <th>Updated</th> - <th>Reason</th> - <th>Message</th> - </tr> - </thead> - <tbody> - {status.conditions.map((condition: any, index: number) => ( - <tr key={index}> - <td>{condition.type}</td> - <td> - <Badge isRead color={condition.status === 'True' ? 'green' : 'grey'}> - {condition.status} - </Badge> - </td> - <td> + <div className="co-m-pane__body" style={{ marginTop: 32 }}> + <div style={{ fontWeight: 700, fontSize: 24, marginBottom: 20, marginTop: 8 }}>Conditions</div> + <div style={{ borderTop: '1px solid #393F44', marginBottom: 0 }} /> + <div style={{ width: '100%' }}> + <div style={{ display: 'flex', fontWeight: 600, fontSize: 16, padding: '16px 0 8px 0' }}> + <div style={{ flex: 2, textAlign: 'left', paddingLeft: 0 }}>Type</div> + <div style={{ flex: 1, textAlign: 'left' }}>Status</div> + <div style={{ flex: 2, textAlign: 'left' }}>Updated</div> + <div style={{ flex: 2, textAlign: 'left' }}>Reason</div> + <div style={{ flex: 4, textAlign: 'left' }}>Message</div> + </div> + <div style={{ borderTop: '1px solid #393F44' }} /> + {status.conditions.map((condition: any, index: number) => ( + <React.Fragment key={index}> + <div style={{ display: 'flex', fontSize: 15, padding: '16px 0', alignItems: 'flex-start' }}> + <div style={{ flex: 2, textAlign: 'left', paddingLeft: 0 }}>{condition.type}</div> + <div style={{ flex: 1, textAlign: 'left' }}>{condition.status}</div> + <div style={{ flex: 2, textAlign: 'left', display: 'flex', alignItems: 'center' }}> <Timestamp timestamp={condition.lastTransitionTime} /> - </td> - <td>{condition.reason || ''}</td> - <td>{condition.message || ''}</td> - </tr> - ))} - </tbody> - </table> + </div> + <div style={{ flex: 2, textAlign: 'left' }}>{condition.reason || ''}</div> + <div style={{ flex: 4, textAlign: 'left' }}>{condition.message || ''}</div> + </div> + {index !== status.conditions.length - 1 && ( + <div style={{ borderTop: '1px solid #393F44' }} /> + )} + </React.Fragment> + ))} + </div> </div> )} </CardBody> From 1e613ed4bbd6c5f856068cde5d7c078a0187e746 Mon Sep 17 00:00:00 2001 From: Atif Ali <atali@redhat.com> Date: Wed, 27 Aug 2025 21:55:13 -0400 Subject: [PATCH 04/16] add Generators, Applications and Event tabs Signed-off-by: Atif Ali <atali@redhat.com> --- .../application/ApplicationSetDetailsPage.tsx | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index c904cecb..ef1db3c4 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -24,6 +24,7 @@ import ApplicationDetailsTitle from './ApplicationDetailsTitle'; import { useLabelsModal, useAnnotationsModal } from '@openshift-console/dynamic-plugin-sdk'; import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import ApplicationList from '../shared/ApplicationList'; const ApplicationSetDetailsPage: React.FC = () => { const { name, ns } = useParams<{ name: string; ns: string }>(); @@ -380,6 +381,251 @@ const ApplicationSetDetailsPage: React.FC = () => { </div> </div> </Tab> + + <Tab eventKey={2} title={<TabTitleText>Generators</TabTitleText>} className="pf-v6-c-tab-content"> + <div className="co-m-pane__body"> + <div className="pf-v6-l-grid pf-m-gutter"> + <div className="pf-v6-l-grid__item pf-m-12-col-on-md"> + <Card> + <CardHeader> + <CardTitle>Generators</CardTitle> + </CardHeader> + <CardBody> + <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}> + {appSet.spec?.generators?.map((generator: any, index: number) => { + const generatorType = Object.keys(generator)[0]; + const generatorData = generator[generatorType]; + + return ( + <div key={index} style={{ + border: '1px solid #393F44', + borderRadius: '8px', + padding: '16px', + backgroundColor: '#212427' + }}> + <div style={{ display: 'flex', alignItems: 'center', marginBottom: '12px' }}> + <div style={{ + width: '24px', + height: '24px', + backgroundColor: '#73bcf7', + borderRadius: '4px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginRight: '8px', + fontSize: '12px', + fontWeight: 'bold', + color: '#003a70' + }}> + {generatorType.charAt(0).toUpperCase()} + </div> + <span style={{ fontWeight: '600', fontSize: '16px' }}>{generatorType}</span> + </div> + + {/* Render different generator types */} + {generatorType === 'git' && ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> + {generatorData.repoURL && ( + <div style={{ display: 'flex', alignItems: 'center' }}> + <span style={{ fontWeight: '500', minWidth: '80px', color: '#8a8d90' }}>Repository:</span> + <span style={{ color: '#73bcf7', textDecoration: 'underline', cursor: 'pointer' }}> + {generatorData.repoURL} + </span> + </div> + )} + {generatorData.revision && ( + <div style={{ display: 'flex', alignItems: 'center' }}> + <span style={{ fontWeight: '500', minWidth: '80px', color: '#8a8d90' }}>Revision:</span> + <span>{generatorData.revision}</span> + </div> + )} + {generatorData.directories && ( + <div style={{ display: 'flex', alignItems: 'center' }}> + <span style={{ fontWeight: '500', minWidth: '80px', color: '#8a8d90' }}>Directories:</span> + <span>{generatorData.directories.length} directory(ies)</span> + </div> + )} + </div> + )} + + {generatorType === 'clusterDecisionResource' && ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> + {generatorData.configMapRef && ( + <div style={{ display: 'flex', alignItems: 'center' }}> + <span style={{ fontWeight: '500', minWidth: '80px', color: '#8a8d90' }}>ConfigMap:</span> + <span>{generatorData.configMapRef.name}</span> + </div> + )} + </div> + )} + + {generatorType === 'matrix' && ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> + <div style={{ color: '#8a8d90', fontSize: '14px' }}> + Matrix generator with {Object.keys(generatorData).length} generators + </div> + </div> + )} + + {generatorType === 'clusters' && ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}> + {generatorData.selector && ( + <div style={{ display: 'flex', alignItems: 'center' }}> + <span style={{ fontWeight: '500', minWidth: '80px', color: '#8a8d90' }}>Selector:</span> + <span style={{ fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace', fontSize: '12px' }}> + {JSON.stringify(generatorData.selector)} + </span> + </div> + )} + </div> + )} + </div> + ); + })} + + {(!appSet.spec?.generators || appSet.spec.generators.length === 0) && ( + <div style={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '40px 20px', + color: '#8a8d90', + fontSize: '16px' + }}> + <div style={{ + width: '48px', + height: '48px', + backgroundColor: '#393F44', + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginBottom: '16px', + fontSize: '24px' + }}> + ⚙️ + </div> + <div style={{ textAlign: 'center' }}> + <div style={{ fontWeight: '600', marginBottom: '8px' }}>No Generators</div> + <div style={{ fontSize: '14px' }}> + This ApplicationSet has no generators configured. + </div> + </div> + </div> + )} + </div> + </CardBody> + </Card> + </div> + </div> + </div> + </Tab> + + <Tab eventKey={3} title={<TabTitleText>Applications</TabTitleText>} className="pf-v6-c-tab-content"> + <div className="co-m-pane__body"> + <div style={{ padding: '0' }}> + <ApplicationList + namespace={ns} + hideNameLabelFilters={false} + showTitle={false} + appset={appSet} + /> + </div> + </div> + </Tab> + + <Tab eventKey={4} title={<TabTitleText>Events</TabTitleText>} className="pf-v6-c-tab-content"> + <div className="co-m-pane__body"> + <div className="pf-v6-l-grid pf-m-gutter"> + <div className="pf-v6-l-grid__item pf-m-12-col-on-md"> + <Card> + <CardHeader> + <CardTitle>Events</CardTitle> + </CardHeader> + <CardBody> + {status.conditions && status.conditions.length > 0 ? ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> + {status.conditions.map((condition: any, index: number) => ( + <div key={index} style={{ + border: '1px solid #393F44', + borderRadius: '8px', + padding: '16px', + backgroundColor: '#212427' + }}> + <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}> + <div style={{ display: 'flex', alignItems: 'center' }}> + <div style={{ + width: '24px', + height: '24px', + backgroundColor: condition.status === 'True' ? '#3e8635' : '#c9190b', + borderRadius: '4px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginRight: '8px', + fontSize: '12px', + fontWeight: 'bold', + color: 'white' + }}> + {condition.status === 'True' ? '✓' : '✗'} + </div> + <span style={{ fontWeight: '600', fontSize: '16px' }}> + {condition.type} + </span> + </div> + <Badge isRead color={condition.status === 'True' ? 'green' : 'red'}> + {condition.status} + </Badge> + </div> + <div style={{ fontSize: '14px', color: '#8a8d90', marginBottom: '8px' }}> + {condition.message || 'No message available'} + </div> + {condition.lastTransitionTime && ( + <div style={{ fontSize: '12px', color: '#8a8d90' }}> + Last updated: {new Date(condition.lastTransitionTime).toLocaleString()} + </div> + )} + </div> + ))} + </div> + ) : ( + <div style={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '40px 20px', + color: '#8a8d90', + fontSize: '16px' + }}> + <div style={{ + width: '48px', + height: '48px', + backgroundColor: '#393F44', + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginBottom: '16px', + fontSize: '24px' + }}> + 📊 + </div> + <div style={{ textAlign: 'center' }}> + <div style={{ fontWeight: '600', marginBottom: '8px' }}>No Events</div> + <div style={{ fontSize: '14px' }}> + No events have been recorded for this ApplicationSet. + </div> + </div> + </div> + )} + </CardBody> + </Card> + </div> + </div> + </div> + </Tab> </Tabs> </div> </div> From 65876af98b497cf74d7bf338dc2c6fd481c48da4 Mon Sep 17 00:00:00 2001 From: Atif Ali <atali@redhat.com> Date: Wed, 27 Aug 2025 22:11:53 -0400 Subject: [PATCH 05/16] add filter logic for generated apps to display on appset details page Signed-off-by: Atif Ali <atali@redhat.com> restore Applist Signed-off-by: Atif Ali <atali@redhat.com> reapply filter logic keeping the empty state when no apps are available Signed-off-by: Atif Ali <atali@redhat.com> rename ApplicationDetailsTitle and move it to where DetailsPageTitle Signed-off-by: Atif Ali <atali@redhat.com> remove border on the pages Signed-off-by: Atif Ali <atali@redhat.com> fix formatting Signed-off-by: Atif Ali <atali@redhat.com> --- .../application/ApplicationSetDetailsPage.tsx | 53 ++++++++----------- .../components/shared/ApplicationList.tsx | 36 ++++++------- .../ResourceDetailsTitle.tsx} | 49 +++++++++-------- 3 files changed, 68 insertions(+), 70 deletions(-) rename src/gitops/{components/application/ApplicationDetailsTitle.tsx => utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx} (53%) diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index ef1db3c4..02a5b138 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -3,10 +3,6 @@ import { useK8sWatchResource, Timestamp } from '@openshift-console/dynamic-plugi import { useParams } from 'react-router-dom-v5-compat'; import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; import { - Card, - CardBody, - CardTitle, - CardHeader, Spinner, Badge, Label, @@ -20,7 +16,7 @@ import { import { PencilAltIcon } from '@patternfly/react-icons'; import * as _ from 'lodash'; import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; -import ApplicationDetailsTitle from './ApplicationDetailsTitle'; +import ResourceDetailsTitle from '../../utils/components/DetailsPageTitle/ResourceDetailsTitle'; import { useLabelsModal, useAnnotationsModal } from '@openshift-console/dynamic-plugin-sdk'; import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; @@ -61,12 +57,15 @@ const ApplicationSetDetailsPage: React.FC = () => { return ( <div className="pf-v6-c-page__main-section pf-m-no-padding pf-m-fill pf-v6-c-page__main-section--no-gap pf-v6-u-flex-shrink-1"> - <ApplicationDetailsTitle + <ResourceDetailsTitle obj={appSet} model={ApplicationSetModel} name={name} namespace={ns} actions={actions} + iconText="AS" + iconTitle="Argo CD ApplicationSet" + resourcePrefix="Argo CD" /> {/* Main Content */} @@ -78,11 +77,10 @@ const ApplicationSetDetailsPage: React.FC = () => { <div className="co-m-pane__body"> <div className="pf-v6-l-grid pf-m-gutter"> <div className="pf-v6-l-grid__item pf-m-12-col-on-md"> - <Card> - <CardHeader> - <CardTitle>ApplicationSet details</CardTitle> - </CardHeader> - <CardBody> + <div style={{ marginBottom: '24px', paddingLeft: '24px', paddingTop: '24px' }}> + <h2 style={{ fontSize: '20px', fontWeight: '600', marginBottom: '16px' }}>Argo CD ApplicationSet details</h2> + </div> + <div style={{ paddingLeft: '24px' }}> <DescriptionList data-test-id="resource-summary"> <div className="pf-v6-c-description-list__group"> <dt className="pf-v6-c-description-list__term" data-test-selector="details-item-label_Name"> @@ -349,8 +347,7 @@ const ApplicationSetDetailsPage: React.FC = () => { </div> </div> )} - </CardBody> - </Card> + </div> </div> </div> </div> @@ -383,14 +380,12 @@ const ApplicationSetDetailsPage: React.FC = () => { </Tab> <Tab eventKey={2} title={<TabTitleText>Generators</TabTitleText>} className="pf-v6-c-tab-content"> - <div className="co-m-pane__body"> - <div className="pf-v6-l-grid pf-m-gutter"> - <div className="pf-v6-l-grid__item pf-m-12-col-on-md"> - <Card> - <CardHeader> - <CardTitle>Generators</CardTitle> - </CardHeader> - <CardBody> + <div className="pf-v6-l-grid pf-m-gutter"> + <div className="pf-v6-l-grid__item pf-m-12-col-on-md"> + <div style={{ marginBottom: '24px', paddingLeft: '24px', paddingTop: '24px' }}> + <h2 style={{ fontSize: '20px', fontWeight: '600', marginBottom: '16px' }}>Generators</h2> + </div> + <div style={{ paddingLeft: '24px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}> {appSet.spec?.generators?.map((generator: any, index: number) => { const generatorType = Object.keys(generator)[0]; @@ -515,11 +510,9 @@ const ApplicationSetDetailsPage: React.FC = () => { </div> )} </div> - </CardBody> - </Card> + </div> </div> </div> - </div> </Tab> <Tab eventKey={3} title={<TabTitleText>Applications</TabTitleText>} className="pf-v6-c-tab-content"> @@ -539,11 +532,10 @@ const ApplicationSetDetailsPage: React.FC = () => { <div className="co-m-pane__body"> <div className="pf-v6-l-grid pf-m-gutter"> <div className="pf-v6-l-grid__item pf-m-12-col-on-md"> - <Card> - <CardHeader> - <CardTitle>Events</CardTitle> - </CardHeader> - <CardBody> + <div style={{ marginBottom: '24px', paddingLeft: '24px', paddingTop: '24px' }}> + <h2 style={{ fontSize: '20px', fontWeight: '600', marginBottom: '16px' }}>Events</h2> + </div> + <div style={{ paddingLeft: '24px' }}> {status.conditions && status.conditions.length > 0 ? ( <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> {status.conditions.map((condition: any, index: number) => ( @@ -620,8 +612,7 @@ const ApplicationSetDetailsPage: React.FC = () => { </div> </div> )} - </CardBody> - </Card> + </div> </div> </div> </div> diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index 4c994961..b57ed5c8 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -1,3 +1,13 @@ +import { + DataViewTable, + DataViewTh, + DataViewTr, +} from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks'; +import { Spinner, Flex, FlexItem, EmptyState, EmptyStateBody } from '@patternfly/react-core'; +import { CubesIcon } from '@patternfly/react-icons'; +import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; +import DataView, { DataViewState } from '@patternfly/react-data-view/dist/esm/DataView'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom-v5-compat'; @@ -15,17 +25,6 @@ import { useK8sWatchResource, useListPageFilter, } from '@openshift-console/dynamic-plugin-sdk'; -import { ErrorState } from '@patternfly/react-component-groups'; -import { EmptyState, EmptyStateBody, Flex, FlexItem, Spinner } from '@patternfly/react-core'; -import { - DataViewTable, - DataViewTh, - DataViewTr, -} from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; -import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks'; -import DataView, { DataViewState } from '@patternfly/react-data-view/dist/esm/DataView'; -import { CubesIcon } from '@patternfly/react-icons'; -import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; import { useApplicationActionsProvider } from '../..//hooks/useApplicationActionsProvider'; import RevisionFragment from '../..//Revision/Revision'; @@ -126,7 +125,7 @@ const ApplicationList: React.FC<ApplicationProps> = ({ return sortData(applications, sortBy, direction); }, [applications, sortBy, direction]); // TODO: use alternate filter since it is deprecated. See DataTableView potentially - const [data, filteredData, onFilterChange] = useListPageFilter(sortedApplications, filters); + const [, filteredData, onFilterChange] = useListPageFilter(sortedApplications, filters); // Filter applications by project or appset before rendering rows const filteredByOwner = React.useMemo( () => filteredData.filter(filterApp(project, appset)), @@ -150,10 +149,11 @@ const ApplicationList: React.FC<ApplicationProps> = ({ <Tbody> <Tr key="loading" ouiaId={'table-tr-loading'}> <Td colSpan={columnsDV.length}> - <ErrorState - titleText="Unable to load data" - bodyText="There was an error retrieving applications. Check your connection and reload the page." - /> + <EmptyState headingLevel="h4" icon={CubesIcon} titleText="Unable to load data"> + <EmptyStateBody> + There was an error retrieving applications. Check your connection and reload the page. + </EmptyStateBody> + </EmptyState> </Td> </Tr> </Tbody> @@ -180,7 +180,7 @@ const ApplicationList: React.FC<ApplicationProps> = ({ <ListPageBody> {!hideNameLabelFilters && ( <ListPageFilter - data={data.filter(filterApp(project, appset))} + data={applications.filter(filterApp(project, appset))} loaded={loaded} rowFilters={filters} onFilterChange={onFilterChange} @@ -476,4 +476,4 @@ export const filters: RowFilter[] = [ }, ]; -export default ApplicationList; +export default ApplicationList; \ No newline at end of file diff --git a/src/gitops/components/application/ApplicationDetailsTitle.tsx b/src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx similarity index 53% rename from src/gitops/components/application/ApplicationDetailsTitle.tsx rename to src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx index 21127168..7bfcb8ff 100644 --- a/src/gitops/components/application/ApplicationDetailsTitle.tsx +++ b/src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx @@ -1,37 +1,42 @@ import * as React from 'react'; import { Link } from 'react-router-dom-v5-compat'; -import DevPreviewBadge from '../../../components/import/badges/DevPreviewBadge'; -import { DEFAULT_NAMESPACE } from '../../utils/constants'; -import { isApplicationRefreshing } from '../../utils/gitops'; -import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import DevPreviewBadge from '../../../../components/import/badges/DevPreviewBadge'; +import { DEFAULT_NAMESPACE } from '../../../utils/constants'; +import { isApplicationRefreshing } from '../../../utils/gitops'; +import { useGitOpsTranslation } from '../../../utils/hooks/useGitOpsTranslation'; import { Action, K8sModel, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; import { Breadcrumb, BreadcrumbItem, Spinner, Title } from '@patternfly/react-core'; -import ActionsDropdown from '../../utils/components/ActionDropDown/ActionDropDown'; -import DetailsPageTitle, { PaneHeading } from '../../utils/components/DetailsPageTitle/DetailsPageTitle'; -import './application-details-title.scss'; +import ActionsDropdown from '../../../utils/components/ActionDropDown/ActionDropDown'; +import DetailsPageTitle, { PaneHeading } from './DetailsPageTitle'; -type ApplicationPageTitleProps = { +type ResourceDetailsTitleProps = { obj: K8sResourceCommon; model: K8sModel; name: string; namespace: string; actions: Action[]; + // Configurable properties for different resource types + iconText: string; + iconTitle: string; + resourcePrefix?: string; // e.g., "Argo CD" for Applications/ApplicationSets + showDevPreviewBadge?: boolean; + showRefreshSpinner?: boolean; }; -const ApplicationDetailsTitle: React.FC<ApplicationPageTitleProps> = ({ +const ResourceDetailsTitle: React.FC<ResourceDetailsTitleProps> = ({ obj, model, name, namespace, actions, + iconText, + iconTitle, + resourcePrefix = '', + showDevPreviewBadge = true, + showRefreshSpinner = true, }) => { const { t } = useGitOpsTranslation(); - // Determine the correct icon text and styling based on the model - const isApplicationSet = model.kind === 'ApplicationSet'; - const iconText = isApplicationSet ? 'AS' : 'A'; - const iconTitle = isApplicationSet ? 'Argo CD ApplicationSet' : 'Argo CD Application'; - return ( <> <div> @@ -44,10 +49,10 @@ const ApplicationDetailsTitle: React.FC<ApplicationPageTitleProps> = ({ model.apiGroup + '~' + model.apiVersion + '~' + model.kind }`} > - Argo CD {t(model.labelPlural)} + {resourcePrefix} {t(model.labelPlural)} </Link> </BreadcrumbItem> - <BreadcrumbItem>Argo CD {t(model.labelPlural + ' Details')}</BreadcrumbItem> + <BreadcrumbItem>{resourcePrefix} {t(model.labelPlural + ' Details')}</BreadcrumbItem> </Breadcrumb> } > @@ -61,11 +66,13 @@ const ApplicationDetailsTitle: React.FC<ApplicationPageTitleProps> = ({ </span> <span className="co-resource-item__resource-name"> {name ?? obj?.metadata?.name}{' '} - {isApplicationRefreshing(obj) ? <Spinner size="md" /> : <span></span>} - </span> - <span style={{ marginLeft: '10px', marginBottom: '3px' }}> - <DevPreviewBadge /> + {showRefreshSpinner && isApplicationRefreshing(obj) ? <Spinner size="md" /> : <span></span>} </span> + {showDevPreviewBadge && ( + <span style={{ marginLeft: '10px', marginBottom: '3px' }}> + <DevPreviewBadge /> + </span> + )}
@@ -77,4 +84,4 @@ const ApplicationDetailsTitle: React.FC = ({ ); }; -export default ApplicationDetailsTitle; +export default ResourceDetailsTitle; From 274bee968cb58a3285fe5cf6b833c840342184e7 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 28 Aug 2025 15:36:55 -0400 Subject: [PATCH 06/16] pull common attributes to a common component Signed-off-by: Atif Ali use pf Signed-off-by: Atif Ali Replaced inline styles with CSS classes Signed-off-by: Atif Ali enable or disable the edit buttons according to permission Signed-off-by: Atif Ali --- .../ApplicationSetDetailsPage.scss | 219 ++++++++ .../application/ApplicationSetDetailsPage.tsx | 410 +++------------ .../ResourceDetailsAttributes.tsx | 476 ++++++++++++++++++ 3 files changed, 771 insertions(+), 334 deletions(-) create mode 100644 src/gitops/components/application/ApplicationSetDetailsPage.scss create mode 100644 src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.scss b/src/gitops/components/application/ApplicationSetDetailsPage.scss new file mode 100644 index 00000000..825e82d6 --- /dev/null +++ b/src/gitops/components/application/ApplicationSetDetailsPage.scss @@ -0,0 +1,219 @@ +.application-set-details-page { + &__main-section { + // PatternFly page main section styles + } + + &__body { + // PatternFly flex layout styles + } + + &__pane-body { + // Console pane body styles + } + + &__grid { + // PatternFly grid styles + } + + &__grid-item { + // PatternFly grid item styles + } + + &__header { + margin-bottom: 24px; + padding-left: 24px; + padding-top: 24px; + + &-title { + font-size: 20px; + font-weight: 600; + margin-bottom: 16px; + } + } + + &__content { + padding-left: 24px; + } + + &__conditions { + margin-top: 32px; + + &-title { + font-weight: 700; + font-size: 24px; + margin-bottom: 20px; + margin-top: 8px; + } + + &-table { + width: 100%; + border-top: 1px solid #393F44; + margin-bottom: 0; + + &-header { + display: flex; + font-weight: 600; + font-size: 16px; + padding: 16px 0 8px 0; + + &-cell { + text-align: left; + + &--type { + flex: 2; + padding-left: 0; + } + + &--status { + flex: 1; + } + + &--updated { + flex: 2; + } + + &--reason { + flex: 2; + } + + &--message { + flex: 4; + } + } + } + + &-row { + display: flex; + font-size: 15px; + padding: 16px 0; + align-items: flex-start; + border-top: 1px solid #393F44; + + &:first-child { + border-top: none; + } + + &-cell { + text-align: left; + + &--type { + flex: 2; + padding-left: 0; + } + + &--status { + flex: 1; + } + + &--updated { + flex: 2; + display: flex; + align-items: center; + } + + &--reason { + flex: 2; + } + + &--message { + flex: 4; + } + } + } + } + } + + &__generators { + &-container { + display: flex; + flex-direction: column; + gap: 16px; + } + + &-item { + border: 1px solid #393F44; + border-radius: 8px; + padding: 16px; + background-color: #212427; + + &-header { + display: flex; + align-items: center; + margin-bottom: 12px; + + &-icon { + width: 24px; + height: 24px; + background-color: #73bcf7; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 8px; + font-size: 12px; + font-weight: bold; + color: #003a70; + } + + &-title { + font-weight: 600; + font-size: 16px; + } + } + + &-content { + display: flex; + flex-direction: column; + gap: 8px; + + &-row { + display: flex; + align-items: center; + + &-label { + font-weight: 500; + min-width: 80px; + color: #8a8d90; + } + + &-value { + color: #73bcf7; + text-decoration: underline; + cursor: pointer; + } + } + } + } + } + + &__yaml-editor { + &-header { + &-buttons { + // YAML editor header buttons styles + } + + &-shortcuts { + // YAML editor shortcuts styles + + &-link { + // YAML editor shortcuts link styles + } + } + } + + &-content { + background: #1e1e1e; + color: #d4d4d4; + font-family: monospace; + font-size: 14px; + border-radius: 4px; + padding: 0; + + pre { + margin: 0; + padding: 16px; + overflow: auto; + } + } + } +} diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index 02a5b138..832c36b0 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -5,22 +5,15 @@ import { ApplicationSetKind, ApplicationSetModel } from '../../models/Applicatio import { Spinner, Badge, - Label, - LabelGroup, - DescriptionList, Tabs, Tab, TabTitleText, - Button, } from '@patternfly/react-core'; -import { PencilAltIcon } from '@patternfly/react-icons'; -import * as _ from 'lodash'; import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; import ResourceDetailsTitle from '../../utils/components/DetailsPageTitle/ResourceDetailsTitle'; -import { useLabelsModal, useAnnotationsModal } from '@openshift-console/dynamic-plugin-sdk'; - -import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; import ApplicationList from '../shared/ApplicationList'; +import ResourceDetailsAttributes from '../../utils/components/ResourceDetails/ResourceDetailsAttributes'; +import './ApplicationSetDetailsPage.scss'; const ApplicationSetDetailsPage: React.FC = () => { const { name, ns } = useParams<{ name: string; ns: string }>(); @@ -37,8 +30,6 @@ const ApplicationSetDetailsPage: React.FC = () => { }); const [actions] = useApplicationSetActionsProvider(appSet); - const launchLabelsModal = useLabelsModal(appSet); - const launchAnnotationsModal = useAnnotationsModal(appSet); if (loadError) return
Error loading ApplicationSet details.
; if (!loaded || !appSet) return ; @@ -50,13 +41,8 @@ const ApplicationSetDetailsPage: React.FC = () => { setActiveTabKey(tabIndex); }; - const labelItems = metadata.labels || {}; - const annotationItems = metadata.annotations || {}; - // Helper to count object keys - const countAnnotations = Object.keys(annotationItems).length; - return ( -
+
{ /> {/* Main Content */} -
+
{/* Tabs Section */} -
+
Details} className="pf-v6-c-tab-content"> -
-
-
-
-

Argo CD ApplicationSet details

+
+
+
+
+

Argo CD ApplicationSet details

-
- -
-
-
-
Name
-
-
-
-
-
{metadata.name}
-
-
-
- -
-
-
-
Namespace
-
-
-
-
-
- -
-
-
-
- -
-
- Labels -
-
-
- -
- {_.isEmpty(labelItems) ? ( - No labels - ) : ( - - {Object.entries(labelItems).map(([key, value]) => ( - - ))} - - )} -
-
-
-
- - {/* Annotations Section - matches Console style */} - - -
-
-
- -
Created at
-
-
-
-
-
- -
-
-
-
- -
-
-
-
Status
-
-
-
-
-
- Healthy -
-
-
-
- -
-
-
-
Generated Apps
-
-
-
-
-
- 3 applications -
-
-
-
- - {/* Generators Section */} -
-
-
-
Generators
-
-
-
-
-
- 1 generators -
-
-
-
- - {/* App Project Section (blue badge, no extra Created at) */} -
-
-
-
App Project
-
-
-
-
-
- AP default -
-
-
-
- -
-
-
-
Repository
-
-
-
- -
-
-
+
+ {/* Conditions Section */} {status.conditions && status.conditions.length > 0 && ( -
-
Conditions
-
-
-
-
Type
-
Status
-
Updated
-
Reason
-
Message
+
+
Conditions
+
+
+
Type
+
Status
+
Updated
+
Reason
+
Message
-
{status.conditions.map((condition: any, index: number) => ( -
-
{condition.type}
-
{condition.status}
-
+
+
{condition.type}
+
{condition.status}
+
-
{condition.reason || ''}
-
{condition.message || ''}
+
{condition.reason || ''}
+
{condition.message || ''}
- {index !== status.conditions.length - 1 && ( -
- )} ))}
@@ -354,10 +113,10 @@ const ApplicationSetDetailsPage: React.FC = () => { YAML} className="pf-v6-c-tab-content"> -
-
-
-
+
+
+
+
@@ -368,75 +127,58 @@ const ApplicationSetDetailsPage: React.FC = () => {
- -
-
{JSON.stringify(appSet, null, 2)}
+
+
{JSON.stringify(appSet, null, 2)}
Generators} className="pf-v6-c-tab-content"> -
-
-
-

Generators

+
+
+
+

Generators

-
-
+
+
{appSet.spec?.generators?.map((generator: any, index: number) => { const generatorType = Object.keys(generator)[0]; const generatorData = generator[generatorType]; return ( -
-
-
+
+
+
{generatorType.charAt(0).toUpperCase()}
- {generatorType} + {generatorType}
{/* Render different generator types */} {generatorType === 'git' && ( -
+
{generatorData.repoURL && ( -
- Repository: - +
+ Repository: + {generatorData.repoURL}
)} {generatorData.revision && ( -
- Revision: +
+ Revision: {generatorData.revision}
)} {generatorData.directories && ( -
- Directories: +
+ Directories: {generatorData.directories.length} directory(ies)
)} @@ -444,10 +186,10 @@ const ApplicationSetDetailsPage: React.FC = () => { )} {generatorType === 'clusterDecisionResource' && ( -
+
{generatorData.configMapRef && ( -
- ConfigMap: +
+ ConfigMap: {generatorData.configMapRef.name}
)} @@ -455,7 +197,7 @@ const ApplicationSetDetailsPage: React.FC = () => { )} {generatorType === 'matrix' && ( -
+
Matrix generator with {Object.keys(generatorData).length} generators
@@ -463,10 +205,10 @@ const ApplicationSetDetailsPage: React.FC = () => { )} {generatorType === 'clusters' && ( -
+
{generatorData.selector && ( -
- Selector: +
+ Selector: {JSON.stringify(generatorData.selector)} @@ -516,7 +258,7 @@ const ApplicationSetDetailsPage: React.FC = () => { Applications} className="pf-v6-c-tab-content"> -
+
{ Events} className="pf-v6-c-tab-content"> -
-
-
-
-

Events

+
+
+
+
+

Events

-
+
{status.conditions && status.conditions.length > 0 ? (
{status.conditions.map((condition: any, index: number) => ( diff --git a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx new file mode 100644 index 00000000..334f3efd --- /dev/null +++ b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx @@ -0,0 +1,476 @@ +import * as React from 'react'; +import { + DescriptionList, + DescriptionListGroup, + DescriptionListDescription, + DescriptionListTermHelpText, + DescriptionListTermHelpTextButton, + LabelGroup, + Label, + Badge, + Popover +} from '@patternfly/react-core'; +import { PencilAltIcon } from '@patternfly/react-icons'; +import { ResourceLink, Timestamp, useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; +import { useLabelsModal, useAnnotationsModal } from '@openshift-console/dynamic-plugin-sdk'; +import * as _ from 'lodash'; + +interface ResourceDetailsAttributesProps { + metadata: { + name?: string; + namespace?: string; + labels?: Record; + annotations?: Record; + creationTimestamp?: string; + ownerReferences?: Array<{ + name: string; + kind: string; + apiVersion: string; + }>; + }; + resource: any; // The full resource object for modal hooks + showOwner?: boolean; + showStatus?: boolean; + showGeneratedApps?: boolean; + showGenerators?: boolean; + showAppProject?: boolean; + showRepository?: boolean; +} + +const ResourceDetailsAttributes: React.FC = ({ + metadata, + resource, + showOwner = true, + showStatus = false, + showGeneratedApps = false, + showGenerators = false, + showAppProject = false, + showRepository = false, +}) => { + const launchLabelsModal = useLabelsModal(resource); + const launchAnnotationsModal = useAnnotationsModal(resource); + + // Check if user has permission to update the resource + // This enables/disables the Labels and Annotations edit buttons based on user permissions + const [canUpdate] = useAccessReview({ + group: 'argoproj.io', + verb: 'patch', + resource: 'applicationsets', + name: metadata.name, + namespace: metadata.namespace, + }); + + const labelItems = metadata.labels || {}; + const annotationItems = metadata.annotations || {}; + const countAnnotations = Object.keys(annotationItems).length; + + return ( + + {/* Name */} + + + Name
} + bodyContent={ +
+
+ Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. +
+ +
+ Application {'>'} metadata {'>'} name +
+
+ } + > + + Name + + + + + {metadata.name} + + + + {/* Namespace */} + + + Namespace
} + bodyContent={ +
+
+ Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty. +
+
+ Must be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces +
+
+ Application {'>'} metadata {'>'} namespace +
+
+ } + > + + Namespace + + + + + + + + + {/* Labels */} + + + Labels
} + bodyContent={ +
+
+ Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. +
+ +
+ Application {'>'} metadata {'>'} labels +
+
+ } + > + + Labels + + + + +
+ {canUpdate && ( + + )} +
+ {_.isEmpty(labelItems) ? ( + No labels + ) : ( + + {Object.entries(labelItems).map(([key, value]) => ( + + ))} + + )} +
+
+
+ + + {/* Annotations */} + + + Annotations
} + bodyContent={ +
+
+ Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. +
+ +
+ Application {'>'} metadata {'>'} annotations +
+
+ } + > + + Annotations + + + + + + + + + {/* Created at */} + + + Created at
} + bodyContent={ +
+
+ CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. +
+
+ Populated by the system. Read-only. Null for lists. +
+ +
+ Application {'>'} metadata {'>'} creationTimestamp +
+
+ } + > + + Created at + + + + + + + + + {/* Owner - conditional */} + {showOwner && ( + + + Owner
} + bodyContent={ +
+
+ List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller. +
+
+ Application {'>'} metadata {'>'} ownerReferences +
+
+ } + > + + Owner + + + + + {metadata.ownerReferences && metadata.ownerReferences.length > 0 ? ( + + ) : ( + 'No owner' + )} + + + )} + + {/* Status - conditional */} + {showStatus && ( + + + Status
} + bodyContent={ +
+
+ Current status of the resource +
+
+ Application {'>'} status +
+
+ } + > + + Status + + + + + Healthy + + + )} + + {/* Generated Apps - conditional */} + {showGeneratedApps && ( + + + Generated Apps
} + bodyContent={ +
+
+ Number of applications generated by this ApplicationSet +
+
+ ApplicationSet {'>'} status {'>'} applications +
+
+ } + > + + Generated Apps + + + + + 3 applications + + + )} + + {/* Generators - conditional */} + {showGenerators && ( + + + Generators
} + bodyContent={ +
+
+ Number of generators configured in this ApplicationSet +
+
+ ApplicationSet {'>'} spec {'>'} generators +
+
+ } + > + + Generators + + + + + 1 generators + + + )} + + {/* App Project - conditional */} + {showAppProject && ( + + + App Project
} + bodyContent={ +
+
+ Argo CD project that this ApplicationSet belongs to +
+
+ ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} project +
+
+ } + > + + App Project + + + + + AP default + + + )} + + {/* Repository - conditional */} + {showRepository && ( + + + Repository
} + bodyContent={ +
+
+ Git repository URL where the ApplicationSet configuration is stored +
+
+ ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} source {'>'} repoURL +
+
+ } + > + + Repository + + + + + + https://github.com/aal/309/argocd-test-nested.git + + + + )} + + ); +}; + +export default ResourceDetailsAttributes; From 4c9b72d80a63b66108df5575075657eb1b9fdaa0 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 28 Aug 2025 18:29:19 -0400 Subject: [PATCH 07/16] proper logic for matrix genertar Signed-off-by: Atif Ali refactor and restructure code Signed-off-by: Atif Ali --- .../application/ApplicationSetDetailsPage.tsx | 364 +----------------- .../components/appset/AppSetDetailsTab.scss | 117 ++++++ .../components/appset/AppSetDetailsTab.tsx | 88 +++++ .../components/appset/AppSetNavPage.scss | 19 + .../components/appset/AppSetNavPage.tsx | 90 +++++ src/gitops/components/appset/AppsTab.scss | 6 + src/gitops/components/appset/AppsTab.tsx | 28 ++ src/gitops/components/appset/EventsTab.scss | 7 + src/gitops/components/appset/EventsTab.tsx | 106 +++++ src/gitops/components/appset/Generators.tsx | 49 +++ .../components/appset/GeneratorsTab.scss | 7 + .../components/appset/GeneratorsTab.tsx | 55 +++ src/gitops/components/appset/README.md | 89 +++++ src/gitops/components/appset/YAMLTab.scss | 51 +++ src/gitops/components/appset/YAMLTab.tsx | 19 + .../appset/generators/ClusterGenerator.tsx | 36 ++ .../appset/generators/GeneratorView.tsx | 25 ++ .../appset/generators/Generators.scss | 55 +++ .../appset/generators/GenericGenerator.tsx | 35 ++ .../appset/generators/GitGenerator.tsx | 45 +++ .../appset/generators/ListGenerator.tsx | 52 +++ .../appset/generators/MatrixGenerator.tsx | 22 ++ .../appset/generators/MergeGenerator.tsx | 32 ++ .../appset/generators/UnionGenerator.tsx | 22 ++ src/gitops/components/appset/index.ts | 16 + .../ResourceDetailsAttributes.tsx | 6 +- 26 files changed, 1082 insertions(+), 359 deletions(-) create mode 100644 src/gitops/components/appset/AppSetDetailsTab.scss create mode 100644 src/gitops/components/appset/AppSetDetailsTab.tsx create mode 100644 src/gitops/components/appset/AppSetNavPage.scss create mode 100644 src/gitops/components/appset/AppSetNavPage.tsx create mode 100644 src/gitops/components/appset/AppsTab.scss create mode 100644 src/gitops/components/appset/AppsTab.tsx create mode 100644 src/gitops/components/appset/EventsTab.scss create mode 100644 src/gitops/components/appset/EventsTab.tsx create mode 100644 src/gitops/components/appset/Generators.tsx create mode 100644 src/gitops/components/appset/GeneratorsTab.scss create mode 100644 src/gitops/components/appset/GeneratorsTab.tsx create mode 100644 src/gitops/components/appset/README.md create mode 100644 src/gitops/components/appset/YAMLTab.scss create mode 100644 src/gitops/components/appset/YAMLTab.tsx create mode 100644 src/gitops/components/appset/generators/ClusterGenerator.tsx create mode 100644 src/gitops/components/appset/generators/GeneratorView.tsx create mode 100644 src/gitops/components/appset/generators/Generators.scss create mode 100644 src/gitops/components/appset/generators/GenericGenerator.tsx create mode 100644 src/gitops/components/appset/generators/GitGenerator.tsx create mode 100644 src/gitops/components/appset/generators/ListGenerator.tsx create mode 100644 src/gitops/components/appset/generators/MatrixGenerator.tsx create mode 100644 src/gitops/components/appset/generators/MergeGenerator.tsx create mode 100644 src/gitops/components/appset/generators/UnionGenerator.tsx create mode 100644 src/gitops/components/appset/index.ts diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index 832c36b0..212577fe 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -1,368 +1,16 @@ import * as React from 'react'; -import { useK8sWatchResource, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; import { useParams } from 'react-router-dom-v5-compat'; -import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; -import { - Spinner, - Badge, - Tabs, - Tab, - TabTitleText, -} from '@patternfly/react-core'; -import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; -import ResourceDetailsTitle from '../../utils/components/DetailsPageTitle/ResourceDetailsTitle'; -import ApplicationList from '../shared/ApplicationList'; -import ResourceDetailsAttributes from '../../utils/components/ResourceDetails/ResourceDetailsAttributes'; -import './ApplicationSetDetailsPage.scss'; +import AppSetNavPage from '../appset/AppSetNavPage'; const ApplicationSetDetailsPage: React.FC = () => { const { name, ns } = useParams<{ name: string; ns: string }>(); - const [activeTabKey, setActiveTabKey] = React.useState(0); - - const [appSet, loaded, loadError] = useK8sWatchResource({ - groupVersionKind: { - group: 'argoproj.io', - version: 'v1alpha1', - kind: 'ApplicationSet', - }, - name, - namespace: ns, - }); - - const [actions] = useApplicationSetActionsProvider(appSet); - - if (loadError) return
Error loading ApplicationSet details.
; - if (!loaded || !appSet) return ; - - const metadata = appSet.metadata || {}; - const status = appSet.status || {}; - - const handleTabClick = (event: React.MouseEvent, tabIndex: string | number) => { - setActiveTabKey(tabIndex); - }; return ( -
- - - {/* Main Content */} -
- {/* Tabs Section */} -
- - Details} className="pf-v6-c-tab-content"> -
-
-
-
-

Argo CD ApplicationSet details

-
-
- - - {/* Conditions Section */} - {status.conditions && status.conditions.length > 0 && ( -
-
Conditions
-
-
-
Type
-
Status
-
Updated
-
Reason
-
Message
-
- {status.conditions.map((condition: any, index: number) => ( - -
-
{condition.type}
-
{condition.status}
-
- -
-
{condition.reason || ''}
-
{condition.message || ''}
-
-
- ))} -
-
- )} -
-
-
-
-
- - YAML} className="pf-v6-c-tab-content"> -
-
-
-
- - - -
-
- Shortcuts -
-
-
-
{JSON.stringify(appSet, null, 2)}
-
-
-
-
- - Generators} className="pf-v6-c-tab-content"> -
-
-
-

Generators

-
-
-
- {appSet.spec?.generators?.map((generator: any, index: number) => { - const generatorType = Object.keys(generator)[0]; - const generatorData = generator[generatorType]; - - return ( -
-
-
- {generatorType.charAt(0).toUpperCase()} -
- {generatorType} -
- - {/* Render different generator types */} - {generatorType === 'git' && ( -
- {generatorData.repoURL && ( -
- Repository: - - {generatorData.repoURL} - -
- )} - {generatorData.revision && ( -
- Revision: - {generatorData.revision} -
- )} - {generatorData.directories && ( -
- Directories: - {generatorData.directories.length} directory(ies) -
- )} -
- )} - - {generatorType === 'clusterDecisionResource' && ( -
- {generatorData.configMapRef && ( -
- ConfigMap: - {generatorData.configMapRef.name} -
- )} -
- )} - - {generatorType === 'matrix' && ( -
-
- Matrix generator with {Object.keys(generatorData).length} generators -
-
- )} - - {generatorType === 'clusters' && ( -
- {generatorData.selector && ( -
- Selector: - - {JSON.stringify(generatorData.selector)} - -
- )} -
- )} -
- ); - })} - - {(!appSet.spec?.generators || appSet.spec.generators.length === 0) && ( -
-
- ⚙️ -
-
-
No Generators
-
- This ApplicationSet has no generators configured. -
-
-
- )} -
-
-
-
-
- - Applications} className="pf-v6-c-tab-content"> -
-
- -
-
-
- - Events} className="pf-v6-c-tab-content"> -
-
-
-
-

Events

-
-
- {status.conditions && status.conditions.length > 0 ? ( -
- {status.conditions.map((condition: any, index: number) => ( -
-
-
-
- {condition.status === 'True' ? '✓' : '✗'} -
- - {condition.type} - -
- - {condition.status} - -
-
- {condition.message || 'No message available'} -
- {condition.lastTransitionTime && ( -
- Last updated: {new Date(condition.lastTransitionTime).toLocaleString()} -
- )} -
- ))} -
- ) : ( -
-
- 📊 -
-
-
No Events
-
- No events have been recorded for this ApplicationSet. -
-
-
- )} -
-
-
-
-
-
-
-
-
+ ); }; diff --git a/src/gitops/components/appset/AppSetDetailsTab.scss b/src/gitops/components/appset/AppSetDetailsTab.scss new file mode 100644 index 00000000..2d0ae962 --- /dev/null +++ b/src/gitops/components/appset/AppSetDetailsTab.scss @@ -0,0 +1,117 @@ +.application-set-details-page { + &__grid { + display: grid; + grid-template-columns: 1fr; + gap: 20px; + } + + &__grid-item { + background: #212427; + border: 1px solid #393F44; + border-radius: 8px; + padding: 20px; + } + + &__header { + margin-bottom: 20px; + } + + &__header-title { + font-size: 18px; + font-weight: 600; + color: #ffffff; + margin: 0; + } + + &__content { + color: #ffffff; + } + + &__conditions { + margin-top: 20px; + } + + &__conditions-title { + font-size: 16px; + font-weight: 600; + color: #ffffff; + margin-bottom: 12px; + } + + &__conditions-table { + border: 1px solid #393F44; + border-radius: 6px; + overflow: hidden; + } + + &__conditions-table-header { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 2fr; + background: #1a1d21; + border-bottom: 1px solid #393F44; + } + + &__conditions-table-header-cell { + padding: 12px; + font-weight: 600; + color: #ffffff; + font-size: 14px; + + &--type { + grid-column: 1; + } + + &--status { + grid-column: 2; + } + + &--updated { + grid-column: 3; + } + + &--reason { + grid-column: 4; + } + + &--message { + grid-column: 5; + } + } + + &__conditions-table-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 2fr; + border-bottom: 1px solid #393F44; + + &:last-child { + border-bottom: none; + } + } + + &__conditions-table-row-cell { + padding: 12px; + color: #ffffff; + font-size: 14px; + + &--type { + grid-column: 1; + font-weight: 500; + } + + &--status { + grid-column: 2; + } + + &--updated { + grid-column: 3; + } + + &--reason { + grid-column: 4; + } + + &--message { + grid-column: 5; + } + } +} diff --git a/src/gitops/components/appset/AppSetDetailsTab.tsx b/src/gitops/components/appset/AppSetDetailsTab.tsx new file mode 100644 index 00000000..306fdcfa --- /dev/null +++ b/src/gitops/components/appset/AppSetDetailsTab.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { ApplicationSetKind } from '../../models/ApplicationSetModel'; +import { + Badge, + PageSection, + Title, + DescriptionList, + Grid, + GridItem, +} from '@patternfly/react-core'; +import ResourceDetailsAttributes from '../../utils/components/ResourceDetails/ResourceDetailsAttributes'; +import './AppSetDetailsTab.scss'; + +type AppSetDetailsTabProps = { + obj?: ApplicationSetKind; + namespace?: string; + name?: string; +}; + +const AppSetDetailsTab: React.FC = ({ obj }) => { + if (!obj) return null; + + const metadata = obj.metadata || {}; + const status = obj.status || {}; + + return ( + <> + + + ApplicationSet details + + + + + + + + + + + {status.conditions && status.conditions.length > 0 && ( + + + Conditions + +
+
+
Type
+
Status
+
Updated
+
Reason
+
Message
+
+ {status.conditions.map((condition: any, index: number) => ( + +
+
{condition.type}
+
+ + {condition.status} + +
+
+ +
+
{condition.reason || ''}
+
{condition.message || ''}
+
+
+ ))} +
+
+ )} + + ); +}; + +export default AppSetDetailsTab; diff --git a/src/gitops/components/appset/AppSetNavPage.scss b/src/gitops/components/appset/AppSetNavPage.scss new file mode 100644 index 00000000..f0c80b84 --- /dev/null +++ b/src/gitops/components/appset/AppSetNavPage.scss @@ -0,0 +1,19 @@ +.application-set-details-page { + &__main-section { + display: flex; + flex-direction: column; + height: 100%; + } + + &__body { + flex: 1; + display: flex; + flex-direction: column; + } + + &__pane-body { + flex: 1; + padding: 20px; + overflow-y: auto; + } +} diff --git a/src/gitops/components/appset/AppSetNavPage.tsx b/src/gitops/components/appset/AppSetNavPage.tsx new file mode 100644 index 00000000..42eef4e5 --- /dev/null +++ b/src/gitops/components/appset/AppSetNavPage.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; +import { + Spinner, + Bullseye, + Tabs, + Tab, + TabTitleText, +} from '@patternfly/react-core'; +import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; +import ResourceDetailsTitle from '../../utils/components/DetailsPageTitle/ResourceDetailsTitle'; +import AppSetDetailsTab from './AppSetDetailsTab'; +import GeneratorsTab from './GeneratorsTab'; +import AppsTab from './AppsTab'; +import EventsTab from './EventsTab'; +import YAMLTab from './YAMLTab'; +import './AppSetNavPage.scss'; + +type AppSetPageProps = { + name: string; + namespace: string; + kind: string; +}; + +const AppSetNavPage: React.FC = ({ name, namespace, kind }) => { + const [activeTabKey, setActiveTabKey] = React.useState(0); + + const [appSet, loaded, loadError] = useK8sWatchResource({ + groupVersionKind: { + group: 'argoproj.io', + version: 'v1alpha1', + kind: 'ApplicationSet', + }, + name, + namespace, + }); + + const [actions] = useApplicationSetActionsProvider(appSet); + + if (loadError) return
Error loading ApplicationSet details.
; + if (!loaded || !appSet) return ( + + + + ); + + const handleTabClick = (event: React.MouseEvent, tabIndex: string | number) => { + setActiveTabKey(tabIndex); + }; + + return ( +
+ + +
+
+ + Details} className="pf-v6-c-tab-content"> + + + YAML} className="pf-v6-c-tab-content"> + + + Generators} className="pf-v6-c-tab-content"> + + + Applications} className="pf-v6-c-tab-content"> + + + Events} className="pf-v6-c-tab-content"> + + + +
+
+
+ ); +}; + +export default AppSetNavPage; diff --git a/src/gitops/components/appset/AppsTab.scss b/src/gitops/components/appset/AppsTab.scss new file mode 100644 index 00000000..f0607610 --- /dev/null +++ b/src/gitops/components/appset/AppsTab.scss @@ -0,0 +1,6 @@ +.application-set-details-page { + &__apps-container { + display: flex; + flex-direction: column; + } +} diff --git a/src/gitops/components/appset/AppsTab.tsx b/src/gitops/components/appset/AppsTab.tsx new file mode 100644 index 00000000..f0ff1245 --- /dev/null +++ b/src/gitops/components/appset/AppsTab.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { ApplicationSetKind } from '../../models/ApplicationSetModel'; +import { PageSection } from '@patternfly/react-core'; +import ApplicationList from '../shared/ApplicationList'; +import './AppsTab.scss'; + +type AppsTabProps = { + obj?: ApplicationSetKind; + namespace?: string; + name?: string; +}; + +const AppsTab: React.FC = ({ obj, namespace }) => { + if (!obj || !namespace) return null; + + return ( + + + + ); +}; + +export default AppsTab; diff --git a/src/gitops/components/appset/EventsTab.scss b/src/gitops/components/appset/EventsTab.scss new file mode 100644 index 00000000..9d851721 --- /dev/null +++ b/src/gitops/components/appset/EventsTab.scss @@ -0,0 +1,7 @@ +.application-set-details-page { + &__events-container { + display: flex; + flex-direction: column; + gap: 12px; + } +} diff --git a/src/gitops/components/appset/EventsTab.tsx b/src/gitops/components/appset/EventsTab.tsx new file mode 100644 index 00000000..c5735633 --- /dev/null +++ b/src/gitops/components/appset/EventsTab.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { ApplicationSetKind } from '../../models/ApplicationSetModel'; +import { + Badge, + PageSection, + Title, +} from '@patternfly/react-core'; +import './EventsTab.scss'; + +type EventsTabProps = { + obj?: ApplicationSetKind; + namespace?: string; + name?: string; +}; + +const EventsTab: React.FC = ({ obj }) => { + if (!obj) return null; + + const status = obj.status || {}; + + return ( + + + Events + + {status.conditions && status.conditions.length > 0 ? ( +
+ {status.conditions.map((condition: any, index: number) => ( +
+
+
+
+ {condition.status === 'True' ? '✓' : '✗'} +
+ + {condition.type} + +
+ + {condition.status} + +
+
+ {condition.message || 'No message available'} +
+ {condition.lastTransitionTime && ( +
+ Last updated: {new Date(condition.lastTransitionTime).toLocaleString()} +
+ )} +
+ ))} +
+ ) : ( +
+
+ 📊 +
+
+
No Events
+
+ No events have been recorded for this ApplicationSet. +
+
+
+ )} +
+ ); +}; + +export default EventsTab; diff --git a/src/gitops/components/appset/Generators.tsx b/src/gitops/components/appset/Generators.tsx new file mode 100644 index 00000000..0dd7bce7 --- /dev/null +++ b/src/gitops/components/appset/Generators.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import GitGenerator from './generators/GitGenerator'; +import ListGenerator from './generators/ListGenerator'; +import MatrixGenerator from './generators/MatrixGenerator'; +import UnionGenerator from './generators/UnionGenerator'; +import MergeGenerator from './generators/MergeGenerator'; +import ClusterGenerator from './generators/ClusterGenerator'; +import GenericGenerator from './generators/GenericGenerator'; + +interface GeneratorsProps { + generators: any[]; +} + +const Generators: React.FC = ({ generators }) => { + const renderGenerator = (generator: any, index: number) => { + const generatorType = Object.keys(generator)[0]; + const generatorData = generator[generatorType]; + + switch (generatorType) { + case 'clusters': + return ; + case 'git': + return ; + case 'list': + return ; + case 'merge': + return ; + case 'matrix': + return ; + case 'union': + return ; + default: + return ; + } + }; + + return ( +
+ {generators.map((generator, index) => ( +
+ {renderGenerator(generator, index)} +
+
+ ))} +
+ ); +}; + +export default Generators; diff --git a/src/gitops/components/appset/GeneratorsTab.scss b/src/gitops/components/appset/GeneratorsTab.scss new file mode 100644 index 00000000..c7fe0f7a --- /dev/null +++ b/src/gitops/components/appset/GeneratorsTab.scss @@ -0,0 +1,7 @@ +.application-set-details-page { + &__generators-container { + display: flex; + flex-direction: column; + gap: 16px; + } +} diff --git a/src/gitops/components/appset/GeneratorsTab.tsx b/src/gitops/components/appset/GeneratorsTab.tsx new file mode 100644 index 00000000..43d611b0 --- /dev/null +++ b/src/gitops/components/appset/GeneratorsTab.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { ApplicationSetKind } from '../../models/ApplicationSetModel'; +import { PageSection } from '@patternfly/react-core'; +import Generators from './Generators'; +import './GeneratorsTab.scss'; + +type GeneratorsTabProps = { + obj?: ApplicationSetKind; + namespace?: string; + name?: string; +}; + +const GeneratorsTab: React.FC = ({ obj }) => { + if (!obj) return null; + + return ( + + {obj.spec?.generators && obj.spec.generators.length > 0 ? ( + + ) : ( +
+
+ ⚙️ +
+
+
No Generators
+
+ This ApplicationSet has no generators configured. +
+
+
+ )} +
+ ); +}; + +export default GeneratorsTab; diff --git a/src/gitops/components/appset/README.md b/src/gitops/components/appset/README.md new file mode 100644 index 00000000..1634ab0a --- /dev/null +++ b/src/gitops/components/appset/README.md @@ -0,0 +1,89 @@ +# ApplicationSet Components + +This directory contains the refactored ApplicationSet components following the modular structure used in the gitops-admin-plugin. + +## Structure + +``` +appset/ +├── AppSetNavPage.tsx # Main navigation page with tabs +├── AppSetDetailsTab.tsx # Details tab component +├── GeneratorsTab.tsx # Generators tab component +├── AppsTab.tsx # Applications tab component +├── EventsTab.tsx # Events tab component +├── YAMLTab.tsx # YAML tab component +├── Generators.tsx # Main generators component +├── generators/ # Individual generator components +│ ├── GitGenerator.tsx +│ ├── ListGenerator.tsx +│ ├── ClusterGenerator.tsx +│ ├── MatrixGenerator.tsx +│ ├── UnionGenerator.tsx +│ ├── MergeGenerator.tsx +│ ├── GenericGenerator.tsx +│ └── SubGenerator.tsx +├── hooks/ # Custom hooks (if needed) +└── index.ts # Export file +``` + +## Components + +### AppSetNavPage +The main navigation component that handles tab switching and renders the appropriate tab content. + +### Tab Components +- **AppSetDetailsTab**: Displays basic ApplicationSet information and conditions +- **GeneratorsTab**: Shows the generators configuration +- **AppsTab**: Lists applications generated by the ApplicationSet +- **EventsTab**: Displays events and conditions +- **YAMLTab**: Shows the YAML representation of the ApplicationSet + +### Generator Components +Each generator type has its own component: +- **GitGenerator**: For git-based generators +- **ListGenerator**: For list-based generators +- **ClusterGenerator**: For cluster-based generators +- **MatrixGenerator**: For matrix generators with sub-generators +- **UnionGenerator**: For union generators with sub-generators +- **MergeGenerator**: For merge generators with sub-generators +- **GenericGenerator**: For unknown generator types +- **SubGenerator**: For rendering sub-generators within matrix/union/merge generators + +## Usage + +The main entry point is `AppSetNavPage` which is used in the `ApplicationSetDetailsPage`: + +```tsx +import AppSetNavPage from '../appset/AppSetNavPage'; + +const ApplicationSetDetailsPage: React.FC = () => { + const { name, ns } = useParams<{ name: string; ns: string }>(); + + return ( + + ); +}; +``` + +## Styling + +Each component has its own SCSS file for styling: +- `AppSetNavPage.scss` +- `AppSetDetailsTab.scss` +- `GeneratorsTab.scss` +- `AppsTab.scss` +- `EventsTab.scss` +- `YAMLTab.scss` +- `generators/Generators.scss` + +## Benefits of This Structure + +1. **Modularity**: Each component has a single responsibility +2. **Reusability**: Components can be easily reused or modified +3. **Maintainability**: Easier to maintain and debug individual components +4. **Consistency**: Follows the same pattern as gitops-admin-plugin +5. **Scalability**: Easy to add new generator types or modify existing ones diff --git a/src/gitops/components/appset/YAMLTab.scss b/src/gitops/components/appset/YAMLTab.scss new file mode 100644 index 00000000..ff8e15d3 --- /dev/null +++ b/src/gitops/components/appset/YAMLTab.scss @@ -0,0 +1,51 @@ +.application-set-details-page { + &__yaml-editor { + background: #1a1d21; + border: 1px solid #393F44; + border-radius: 8px; + overflow: hidden; + } + + &__yaml-editor-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #212427; + border-bottom: 1px solid #393F44; + } + + &__yaml-editor-header-buttons { + display: flex; + gap: 8px; + } + + &__yaml-editor-header-shortcuts { + font-size: 12px; + } + + &__yaml-editor-header-shortcuts-link { + color: #0066cc; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + &__yaml-editor-content { + padding: 16px; + max-height: 600px; + overflow-y: auto; + + pre { + margin: 0; + color: #ffffff; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 12px; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + } + } +} diff --git a/src/gitops/components/appset/YAMLTab.tsx b/src/gitops/components/appset/YAMLTab.tsx new file mode 100644 index 00000000..42f2ace5 --- /dev/null +++ b/src/gitops/components/appset/YAMLTab.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { ApplicationSetKind } from '../../models/ApplicationSetModel'; +import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; + +type YAMLTabProps = { + obj?: ApplicationSetKind; + namespace?: string; + name?: string; +}; + +const YAMLTab: React.FC = ({ obj }) => { + if (!obj) return null; + + return ( + + ); +}; + +export default YAMLTab; diff --git a/src/gitops/components/appset/generators/ClusterGenerator.tsx b/src/gitops/components/appset/generators/ClusterGenerator.tsx new file mode 100644 index 00000000..892654d5 --- /dev/null +++ b/src/gitops/components/appset/generators/ClusterGenerator.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core'; +import { ClusterIcon } from '@patternfly/react-icons'; +import GeneratorView from './GeneratorView'; + +interface ClusterGeneratorProps { + generator: any; +} + +const ClusterGenerator: React.FC = ({ generator }) => { + return ( + } title="Cluster"> + + {generator.selector && ( + + Selector + +
+                {JSON.stringify(generator.selector, null, 2)}
+              
+
+
+ )} +
+
+ ); +}; + +export default ClusterGenerator; diff --git a/src/gitops/components/appset/generators/GeneratorView.tsx b/src/gitops/components/appset/generators/GeneratorView.tsx new file mode 100644 index 00000000..c98112bc --- /dev/null +++ b/src/gitops/components/appset/generators/GeneratorView.tsx @@ -0,0 +1,25 @@ +import { ReactNode } from 'react'; +import { Card, CardBody, CardTitle, Divider, Icon } from '@patternfly/react-core'; + +type GeneratorViewProps = { + title: string; + icon?: JSX.Element; + children?: ReactNode; +}; + +const GeneratorView = ({ title, icon, children }: GeneratorViewProps) => ( + + +
+ {icon && {icon}} + {title} +
+ {children && ( + + )} +
+ {children && {children}} +
+); + +export default GeneratorView; diff --git a/src/gitops/components/appset/generators/Generators.scss b/src/gitops/components/appset/generators/Generators.scss new file mode 100644 index 00000000..eecf52fe --- /dev/null +++ b/src/gitops/components/appset/generators/Generators.scss @@ -0,0 +1,55 @@ +.application-set-details-page { + &__generators-item { + background: #212427; + border: 1px solid #393F44; + border-radius: 8px; + padding: 16px; + } + + &__generators-item-header { + display: flex; + align-items: center; + margin-bottom: 12px; + } + + &__generators-item-header-icon { + width: 32px; + height: 32px; + background: #0066cc; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; + font-size: 14px; + font-weight: 600; + color: #ffffff; + } + + &__generators-item-header-title { + font-size: 16px; + font-weight: 600; + color: #ffffff; + } + + &__generators-item-content { + color: #ffffff; + } + + &__generators-item-content-row { + display: flex; + margin-bottom: 8px; + font-size: 14px; + } + + &__generators-item-content-row-label { + color: #8a8d90; + min-width: 120px; + margin-right: 8px; + } + + &__generators-item-content-row-value { + color: #ffffff; + word-break: break-all; + } +} diff --git a/src/gitops/components/appset/generators/GenericGenerator.tsx b/src/gitops/components/appset/generators/GenericGenerator.tsx new file mode 100644 index 00000000..a2c255f7 --- /dev/null +++ b/src/gitops/components/appset/generators/GenericGenerator.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core'; +import { QuestionCircleIcon } from '@patternfly/react-icons'; +import GeneratorView from './GeneratorView'; + +interface GenericGeneratorProps { + gentype: string; + generator: any; +} + +const GenericGenerator: React.FC = ({ gentype, generator }) => { + return ( + } title={`${gentype} Generator`}> + + + Configuration + +
+              {JSON.stringify(generator, null, 2)}
+            
+
+
+
+
+ ); +}; + +export default GenericGenerator; diff --git a/src/gitops/components/appset/generators/GitGenerator.tsx b/src/gitops/components/appset/generators/GitGenerator.tsx new file mode 100644 index 00000000..e1c60c86 --- /dev/null +++ b/src/gitops/components/appset/generators/GitGenerator.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core'; +import { GitAltIcon } from '@patternfly/react-icons'; +import GeneratorView from './GeneratorView'; + +interface GitGeneratorProps { + generator: any; +} + +const GitGenerator: React.FC = ({ generator }) => { + const generatorType = generator.files ? "File" : "Directory"; + + return ( + } title={`git (${generatorType})`}> + + {generator.repoURL && ( + + Repository + {generator.repoURL} + + )} + {generator.revision && ( + + Revision + {generator.revision} + + )} + {generator.directories && ( + + Directories + {generator.directories.length} directory(ies) + + )} + {generator.files && ( + + Files + {generator.files.length} file(s) + + )} + + + ); +}; + +export default GitGenerator; diff --git a/src/gitops/components/appset/generators/ListGenerator.tsx b/src/gitops/components/appset/generators/ListGenerator.tsx new file mode 100644 index 00000000..34be1854 --- /dev/null +++ b/src/gitops/components/appset/generators/ListGenerator.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { ExpandableSection, DataList, DataListItem, DataListCell, DataListItemRow, DataListItemCells } from '@patternfly/react-core'; +import { ListIcon } from '@patternfly/react-icons'; +import GeneratorView from './GeneratorView'; + +interface ListGeneratorProps { + generator: any; +} + +const ListGenerator: React.FC = ({ generator }) => { + const [isExpanded, setIsExpanded] = React.useState(false); + + const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => { + setIsExpanded(isExpanded); + }; + + const displayValue = (value: any) => { + if (value === undefined) return "null"; + else if (typeof value === "object") return JSON.stringify(value); + else return value; + }; + + return ( + } title="List"> + + {generator.elements && generator.elements.length > 0 && ( + + {generator.elements.map((item: any, rowIndex: number) => ( + + + ( + + {key}: {displayValue(val)} + + ))} + /> + + + ))} + + )} + + + ); +}; + +export default ListGenerator; diff --git a/src/gitops/components/appset/generators/MatrixGenerator.tsx b/src/gitops/components/appset/generators/MatrixGenerator.tsx new file mode 100644 index 00000000..5bf1461a --- /dev/null +++ b/src/gitops/components/appset/generators/MatrixGenerator.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { ThLargeIcon } from '@patternfly/react-icons'; +import GeneratorView from './GeneratorView'; +import Generators from '../Generators'; + +interface MatrixGeneratorProps { + generator: any; +} + +const MatrixGenerator: React.FC = ({ generator }) => { + return ( + <> + } title="Matrix" /> +
+
+ +
+ + ); +}; + +export default MatrixGenerator; diff --git a/src/gitops/components/appset/generators/MergeGenerator.tsx b/src/gitops/components/appset/generators/MergeGenerator.tsx new file mode 100644 index 00000000..23b76ba3 --- /dev/null +++ b/src/gitops/components/appset/generators/MergeGenerator.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core'; +import { ObjectGroupIcon } from '@patternfly/react-icons'; +import GeneratorView from './GeneratorView'; +import Generators from '../Generators'; + +interface MergeGeneratorProps { + generator: any; +} + +const MergeGenerator: React.FC = ({ generator }) => { + return ( + <> + } title="Merge"> + {generator.mergeKeys && ( + + + Merge Keys + {generator.mergeKeys.join(', ')} + + + )} + +
+
+ +
+ + ); +}; + +export default MergeGenerator; diff --git a/src/gitops/components/appset/generators/UnionGenerator.tsx b/src/gitops/components/appset/generators/UnionGenerator.tsx new file mode 100644 index 00000000..64d4c3c5 --- /dev/null +++ b/src/gitops/components/appset/generators/UnionGenerator.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { ThIcon } from '@patternfly/react-icons'; +import GeneratorView from './GeneratorView'; +import Generators from '../Generators'; + +interface UnionGeneratorProps { + generator: any; +} + +const UnionGenerator: React.FC = ({ generator }) => { + return ( + <> + } title="Union" /> +
+
+ +
+ + ); +}; + +export default UnionGenerator; diff --git a/src/gitops/components/appset/index.ts b/src/gitops/components/appset/index.ts new file mode 100644 index 00000000..63d61cb9 --- /dev/null +++ b/src/gitops/components/appset/index.ts @@ -0,0 +1,16 @@ +export { default as AppSetNavPage } from './AppSetNavPage'; +export { default as AppSetDetailsTab } from './AppSetDetailsTab'; +export { default as GeneratorsTab } from './GeneratorsTab'; +export { default as AppsTab } from './AppsTab'; +export { default as EventsTab } from './EventsTab'; +export { default as YAMLTab } from './YAMLTab'; +export { default as Generators } from './Generators'; + +// Export generator components +export { default as GitGenerator } from './generators/GitGenerator'; +export { default as ListGenerator } from './generators/ListGenerator'; +export { default as ClusterGenerator } from './generators/ClusterGenerator'; +export { default as MatrixGenerator } from './generators/MatrixGenerator'; +export { default as UnionGenerator } from './generators/UnionGenerator'; +export { default as MergeGenerator } from './generators/MergeGenerator'; +export { default as GenericGenerator } from './generators/GenericGenerator'; diff --git a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx index 334f3efd..e47f8d14 100644 --- a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx +++ b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx @@ -14,6 +14,7 @@ import { PencilAltIcon } from '@patternfly/react-icons'; import { ResourceLink, Timestamp, useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; import { useLabelsModal, useAnnotationsModal } from '@openshift-console/dynamic-plugin-sdk'; import * as _ from 'lodash'; +import { getAppSetGeneratorCount } from '../../../utils/gitops'; interface ResourceDetailsAttributesProps { metadata: { @@ -60,6 +61,9 @@ const ResourceDetailsAttributes: React.FC = ({ namespace: metadata.namespace, }); + // Calculate the total number of generators (including sub-generators in matrix/union/merge) + const totalGenerators = showGenerators ? getAppSetGeneratorCount(resource) : 0; + const labelItems = metadata.labels || {}; const annotationItems = metadata.annotations || {}; const countAnnotations = Object.keys(annotationItems).length; @@ -407,7 +411,7 @@ const ResourceDetailsAttributes: React.FC = ({ - 1 generators + {totalGenerators} generator{totalGenerators !== 1 ? 's' : ''} )} From 382973520b1709ab368704ab94b4bc10b94a5220 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 29 Aug 2025 04:41:45 -0400 Subject: [PATCH 08/16] show YAML page Signed-off-by: Atif Ali show YAML page better Signed-off-by: Atif Ali complete YAML page and cleanup Signed-off-by: Atif Ali implement edit appset Signed-off-by: Atif Ali --- .../components/appset/AppSetDetailsTab.tsx | 3 +- .../components/appset/AppSetNavPage.scss | 31 ++++++- .../components/appset/AppSetNavPage.tsx | 20 ++++- src/gitops/components/appset/AppsTab.tsx | 3 +- src/gitops/components/appset/EventsTab.tsx | 3 +- .../components/appset/GeneratorsTab.tsx | 3 +- src/gitops/components/appset/YAMLTab.scss | 84 +++++++++++-------- src/gitops/components/appset/YAMLTab.tsx | 8 +- 8 files changed, 108 insertions(+), 47 deletions(-) diff --git a/src/gitops/components/appset/AppSetDetailsTab.tsx b/src/gitops/components/appset/AppSetDetailsTab.tsx index 306fdcfa..773487c2 100644 --- a/src/gitops/components/appset/AppSetDetailsTab.tsx +++ b/src/gitops/components/appset/AppSetDetailsTab.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; import { ApplicationSetKind } from '../../models/ApplicationSetModel'; import { @@ -12,7 +13,7 @@ import { import ResourceDetailsAttributes from '../../utils/components/ResourceDetails/ResourceDetailsAttributes'; import './AppSetDetailsTab.scss'; -type AppSetDetailsTabProps = { +type AppSetDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: ApplicationSetKind; namespace?: string; name?: string; diff --git a/src/gitops/components/appset/AppSetNavPage.scss b/src/gitops/components/appset/AppSetNavPage.scss index f0c80b84..a7e7d83c 100644 --- a/src/gitops/components/appset/AppSetNavPage.scss +++ b/src/gitops/components/appset/AppSetNavPage.scss @@ -2,7 +2,7 @@ &__main-section { display: flex; flex-direction: column; - height: 100%; + height: 100vh; // Use full viewport height } &__body { @@ -14,6 +14,33 @@ &__pane-body { flex: 1; padding: 20px; - overflow-y: auto; + overflow-y: auto; // Restore scrolling for other tabs + + /* Remove min-height constraints that force outer scrolling */ + .pf-v6-c-code-editor, + .pf-c-code-editor { + height: 100% !important; + border: 0 !important; + box-shadow: none !important; + outline: none !important; + } + .pf-v6-c-code-editor__main, + .pf-c-code-editor__main { + height: 100% !important; + border: 0 !important; + box-shadow: none !important; + outline: none !important; + } + .pf-v6-c-code-editor__main::before, + .pf-v6-c-code-editor__main::after, + .pf-v6-c-code-editor__main::before, + .pf-c-code-editor__main::after { + content: none !important; + display: none !important; + } + .monaco-editor, + .monaco-editor .overflow-guard { + height: 100% !important; + } } } diff --git a/src/gitops/components/appset/AppSetNavPage.tsx b/src/gitops/components/appset/AppSetNavPage.tsx index 42eef4e5..ed3b840f 100644 --- a/src/gitops/components/appset/AppSetNavPage.tsx +++ b/src/gitops/components/appset/AppSetNavPage.tsx @@ -16,6 +16,7 @@ import AppsTab from './AppsTab'; import EventsTab from './EventsTab'; import YAMLTab from './YAMLTab'; import './AppSetNavPage.scss'; +import { useLocation } from 'react-router-dom-v5-compat'; type AppSetPageProps = { name: string; @@ -24,8 +25,9 @@ type AppSetPageProps = { }; const AppSetNavPage: React.FC = ({ name, namespace, kind }) => { + const location = useLocation(); const [activeTabKey, setActiveTabKey] = React.useState(0); - + const [appSet, loaded, loadError] = useK8sWatchResource({ groupVersionKind: { group: 'argoproj.io', @@ -38,6 +40,15 @@ const AppSetNavPage: React.FC = ({ name, namespace, kind }) => const [actions] = useApplicationSetActionsProvider(appSet); + // Handle tab query parameter + React.useEffect(() => { + const searchParams = new URLSearchParams(location.search); + const tabParam = searchParams.get('tab'); + if (tabParam === 'yaml') { + setActiveTabKey(1); // YAML tab is at index 1 + } + }, [location.search]); + if (loadError) return
Error loading ApplicationSet details.
; if (!loaded || !appSet) return ( @@ -45,7 +56,10 @@ const AppSetNavPage: React.FC = ({ name, namespace, kind }) => ); - const handleTabClick = (event: React.MouseEvent, tabIndex: string | number) => { + const handleTabClick = ( + event: React.MouseEvent, + tabIndex: string | number, + ) => { setActiveTabKey(tabIndex); }; @@ -60,8 +74,8 @@ const AppSetNavPage: React.FC = ({ name, namespace, kind }) => iconText="AS" iconTitle="Argo CD ApplicationSet" resourcePrefix="Argo CD" + showDevPreviewBadge={true} /> -
diff --git a/src/gitops/components/appset/AppsTab.tsx b/src/gitops/components/appset/AppsTab.tsx index f0ff1245..15cacc20 100644 --- a/src/gitops/components/appset/AppsTab.tsx +++ b/src/gitops/components/appset/AppsTab.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; import { ApplicationSetKind } from '../../models/ApplicationSetModel'; import { PageSection } from '@patternfly/react-core'; import ApplicationList from '../shared/ApplicationList'; import './AppsTab.scss'; -type AppsTabProps = { +type AppsTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: ApplicationSetKind; namespace?: string; name?: string; diff --git a/src/gitops/components/appset/EventsTab.tsx b/src/gitops/components/appset/EventsTab.tsx index c5735633..59a72ecc 100644 --- a/src/gitops/components/appset/EventsTab.tsx +++ b/src/gitops/components/appset/EventsTab.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; import { ApplicationSetKind } from '../../models/ApplicationSetModel'; import { Badge, @@ -7,7 +8,7 @@ import { } from '@patternfly/react-core'; import './EventsTab.scss'; -type EventsTabProps = { +type EventsTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: ApplicationSetKind; namespace?: string; name?: string; diff --git a/src/gitops/components/appset/GeneratorsTab.tsx b/src/gitops/components/appset/GeneratorsTab.tsx index 43d611b0..9030b4ff 100644 --- a/src/gitops/components/appset/GeneratorsTab.tsx +++ b/src/gitops/components/appset/GeneratorsTab.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; import { ApplicationSetKind } from '../../models/ApplicationSetModel'; import { PageSection } from '@patternfly/react-core'; import Generators from './Generators'; import './GeneratorsTab.scss'; -type GeneratorsTabProps = { +type GeneratorsTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: ApplicationSetKind; namespace?: string; name?: string; diff --git a/src/gitops/components/appset/YAMLTab.scss b/src/gitops/components/appset/YAMLTab.scss index ff8e15d3..5b90037b 100644 --- a/src/gitops/components/appset/YAMLTab.scss +++ b/src/gitops/components/appset/YAMLTab.scss @@ -1,51 +1,61 @@ -.application-set-details-page { - &__yaml-editor { - background: #1a1d21; - border: 1px solid #393F44; - border-radius: 8px; - overflow: hidden; +.yaml-tab-container { + // Use viewport height to fit the page without scrolling - adjust for header, tabs, and footer + height: calc(97vh - 320px); // Slightly reduced to make footer fully visible + display: flex; + flex-direction: column; + overflow: hidden; // Prevent page scrolling for YAML tab only + + // Override parent scrolling behavior for YAML tab + .application-set-details-page__pane-body { + overflow: hidden !important; // Override parent scrolling for YAML tab + } + + .pf-v6-c-code-editor:focus-within, + .pf-c-code-editor:focus-within { + box-shadow: none !important; + outline: none !important; } - &__yaml-editor-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - background: #212427; - border-bottom: 1px solid #393F44; + .monaco-editor .scrollbar .slider { + border: 0 !important; } - &__yaml-editor-header-buttons { - display: flex; - gap: 8px; + .monaco-editor .scrollbar .slider:hover { + border: 0 !important; } - &__yaml-editor-header-shortcuts { - font-size: 12px; + .monaco-editor .scrollbar.vertical { + border-left: 0 !important; } - &__yaml-editor-header-shortcuts-link { - color: #0066cc; - text-decoration: none; + .monaco-editor .scrollbar.horizontal { + border-top: 0 !important; + } + + .monaco-editor .margin-view-overlays { + border: 0 !important; + } - &:hover { - text-decoration: underline; - } + .monaco-editor .glyph-margin { + border: 0 !important; } - &__yaml-editor-content { - padding: 16px; - max-height: 600px; - overflow-y: auto; + .monaco-editor .monaco-editor-background:focus { + outline: none !important; + border: 0 !important; + } + + .monaco-editor .scrollbar .corner:hover { + background: transparent !important; + border: 0 !important; + } + + .monaco-editor .scrollbar .slider.active:hover { + border: 0 !important; + } - pre { - margin: 0; - color: #ffffff; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 12px; - line-height: 1.5; - white-space: pre-wrap; - word-break: break-word; - } + .monaco-editor .scrollbar .slider::before, + .monaco-editor .scrollbar .slider::after { + display: none !important; } } diff --git a/src/gitops/components/appset/YAMLTab.tsx b/src/gitops/components/appset/YAMLTab.tsx index 42f2ace5..422e36d3 100644 --- a/src/gitops/components/appset/YAMLTab.tsx +++ b/src/gitops/components/appset/YAMLTab.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import { ApplicationSetKind } from '../../models/ApplicationSetModel'; import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import './YAMLTab.scss'; type YAMLTabProps = { obj?: ApplicationSetKind; @@ -12,7 +14,11 @@ const YAMLTab: React.FC = ({ obj }) => { if (!obj) return null; return ( - +
+ }> + + +
); }; From 5c1b2dd07430eb16b97d3370d15df3f9d60d45f1 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 29 Aug 2025 06:17:46 -0400 Subject: [PATCH 09/16] add fav icon and fix formatting Signed-off-by: Atif Ali more Details formating Signed-off-by: Atif Ali --- src/gitops/components/appset/AppsTab.scss | 2 + src/gitops/components/appset/EventsTab.scss | 3 + .../components/appset/FavoriteButton.tsx | 162 ++++++++++++++++++ src/gitops/components/appset/Generators.tsx | 2 + .../components/appset/GeneratorsTab.scss | 2 + src/gitops/components/appset/README.md | 3 + .../appset/generators/Generators.scss | 3 + .../components/shared/ApplicationSetList.tsx | 6 +- .../DetailsPageTitle/ResourceDetailsTitle.tsx | 4 +- 9 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 src/gitops/components/appset/FavoriteButton.tsx diff --git a/src/gitops/components/appset/AppsTab.scss b/src/gitops/components/appset/AppsTab.scss index f0607610..95bbf403 100644 --- a/src/gitops/components/appset/AppsTab.scss +++ b/src/gitops/components/appset/AppsTab.scss @@ -4,3 +4,5 @@ flex-direction: column; } } + + diff --git a/src/gitops/components/appset/EventsTab.scss b/src/gitops/components/appset/EventsTab.scss index 9d851721..142aef31 100644 --- a/src/gitops/components/appset/EventsTab.scss +++ b/src/gitops/components/appset/EventsTab.scss @@ -5,3 +5,6 @@ gap: 12px; } } + + + diff --git a/src/gitops/components/appset/FavoriteButton.tsx b/src/gitops/components/appset/FavoriteButton.tsx new file mode 100644 index 00000000..cdb6e5cf --- /dev/null +++ b/src/gitops/components/appset/FavoriteButton.tsx @@ -0,0 +1,162 @@ +import * as React from 'react'; +import { Button, Tooltip, Form, FormGroup, TextInput, FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core'; +import { Modal, ModalVariant } from '@patternfly/react-core/deprecated'; +import { StarIcon } from '@patternfly/react-icons'; + +type FavoriteButtonProps = { + defaultName?: string; +}; + +const FavoriteButton: React.FC = ({ defaultName }) => { + const [isStarred, setIsStarred] = React.useState(false); + const [isModalOpen, setIsModalOpen] = React.useState(false); + const [name, setName] = React.useState(''); + const [error, setError] = React.useState(null); + + // Check if this page is already favorited on mount + React.useEffect(() => { + const currentUrlPath = window.location.pathname; + const favorites = JSON.parse(localStorage.getItem('console-favorites') || '[]'); + const isCurrentlyFavorited = favorites.some((favorite: any) => favorite.url === currentUrlPath); + setIsStarred(isCurrentlyFavorited); + }, []); + + const handleStarClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const currentUrlPath = window.location.pathname; + const favorites = JSON.parse(localStorage.getItem('console-favorites') || '[]'); + const isCurrentlyFavorited = favorites.some((favorite: any) => favorite.url === currentUrlPath); + + if (isCurrentlyFavorited) { + // Remove from favorites + const updatedFavorites = favorites.filter((favorite: any) => favorite.url !== currentUrlPath); + localStorage.setItem('console-favorites', JSON.stringify(updatedFavorites)); + setIsStarred(false); + } else { + // Open modal to add to favorites + const currentUrlSplit = currentUrlPath.includes('~') + ? currentUrlPath.split('~') + : currentUrlPath.split('/'); + const sanitizedDefaultName = ( + defaultName ?? currentUrlSplit.slice(-1)[0].split('?')[0] + ).replace(/[^\p{L}\p{N}\s-]/gu, '-'); + setName(sanitizedDefaultName); + setIsModalOpen(true); + } + }; + + const handleModalClose = () => { + setError(''); + setName(''); + setIsModalOpen(false); + }; + + const handleNameChange = (value: string) => { + setName(value); + setError(''); + }; + + const handleConfirmStar = () => { + const trimmedName = name.trim(); + + if (!trimmedName) { + setError('Name is required'); + return; + } + + if (trimmedName.length > 50) { + setError('Name must be 50 characters or less'); + return; + } + + // Check for duplicate names + const favorites = JSON.parse(localStorage.getItem('console-favorites') || '[]'); + const isDuplicate = favorites.some((favorite: any) => favorite.name === trimmedName); + + if (isDuplicate) { + setError('A favorite with this name already exists'); + return; + } + + // Add to favorites + const currentUrlPath = window.location.pathname; + const newFavorite = { + url: currentUrlPath, + name: trimmedName, + }; + const updatedFavorites = [...favorites, newFavorite]; + localStorage.setItem('console-favorites', JSON.stringify(updatedFavorites)); + setIsStarred(true); + handleModalClose(); + }; + + const tooltipText = isStarred ? 'Remove from favorites' : 'Add to favorites'; + + return ( + <> +
+ +
+ + {isModalOpen && ( + + Save + , + , + ]} + > +
+ + handleNameChange(v)} + value={name || ''} + autoFocus + required + /> + {error && ( + + + + {error} + + + + )} + +
+
+ )} + + ); +}; + +export default FavoriteButton; diff --git a/src/gitops/components/appset/Generators.tsx b/src/gitops/components/appset/Generators.tsx index 0dd7bce7..cdd441c3 100644 --- a/src/gitops/components/appset/Generators.tsx +++ b/src/gitops/components/appset/Generators.tsx @@ -47,3 +47,5 @@ const Generators: React.FC = ({ generators }) => { }; export default Generators; + + diff --git a/src/gitops/components/appset/GeneratorsTab.scss b/src/gitops/components/appset/GeneratorsTab.scss index c7fe0f7a..7ba21b3f 100644 --- a/src/gitops/components/appset/GeneratorsTab.scss +++ b/src/gitops/components/appset/GeneratorsTab.scss @@ -5,3 +5,5 @@ gap: 16px; } } + + diff --git a/src/gitops/components/appset/README.md b/src/gitops/components/appset/README.md index 1634ab0a..b2f027aa 100644 --- a/src/gitops/components/appset/README.md +++ b/src/gitops/components/appset/README.md @@ -87,3 +87,6 @@ Each component has its own SCSS file for styling: 3. **Maintainability**: Easier to maintain and debug individual components 4. **Consistency**: Follows the same pattern as gitops-admin-plugin 5. **Scalability**: Easy to add new generator types or modify existing ones + + + diff --git a/src/gitops/components/appset/generators/Generators.scss b/src/gitops/components/appset/generators/Generators.scss index eecf52fe..48fb3927 100644 --- a/src/gitops/components/appset/generators/Generators.scss +++ b/src/gitops/components/appset/generators/Generators.scss @@ -53,3 +53,6 @@ word-break: break-all; } } + + + diff --git a/src/gitops/components/shared/ApplicationSetList.tsx b/src/gitops/components/shared/ApplicationSetList.tsx index c0862df6..56b19bf6 100644 --- a/src/gitops/components/shared/ApplicationSetList.tsx +++ b/src/gitops/components/shared/ApplicationSetList.tsx @@ -189,7 +189,11 @@ const ApplicationSetList: React.FC = ({ return (
{showTitle == undefined && ( - }> + } + hideFavoriteButton={false} + > Create ApplicationSet diff --git a/src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx b/src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx index 7bfcb8ff..2e76fd0d 100644 --- a/src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx +++ b/src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx @@ -8,6 +8,7 @@ import { Action, K8sModel, K8sResourceCommon } from '@openshift-console/dynamic- import { Breadcrumb, BreadcrumbItem, Spinner, Title } from '@patternfly/react-core'; import ActionsDropdown from '../../../utils/components/ActionDropDown/ActionDropDown'; import DetailsPageTitle, { PaneHeading } from './DetailsPageTitle'; +import FavoriteButton from '../../../components/appset/FavoriteButton'; type ResourceDetailsTitleProps = { obj: K8sResourceCommon; @@ -74,7 +75,8 @@ const ResourceDetailsTitle: React.FC = ({ )} -
+
+
From 730c40edbcdfe9000a1750b0b53a4ad88997891d Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 17 Sep 2025 15:34:42 -0400 Subject: [PATCH 10/16] use shared component from Keith Signed-off-by: Atif Ali format details tab info Signed-off-by: Atif Ali fix condition to use pf && generators styling Signed-off-by: Atif Ali remove rebase/conflict commit markers Signed-off-by: Atif Ali --- .../components/appset/AppSetDetailsTab.tsx | 225 +++++++++++++----- .../components/appset/AppSetNavPage.tsx | 6 +- .../components/appset/FavoriteButton.tsx | 162 ------------- src/gitops/components/appset/YAMLTab.scss | 61 ----- src/gitops/components/appset/YAMLTab.tsx | 16 +- .../appset/generators/ClusterGenerator.tsx | 18 +- .../appset/generators/GenericGenerator.tsx | 18 +- .../components/shared/ApplicationList.tsx | 19 +- .../ResourceYAMLTab/ResourceYAMLTab.tsx | 2 +- .../components/Conditions/Conditions.tsx | 81 +++++++ .../DetailsPageTitle/ResourceDetailsTitle.tsx | 89 ------- 11 files changed, 286 insertions(+), 411 deletions(-) delete mode 100644 src/gitops/components/appset/FavoriteButton.tsx delete mode 100644 src/gitops/components/appset/YAMLTab.scss create mode 100644 src/gitops/utils/components/Conditions/Conditions.tsx delete mode 100644 src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx diff --git a/src/gitops/components/appset/AppSetDetailsTab.tsx b/src/gitops/components/appset/AppSetDetailsTab.tsx index 773487c2..c4093ded 100644 --- a/src/gitops/components/appset/AppSetDetailsTab.tsx +++ b/src/gitops/components/appset/AppSetDetailsTab.tsx @@ -1,16 +1,30 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; -import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; -import { ApplicationSetKind } from '../../models/ApplicationSetModel'; +import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; import { Badge, PageSection, Title, DescriptionList, + DescriptionListGroup, + DescriptionListDescription, + DescriptionListTermHelpText, + DescriptionListTermHelpTextButton, + Popover, Grid, GridItem, } from '@patternfly/react-core'; -import ResourceDetailsAttributes from '../../utils/components/ResourceDetails/ResourceDetailsAttributes'; +import BaseDetailsSummary from '../shared/BaseDetailsSummary/BaseDetailsSummary'; +import { getAppSetGeneratorCount, getAppSetStatus } from '../../utils/gitops'; +import { ApplicationSetStatus } from '../../utils/constants'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { ApplicationKind, ApplicationModel } from '../../models/ApplicationModel'; +import { + HealthHealthyIcon, + HealthDegradedIcon, + HealthUnknownIcon, +} from '../../utils/components/Icons/Icons'; +import { Conditions } from '../../utils/components/Conditions/Conditions'; import './AppSetDetailsTab.scss'; type AppSetDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & { @@ -19,69 +33,174 @@ type AppSetDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & name?: string; }; -const AppSetDetailsTab: React.FC = ({ obj }) => { +const AppSetDetailsTab: React.FC = ({ obj, namespace }) => { if (!obj) return null; - const metadata = obj.metadata || {}; const status = obj.status || {}; + const spec = obj.spec || {}; + const totalGenerators = getAppSetGeneratorCount(obj); + const appSetStatus = getAppSetStatus(obj); + + // Get applications to count generated apps + const [applications] = useK8sWatchResource({ + groupVersionKind: { + group: ApplicationModel.apiGroup, + version: ApplicationModel.apiVersion, + kind: ApplicationModel.kind, + }, + namespace: namespace || obj.metadata?.namespace, + isList: true, + }); + + // Count applications owned by this ApplicationSet + const generatedAppsCount = applications?.filter(app => + app.metadata?.ownerReferences?.some(owner => + owner.kind === obj.kind && owner.name === obj.metadata?.name + ) + ).length || 0; return ( <> - ApplicationSet details + Argo CD ApplicationSet details - - - - + + + + + + + {/* Status */} + + + Status
} + bodyContent={ +
+
Current status of the ApplicationSet
+
+ ApplicationSet {'>'} status +
+
+ } + > + Status + + + + + {appSetStatus === ApplicationSetStatus.HEALTHY && } + {appSetStatus === ApplicationSetStatus.ERROR && } + {appSetStatus === ApplicationSetStatus.UNKNOWN && } + {' '}{appSetStatus === ApplicationSetStatus.HEALTHY ? 'Healthy' : + appSetStatus === ApplicationSetStatus.ERROR ? 'Error' : 'Unknown'} + + + + + {/* Generated Apps */} + + + Generated Apps
} + bodyContent={ +
+
Number of applications generated by this ApplicationSet
+
+ ApplicationSet {'>'} status {'>'} applications +
+
+ } + > + Generated Apps + + + + {generatedAppsCount} application{generatedAppsCount !== 1 ? 's' : ''} + + + + {/* Generators */} + + + Generators
} + bodyContent={ +
+
Number of generators configured in this ApplicationSet
+
+ ApplicationSet {'>'} spec {'>'} generators +
+
+ } + > + Generators + + + + {totalGenerators} generator{totalGenerators !== 1 ? 's' : ''} + + + + {/* App Project */} + + + App Project
} + bodyContent={ +
+
Argo CD project that this ApplicationSet belongs to
+
+ ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} project +
+
+ } + > + App Project + + + + AP {spec.template?.spec?.project || 'default'} + + + + {/* Repository */} + {spec.template?.spec?.source?.repoURL && ( + + + Repository
} + bodyContent={ +
+
Git repository URL where the ApplicationSet configuration is stored
+
+ ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} source {'>'} repoURL +
+
+ } + > + Repository + + + + + {spec.template.spec.source.repoURL} + + + + )} - - {status.conditions && status.conditions.length > 0 && ( - - - Conditions - -
-
-
Type
-
Status
-
Updated
-
Reason
-
Message
-
- {status.conditions.map((condition: any, index: number) => ( - -
-
{condition.type}
-
- - {condition.status} - -
-
- -
-
{condition.reason || ''}
-
{condition.message || ''}
-
-
- ))} -
-
- )} + + + + Conditions + + + ); }; diff --git a/src/gitops/components/appset/AppSetNavPage.tsx b/src/gitops/components/appset/AppSetNavPage.tsx index ed3b840f..1cf37360 100644 --- a/src/gitops/components/appset/AppSetNavPage.tsx +++ b/src/gitops/components/appset/AppSetNavPage.tsx @@ -8,8 +8,8 @@ import { Tab, TabTitleText, } from '@patternfly/react-core'; +import DetailsPageHeader from '../shared/DetailsPageHeader/DetailsPageHeader'; import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; -import ResourceDetailsTitle from '../../utils/components/DetailsPageTitle/ResourceDetailsTitle'; import AppSetDetailsTab from './AppSetDetailsTab'; import GeneratorsTab from './GeneratorsTab'; import AppsTab from './AppsTab'; @@ -65,7 +65,7 @@ const AppSetNavPage: React.FC = ({ name, namespace, kind }) => return (
- = ({ name, namespace, kind }) => actions={actions} iconText="AS" iconTitle="Argo CD ApplicationSet" - resourcePrefix="Argo CD" - showDevPreviewBadge={true} />
diff --git a/src/gitops/components/appset/FavoriteButton.tsx b/src/gitops/components/appset/FavoriteButton.tsx deleted file mode 100644 index cdb6e5cf..00000000 --- a/src/gitops/components/appset/FavoriteButton.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import * as React from 'react'; -import { Button, Tooltip, Form, FormGroup, TextInput, FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core'; -import { Modal, ModalVariant } from '@patternfly/react-core/deprecated'; -import { StarIcon } from '@patternfly/react-icons'; - -type FavoriteButtonProps = { - defaultName?: string; -}; - -const FavoriteButton: React.FC = ({ defaultName }) => { - const [isStarred, setIsStarred] = React.useState(false); - const [isModalOpen, setIsModalOpen] = React.useState(false); - const [name, setName] = React.useState(''); - const [error, setError] = React.useState(null); - - // Check if this page is already favorited on mount - React.useEffect(() => { - const currentUrlPath = window.location.pathname; - const favorites = JSON.parse(localStorage.getItem('console-favorites') || '[]'); - const isCurrentlyFavorited = favorites.some((favorite: any) => favorite.url === currentUrlPath); - setIsStarred(isCurrentlyFavorited); - }, []); - - const handleStarClick = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - const currentUrlPath = window.location.pathname; - const favorites = JSON.parse(localStorage.getItem('console-favorites') || '[]'); - const isCurrentlyFavorited = favorites.some((favorite: any) => favorite.url === currentUrlPath); - - if (isCurrentlyFavorited) { - // Remove from favorites - const updatedFavorites = favorites.filter((favorite: any) => favorite.url !== currentUrlPath); - localStorage.setItem('console-favorites', JSON.stringify(updatedFavorites)); - setIsStarred(false); - } else { - // Open modal to add to favorites - const currentUrlSplit = currentUrlPath.includes('~') - ? currentUrlPath.split('~') - : currentUrlPath.split('/'); - const sanitizedDefaultName = ( - defaultName ?? currentUrlSplit.slice(-1)[0].split('?')[0] - ).replace(/[^\p{L}\p{N}\s-]/gu, '-'); - setName(sanitizedDefaultName); - setIsModalOpen(true); - } - }; - - const handleModalClose = () => { - setError(''); - setName(''); - setIsModalOpen(false); - }; - - const handleNameChange = (value: string) => { - setName(value); - setError(''); - }; - - const handleConfirmStar = () => { - const trimmedName = name.trim(); - - if (!trimmedName) { - setError('Name is required'); - return; - } - - if (trimmedName.length > 50) { - setError('Name must be 50 characters or less'); - return; - } - - // Check for duplicate names - const favorites = JSON.parse(localStorage.getItem('console-favorites') || '[]'); - const isDuplicate = favorites.some((favorite: any) => favorite.name === trimmedName); - - if (isDuplicate) { - setError('A favorite with this name already exists'); - return; - } - - // Add to favorites - const currentUrlPath = window.location.pathname; - const newFavorite = { - url: currentUrlPath, - name: trimmedName, - }; - const updatedFavorites = [...favorites, newFavorite]; - localStorage.setItem('console-favorites', JSON.stringify(updatedFavorites)); - setIsStarred(true); - handleModalClose(); - }; - - const tooltipText = isStarred ? 'Remove from favorites' : 'Add to favorites'; - - return ( - <> -
- -
- - {isModalOpen && ( - - Save - , - , - ]} - > -
- - handleNameChange(v)} - value={name || ''} - autoFocus - required - /> - {error && ( - - - - {error} - - - - )} - -
-
- )} - - ); -}; - -export default FavoriteButton; diff --git a/src/gitops/components/appset/YAMLTab.scss b/src/gitops/components/appset/YAMLTab.scss deleted file mode 100644 index 5b90037b..00000000 --- a/src/gitops/components/appset/YAMLTab.scss +++ /dev/null @@ -1,61 +0,0 @@ -.yaml-tab-container { - // Use viewport height to fit the page without scrolling - adjust for header, tabs, and footer - height: calc(97vh - 320px); // Slightly reduced to make footer fully visible - display: flex; - flex-direction: column; - overflow: hidden; // Prevent page scrolling for YAML tab only - - // Override parent scrolling behavior for YAML tab - .application-set-details-page__pane-body { - overflow: hidden !important; // Override parent scrolling for YAML tab - } - - .pf-v6-c-code-editor:focus-within, - .pf-c-code-editor:focus-within { - box-shadow: none !important; - outline: none !important; - } - - .monaco-editor .scrollbar .slider { - border: 0 !important; - } - - .monaco-editor .scrollbar .slider:hover { - border: 0 !important; - } - - .monaco-editor .scrollbar.vertical { - border-left: 0 !important; - } - - .monaco-editor .scrollbar.horizontal { - border-top: 0 !important; - } - - .monaco-editor .margin-view-overlays { - border: 0 !important; - } - - .monaco-editor .glyph-margin { - border: 0 !important; - } - - .monaco-editor .monaco-editor-background:focus { - outline: none !important; - border: 0 !important; - } - - .monaco-editor .scrollbar .corner:hover { - background: transparent !important; - border: 0 !important; - } - - .monaco-editor .scrollbar .slider.active:hover { - border: 0 !important; - } - - .monaco-editor .scrollbar .slider::before, - .monaco-editor .scrollbar .slider::after { - display: none !important; - } -} diff --git a/src/gitops/components/appset/YAMLTab.tsx b/src/gitops/components/appset/YAMLTab.tsx index 422e36d3..5c84fb22 100644 --- a/src/gitops/components/appset/YAMLTab.tsx +++ b/src/gitops/components/appset/YAMLTab.tsx @@ -1,8 +1,6 @@ import * as React from 'react'; import { ApplicationSetKind } from '../../models/ApplicationSetModel'; -import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; -import { Bullseye, Spinner } from '@patternfly/react-core'; -import './YAMLTab.scss'; +import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; type YAMLTabProps = { obj?: ApplicationSetKind; @@ -10,15 +8,15 @@ type YAMLTabProps = { name?: string; }; -const YAMLTab: React.FC = ({ obj }) => { +const YAMLTab: React.FC = ({ obj, namespace, name }) => { if (!obj) return null; return ( -
- }> - - -
+ ); }; diff --git a/src/gitops/components/appset/generators/ClusterGenerator.tsx b/src/gitops/components/appset/generators/ClusterGenerator.tsx index 892654d5..60b2e2d9 100644 --- a/src/gitops/components/appset/generators/ClusterGenerator.tsx +++ b/src/gitops/components/appset/generators/ClusterGenerator.tsx @@ -15,16 +15,18 @@ const ClusterGenerator: React.FC = ({ generator }) => { Selector -
                 {JSON.stringify(generator.selector, null, 2)}
-              
+
)} diff --git a/src/gitops/components/appset/generators/GenericGenerator.tsx b/src/gitops/components/appset/generators/GenericGenerator.tsx index a2c255f7..d62a0a26 100644 --- a/src/gitops/components/appset/generators/GenericGenerator.tsx +++ b/src/gitops/components/appset/generators/GenericGenerator.tsx @@ -15,16 +15,18 @@ const GenericGenerator: React.FC = ({ gentype, generator Configuration -
               {JSON.stringify(generator, null, 2)}
-            
+
diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index b57ed5c8..c99f2a23 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -145,19 +145,6 @@ const ApplicationList: React.FC = ({ ); - const error = loadError && ( - - - - - - There was an error retrieving applications. Check your connection and reload the page. - - - - - - ); let currentActiveState = null; if (loadError) { currentActiveState = DataViewState.error; @@ -180,7 +167,7 @@ const ApplicationList: React.FC = ({ {!hideNameLabelFilters && ( = ({ )} diff --git a/src/gitops/components/shared/ResourceYAMLTab/ResourceYAMLTab.tsx b/src/gitops/components/shared/ResourceYAMLTab/ResourceYAMLTab.tsx index 4e5eab81..70e5b4fa 100644 --- a/src/gitops/components/shared/ResourceYAMLTab/ResourceYAMLTab.tsx +++ b/src/gitops/components/shared/ResourceYAMLTab/ResourceYAMLTab.tsx @@ -23,7 +23,7 @@ const ResourceYAMLTab: React.FC = ({ obj }) => { } > - +
); diff --git a/src/gitops/utils/components/Conditions/Conditions.tsx b/src/gitops/utils/components/Conditions/Conditions.tsx new file mode 100644 index 00000000..730868ae --- /dev/null +++ b/src/gitops/utils/components/Conditions/Conditions.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; +import { CamelCaseWrap, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; + +export const Conditions: React.FC = ({ + conditions +}) => { + const { t } = useGitOpsTranslation(); + + const getStatusLabel = (status: string) => { + switch (status) { + case 'True': + return t('public~True'); + case 'False': + return t('public~False'); + default: + return status; + } + }; + + return ( + <> + {conditions?.length ? ( + + + + + + + + + + + + {conditions?.map?.( + (condition: any, i: number) => ( + + + + + + + + ), + )} + +
{t('public~Type')}{t('public~Status')}{t('public~Updated')}{t('public~Reason')}{t('public~Message')}
+ + + {getStatusLabel(condition.status)} + + + + + + {condition.message?.trim() || '-'} +
+ ) : ( +
+
{t('public~No conditions found')}
+
+ )} + + ); +}; +Conditions.displayName = 'Conditions'; + +export type ConditionsProps = { + conditions: any; + title?: string; + subTitle?: string; +}; + +export default Conditions; diff --git a/src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx b/src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx deleted file mode 100644 index 2e76fd0d..00000000 --- a/src/gitops/utils/components/DetailsPageTitle/ResourceDetailsTitle.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as React from 'react'; -import { Link } from 'react-router-dom-v5-compat'; -import DevPreviewBadge from '../../../../components/import/badges/DevPreviewBadge'; -import { DEFAULT_NAMESPACE } from '../../../utils/constants'; -import { isApplicationRefreshing } from '../../../utils/gitops'; -import { useGitOpsTranslation } from '../../../utils/hooks/useGitOpsTranslation'; -import { Action, K8sModel, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; -import { Breadcrumb, BreadcrumbItem, Spinner, Title } from '@patternfly/react-core'; -import ActionsDropdown from '../../../utils/components/ActionDropDown/ActionDropDown'; -import DetailsPageTitle, { PaneHeading } from './DetailsPageTitle'; -import FavoriteButton from '../../../components/appset/FavoriteButton'; - -type ResourceDetailsTitleProps = { - obj: K8sResourceCommon; - model: K8sModel; - name: string; - namespace: string; - actions: Action[]; - // Configurable properties for different resource types - iconText: string; - iconTitle: string; - resourcePrefix?: string; // e.g., "Argo CD" for Applications/ApplicationSets - showDevPreviewBadge?: boolean; - showRefreshSpinner?: boolean; -}; - -const ResourceDetailsTitle: React.FC = ({ - obj, - model, - name, - namespace, - actions, - iconText, - iconTitle, - resourcePrefix = '', - showDevPreviewBadge = true, - showRefreshSpinner = true, -}) => { - const { t } = useGitOpsTranslation(); - - return ( - <> -
- - - - {resourcePrefix} {t(model.labelPlural)} - - - {resourcePrefix} {t(model.labelPlural + ' Details')} - - } - > - - - <span - className="co-m-resource-icon co-m-resource-icon--lg" - title={iconTitle} - > - {iconText} - </span> - <span className="co-resource-item__resource-name"> - {name ?? obj?.metadata?.name}{' '} - {showRefreshSpinner && isApplicationRefreshing(obj) ? <Spinner size="md" /> : <span></span>} - </span> - {showDevPreviewBadge && ( - <span style={{ marginLeft: '10px', marginBottom: '3px' }}> - <DevPreviewBadge /> - </span> - )} - -
- - -
-
-
-
- - ); -}; - -export default ResourceDetailsTitle; From 2ac7dac5443b5c8ba842600f54adac6d982a5c34 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 2 Oct 2025 10:54:28 -0400 Subject: [PATCH 11/16] apply sorting when label is clicked and formatting Signed-off-by: Atif Ali --- console-extensions.json | 26 +++++++++ .../components/shared/ApplicationList.tsx | 21 ++++++- .../components/shared/ApplicationSetList.tsx | 55 ++++++++++++++----- .../BaseDetailsSummary/BaseDetailsSummary.tsx | 5 +- 4 files changed, 90 insertions(+), 17 deletions(-) diff --git a/console-extensions.json b/console-extensions.json index d97fddf1..c0fc0d8b 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -424,5 +424,31 @@ "$codeRef": "ApplicationSetDetailsPage" } } + }, + { + "type": "console.page/resource/search", + "properties": { + "model": { + "group": "argoproj.io", + "version": "v1alpha1", + "kind": "ApplicationSet" + }, + "component": { + "$codeRef": "ApplicationSetList" + } + } + }, + { + "type": "console.page/resource/search", + "properties": { + "model": { + "group": "argoproj.io", + "version": "v1alpha1", + "kind": "Application" + }, + "component": { + "$codeRef": "ApplicationList" + } + } } ] diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index c99f2a23..102d4489 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -108,6 +108,10 @@ const ApplicationList: React.FC = ({ () => COLUMNS_KEYS_INDEXES.findIndex((item) => item.key === sortBy), [COLUMNS_KEYS_INDEXES, sortBy], ); + + // Get search query from URL parameters + const searchQuery = searchParams.get('q') || ''; + const getSortParams = (columnIndex: number): ThProps['sort'] => ({ sortBy: { index: sortByIndex, @@ -124,8 +128,23 @@ const ApplicationList: React.FC = ({ const sortedApplications = React.useMemo(() => { return sortData(applications, sortBy, direction); }, [applications, sortBy, direction]); + + // Filter by search query if present + const filteredBySearch = React.useMemo(() => { + if (!searchQuery) return sortedApplications; + + return sortedApplications.filter((app) => { + const labels = app.metadata?.labels || {}; + // Check if any label matches the search query + return Object.entries(labels).some(([key, value]) => { + const labelSelector = `${key}=${value}`; + return labelSelector.includes(searchQuery) || key.includes(searchQuery); + }); + }); + }, [sortedApplications, searchQuery]); + // TODO: use alternate filter since it is deprecated. See DataTableView potentially - const [, filteredData, onFilterChange] = useListPageFilter(sortedApplications, filters); + const [, filteredData, onFilterChange] = useListPageFilter(filteredBySearch, filters); // Filter applications by project or appset before rendering rows const filteredByOwner = React.useMemo( () => filteredData.filter(filterApp(project, appset)), diff --git a/src/gitops/components/shared/ApplicationSetList.tsx b/src/gitops/components/shared/ApplicationSetList.tsx index 56b19bf6..a01c9a23 100644 --- a/src/gitops/components/shared/ApplicationSetList.tsx +++ b/src/gitops/components/shared/ApplicationSetList.tsx @@ -133,6 +133,10 @@ const ApplicationSetList: React.FC = ({ const [searchParams, setSearchParams] = useSearchParams(); const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams }); + + // Get search query from URL parameters + const searchQuery = searchParams.get('q') || ''; + const getSortParams = (columnId: string, columnIndex: number) => ({ sortBy: { index: columnIndex, @@ -149,7 +153,22 @@ const ApplicationSetList: React.FC = ({ const sortedApplicationSets = React.useMemo(() => { return sortData(applicationSets as ApplicationSetKind[], sortBy, direction, applications, appsLoaded); }, [applicationSets, sortBy, direction, applications, appsLoaded]); - const [data, filteredData, onFilterChange] = useListPageFilter(sortedApplicationSets, filters); + + // Filter by search query if present + const filteredBySearch = React.useMemo(() => { + if (!searchQuery) return sortedApplicationSets; + + return sortedApplicationSets.filter((appSet) => { + const labels = appSet.metadata?.labels || {}; + // Check if any label matches the search query + return Object.entries(labels).some(([key, value]) => { + const labelSelector = `${key}=${value}`; + return labelSelector.includes(searchQuery) || key.includes(searchQuery); + }); + }); + }, [sortedApplicationSets, searchQuery]); + + const [data, filteredData, onFilterChange] = useListPageFilter(filteredBySearch, filters); const rows = useApplicationSetRowsDV(filteredData, namespace, applications, appsLoaded); const empty = ( @@ -262,24 +281,32 @@ const useApplicationSetRowsDV = (applicationSetsList, namespace, applications, a : []), { id: getAppSetStatus(appSet), + cell: , + }, + { + id: 'generated-apps-' + index, cell: (
- + {getGeneratedAppsCount(appSet, applications, appsLoaded).toString()}
), }, { - id: 'generated-apps-' + index, - cell: getGeneratedAppsCount(appSet, applications, appsLoaded).toString(), + id: 'generators-' + index, + cell: ( +
+ {getAppSetGeneratorCount(appSet).toString()} +
+ ), }, { - id: 'generators-' + index, - cell: getAppSetGeneratorCount(appSet).toString(), + id: 'created-at-' + index, + cell: ( +
+ {formatCreationTimestamp(appSet.metadata.creationTimestamp)} +
+ ), }, - { - id: 'created-at-' + index, - cell: formatCreationTimestamp(appSet.metadata.creationTimestamp), - }, { id: 'actions-' + index, cell: , @@ -312,7 +339,7 @@ const useColumnsDV = (namespace, getSortParams) => { props: { key: 'namespace', 'aria-label': 'namespace', - className: 'pf-m-width-12', + className: 'pf-m-width-15', sort: getSortParams('namespace', 1), }, }, @@ -324,7 +351,7 @@ const useColumnsDV = (namespace, getSortParams) => { props: { key: 'status', 'aria-label': 'health status', - className: 'pf-m-width-12', + className: 'pf-m-width-15', sort: getSortParams('status', 1 + i), }, }, @@ -334,7 +361,7 @@ const useColumnsDV = (namespace, getSortParams) => { props: { key: 'generated-apps', 'aria-label': 'generated apps', - className: 'pf-m-width-12', + className: 'pf-m-width-15', sort: getSortParams('generated-apps', 2 + i), }, }, @@ -344,7 +371,7 @@ const useColumnsDV = (namespace, getSortParams) => { props: { key: 'generators', 'aria-label': 'generators', - className: 'pf-m-width-12', + className: 'pf-m-width-15', sort: getSortParams('generators', 3 + i), }, }, diff --git a/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx b/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx index 544eb95e..1e357ce9 100644 --- a/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx +++ b/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { OwnerReferences } from '@gitops/utils/components/OwnerReferences/owner-references'; import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; -import { kindForReference, useObjectModifyPermissions } from '@gitops/utils/utils'; +import { kindForReference, useObjectModifyPermissions, getSelectorSearchURL } from '@gitops/utils/utils'; import { K8sModel, K8sResourceKind, @@ -58,7 +58,8 @@ type LabelProps = { }; const LabelL: React.SFC = ({ kind, name, value, expand }) => { - const href = `/search?kind=${kind}&q=${value ? encodeURIComponent(`${name}=${value}`) : name}`; + const selector = value ? `${name}=${value}` : name; + const href = getSelectorSearchURL('', kind, selector); const kindOf = `co-m-${kindForReference(kind.toLowerCase())}`; const klass = classNames(kindOf, { 'co-m-expand': expand }, 'co-label'); return ( From b88a4d3b1a31e3a0c56b463ef636d7cac1c7d602 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 3 Oct 2025 23:10:45 -0400 Subject: [PATCH 12/16] applied Keith's latest changes Signed-off-by: Atif Ali --- .../components/appset/AppSetDetailsTab.scss | 13 ++ .../components/appset/AppSetDetailsTab.tsx | 203 ++++++------------ .../components/appset/AppSetNavPage.scss | 46 ---- .../components/appset/AppSetNavPage.tsx | 108 +++++----- src/gitops/components/appset/AppsTab.tsx | 5 +- src/gitops/components/appset/EventsTab.tsx | 99 +-------- .../components/appset/GeneratorsTab.tsx | 2 - src/gitops/components/appset/README.md | 92 -------- src/gitops/components/appset/YAMLTab.tsx | 23 -- .../components/shared/ApplicationList.tsx | 41 ++-- .../components/shared/ApplicationSetList.tsx | 27 ++- 11 files changed, 173 insertions(+), 486 deletions(-) delete mode 100644 src/gitops/components/appset/AppSetNavPage.scss delete mode 100644 src/gitops/components/appset/README.md delete mode 100644 src/gitops/components/appset/YAMLTab.tsx diff --git a/src/gitops/components/appset/AppSetDetailsTab.scss b/src/gitops/components/appset/AppSetDetailsTab.scss index 2d0ae962..72581e3e 100644 --- a/src/gitops/components/appset/AppSetDetailsTab.scss +++ b/src/gitops/components/appset/AppSetDetailsTab.scss @@ -115,3 +115,16 @@ } } } + +// Force dashed border styling for labels in ApplicationSet details +.pf-c-description-list__description .co-label-group { + border: 1px dashed var(--pf-v6-global--BorderColor--200) !important; + border-radius: var(--pf-v6-global--BorderRadius--sm) !important; + padding: var(--pf-v6-global--spacer--sm) !important; + min-height: var(--pf-v6-global--spacer--lg) !important; + display: flex !important; + align-items: center !important; + width: fit-content !important; + max-width: 100% !important; +} + diff --git a/src/gitops/components/appset/AppSetDetailsTab.tsx b/src/gitops/components/appset/AppSetDetailsTab.tsx index c4093ded..4cb2b990 100644 --- a/src/gitops/components/appset/AppSetDetailsTab.tsx +++ b/src/gitops/components/appset/AppSetDetailsTab.tsx @@ -6,34 +6,23 @@ import { PageSection, Title, DescriptionList, - DescriptionListGroup, - DescriptionListDescription, - DescriptionListTermHelpText, - DescriptionListTermHelpTextButton, - Popover, - Grid, - GridItem, + Flex, + FlexItem, } from '@patternfly/react-core'; -import BaseDetailsSummary from '../shared/BaseDetailsSummary/BaseDetailsSummary'; +import BaseDetailsSummary, { DetailsDescriptionGroup } from '../shared/BaseDetailsSummary/BaseDetailsSummary'; import { getAppSetGeneratorCount, getAppSetStatus } from '../../utils/gitops'; -import { ApplicationSetStatus } from '../../utils/constants'; -import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { useK8sWatchResource, ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; import { ApplicationKind, ApplicationModel } from '../../models/ApplicationModel'; -import { - HealthHealthyIcon, - HealthDegradedIcon, - HealthUnknownIcon, -} from '../../utils/components/Icons/Icons'; import { Conditions } from '../../utils/components/Conditions/Conditions'; +import Status from '../shared/Status/Status'; import './AppSetDetailsTab.scss'; type AppSetDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: ApplicationSetKind; - namespace?: string; - name?: string; }; -const AppSetDetailsTab: React.FC = ({ obj, namespace }) => { +const AppSetDetailsTab: React.FC = ({ obj }) => { + const namespace = obj?.metadata?.namespace; if (!obj) return null; const status = obj.status || {}; @@ -65,134 +54,68 @@ const AppSetDetailsTab: React.FC = ({ obj, namespace }) = Argo CD ApplicationSet details - - - - - - - {/* Status */} - - - Status
} - bodyContent={ -
-
Current status of the ApplicationSet
-
- ApplicationSet {'>'} status -
-
- } - > - Status - - - - - {appSetStatus === ApplicationSetStatus.HEALTHY && } - {appSetStatus === ApplicationSetStatus.ERROR && } - {appSetStatus === ApplicationSetStatus.UNKNOWN && } - {' '}{appSetStatus === ApplicationSetStatus.HEALTHY ? 'Healthy' : - appSetStatus === ApplicationSetStatus.ERROR ? 'Error' : 'Unknown'} - - - + + + + + + + + + + + + - {/* Generated Apps */} - - - Generated Apps
} - bodyContent={ -
-
Number of applications generated by this ApplicationSet
-
- ApplicationSet {'>'} status {'>'} applications -
-
- } - > - Generated Apps - - - - {generatedAppsCount} application{generatedAppsCount !== 1 ? 's' : ''} - - + + {generatedAppsCount} application{generatedAppsCount !== 1 ? 's' : ''} + - {/* Generators */} - - - Generators
} - bodyContent={ -
-
Number of generators configured in this ApplicationSet
-
- ApplicationSet {'>'} spec {'>'} generators -
-
- } - > - Generators - - - - {totalGenerators} generator{totalGenerators !== 1 ? 's' : ''} - - + + {totalGenerators} generator{totalGenerators !== 1 ? 's' : ''} + - {/* App Project */} - - - App Project
} - bodyContent={ -
-
Argo CD project that this ApplicationSet belongs to
-
- ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} project -
-
- } - > - App Project - - - - AP {spec.template?.spec?.project || 'default'} - - + + + - {/* Repository */} {spec.template?.spec?.source?.repoURL && ( - - - Repository
} - bodyContent={ -
-
Git repository URL where the ApplicationSet configuration is stored
-
- ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} source {'>'} repoURL -
-
- } - > - Repository - - - - - {spec.template.spec.source.repoURL} - - - + + + {spec.template.spec.source.repoURL} + + )} - - - + + + + diff --git a/src/gitops/components/appset/AppSetNavPage.scss b/src/gitops/components/appset/AppSetNavPage.scss deleted file mode 100644 index a7e7d83c..00000000 --- a/src/gitops/components/appset/AppSetNavPage.scss +++ /dev/null @@ -1,46 +0,0 @@ -.application-set-details-page { - &__main-section { - display: flex; - flex-direction: column; - height: 100vh; // Use full viewport height - } - - &__body { - flex: 1; - display: flex; - flex-direction: column; - } - - &__pane-body { - flex: 1; - padding: 20px; - overflow-y: auto; // Restore scrolling for other tabs - - /* Remove min-height constraints that force outer scrolling */ - .pf-v6-c-code-editor, - .pf-c-code-editor { - height: 100% !important; - border: 0 !important; - box-shadow: none !important; - outline: none !important; - } - .pf-v6-c-code-editor__main, - .pf-c-code-editor__main { - height: 100% !important; - border: 0 !important; - box-shadow: none !important; - outline: none !important; - } - .pf-v6-c-code-editor__main::before, - .pf-v6-c-code-editor__main::after, - .pf-v6-c-code-editor__main::before, - .pf-c-code-editor__main::after { - content: none !important; - display: none !important; - } - .monaco-editor, - .monaco-editor .overflow-guard { - height: 100% !important; - } - } -} diff --git a/src/gitops/components/appset/AppSetNavPage.tsx b/src/gitops/components/appset/AppSetNavPage.tsx index 1cf37360..d308da3a 100644 --- a/src/gitops/components/appset/AppSetNavPage.tsx +++ b/src/gitops/components/appset/AppSetNavPage.tsx @@ -1,22 +1,15 @@ import * as React from 'react'; -import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { useK8sWatchResource, HorizontalNav } from '@openshift-console/dynamic-plugin-sdk'; import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; -import { - Spinner, - Bullseye, - Tabs, - Tab, - TabTitleText, -} from '@patternfly/react-core'; +import { Spinner, Bullseye } from '@patternfly/react-core'; import DetailsPageHeader from '../shared/DetailsPageHeader/DetailsPageHeader'; import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; import AppSetDetailsTab from './AppSetDetailsTab'; import GeneratorsTab from './GeneratorsTab'; import AppsTab from './AppsTab'; -import EventsTab from './EventsTab'; -import YAMLTab from './YAMLTab'; -import './AppSetNavPage.scss'; -import { useLocation } from 'react-router-dom-v5-compat'; +import ApplicationSetEventsTab from './EventsTab'; +import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; type AppSetPageProps = { name: string; @@ -25,77 +18,72 @@ type AppSetPageProps = { }; const AppSetNavPage: React.FC = ({ name, namespace, kind }) => { - const location = useLocation(); - const [activeTabKey, setActiveTabKey] = React.useState(0); - - const [appSet, loaded, loadError] = useK8sWatchResource({ + const { t } = useGitOpsTranslation(); + const [appSet, loaded] = useK8sWatchResource({ groupVersionKind: { group: 'argoproj.io', version: 'v1alpha1', kind: 'ApplicationSet', }, + kind, name, namespace, }); const [actions] = useApplicationSetActionsProvider(appSet); - // Handle tab query parameter - React.useEffect(() => { - const searchParams = new URLSearchParams(location.search); - const tabParam = searchParams.get('tab'); - if (tabParam === 'yaml') { - setActiveTabKey(1); // YAML tab is at index 1 - } - }, [location.search]); - - if (loadError) return
Error loading ApplicationSet details.
; - if (!loaded || !appSet) return ( - - - + const pages = React.useMemo( + () => [ + { + href: '', + name: t('Details'), + component: AppSetDetailsTab, + }, + { + href: 'yaml', + name: t('YAML'), + component: ResourceYAMLTab, + }, + { + href: 'generators', + name: t('Generators'), + component: GeneratorsTab, + }, + { + href: 'applications', + name: t('Applications'), + component: AppsTab, + }, + { + href: 'events', + name: t('Events'), + component: ApplicationSetEventsTab, + }, + ], + [t], ); - const handleTabClick = ( - event: React.MouseEvent, - tabIndex: string | number, - ) => { - setActiveTabKey(tabIndex); - }; - return ( -
+ <> -
-
- - Details} className="pf-v6-c-tab-content"> - - - YAML} className="pf-v6-c-tab-content"> - - - Generators} className="pf-v6-c-tab-content"> - - - Applications} className="pf-v6-c-tab-content"> - - - Events} className="pf-v6-c-tab-content"> - - - + {loaded ? ( +
+
-
-
+ ) : ( + + + + )} + ); }; diff --git a/src/gitops/components/appset/AppsTab.tsx b/src/gitops/components/appset/AppsTab.tsx index 15cacc20..2843c148 100644 --- a/src/gitops/components/appset/AppsTab.tsx +++ b/src/gitops/components/appset/AppsTab.tsx @@ -7,11 +7,10 @@ import './AppsTab.scss'; type AppsTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: ApplicationSetKind; - namespace?: string; - name?: string; }; -const AppsTab: React.FC = ({ obj, namespace }) => { +const AppsTab: React.FC = ({ obj }) => { + const namespace = obj?.metadata?.namespace; if (!obj || !namespace) return null; return ( diff --git a/src/gitops/components/appset/EventsTab.tsx b/src/gitops/components/appset/EventsTab.tsx index 59a72ecc..a1c66822 100644 --- a/src/gitops/components/appset/EventsTab.tsx +++ b/src/gitops/components/appset/EventsTab.tsx @@ -1,107 +1,16 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; import { ApplicationSetKind } from '../../models/ApplicationSetModel'; -import { - Badge, - PageSection, - Title, -} from '@patternfly/react-core'; -import './EventsTab.scss'; +import EventsTab from '../shared/EventsTab/EventsTab'; type EventsTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: ApplicationSetKind; - namespace?: string; - name?: string; }; -const EventsTab: React.FC = ({ obj }) => { +const ApplicationSetEventsTab: React.FC = ({ obj }) => { if (!obj) return null; - const status = obj.status || {}; - - return ( - - - Events - - {status.conditions && status.conditions.length > 0 ? ( -
- {status.conditions.map((condition: any, index: number) => ( -
-
-
-
- {condition.status === 'True' ? '✓' : '✗'} -
- - {condition.type} - -
- - {condition.status} - -
-
- {condition.message || 'No message available'} -
- {condition.lastTransitionTime && ( -
- Last updated: {new Date(condition.lastTransitionTime).toLocaleString()} -
- )} -
- ))} -
- ) : ( -
-
- 📊 -
-
-
No Events
-
- No events have been recorded for this ApplicationSet. -
-
-
- )} -
- ); + return ; }; -export default EventsTab; +export default ApplicationSetEventsTab; diff --git a/src/gitops/components/appset/GeneratorsTab.tsx b/src/gitops/components/appset/GeneratorsTab.tsx index 9030b4ff..c9f77bd1 100644 --- a/src/gitops/components/appset/GeneratorsTab.tsx +++ b/src/gitops/components/appset/GeneratorsTab.tsx @@ -7,8 +7,6 @@ import './GeneratorsTab.scss'; type GeneratorsTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: ApplicationSetKind; - namespace?: string; - name?: string; }; const GeneratorsTab: React.FC = ({ obj }) => { diff --git a/src/gitops/components/appset/README.md b/src/gitops/components/appset/README.md deleted file mode 100644 index b2f027aa..00000000 --- a/src/gitops/components/appset/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# ApplicationSet Components - -This directory contains the refactored ApplicationSet components following the modular structure used in the gitops-admin-plugin. - -## Structure - -``` -appset/ -├── AppSetNavPage.tsx # Main navigation page with tabs -├── AppSetDetailsTab.tsx # Details tab component -├── GeneratorsTab.tsx # Generators tab component -├── AppsTab.tsx # Applications tab component -├── EventsTab.tsx # Events tab component -├── YAMLTab.tsx # YAML tab component -├── Generators.tsx # Main generators component -├── generators/ # Individual generator components -│ ├── GitGenerator.tsx -│ ├── ListGenerator.tsx -│ ├── ClusterGenerator.tsx -│ ├── MatrixGenerator.tsx -│ ├── UnionGenerator.tsx -│ ├── MergeGenerator.tsx -│ ├── GenericGenerator.tsx -│ └── SubGenerator.tsx -├── hooks/ # Custom hooks (if needed) -└── index.ts # Export file -``` - -## Components - -### AppSetNavPage -The main navigation component that handles tab switching and renders the appropriate tab content. - -### Tab Components -- **AppSetDetailsTab**: Displays basic ApplicationSet information and conditions -- **GeneratorsTab**: Shows the generators configuration -- **AppsTab**: Lists applications generated by the ApplicationSet -- **EventsTab**: Displays events and conditions -- **YAMLTab**: Shows the YAML representation of the ApplicationSet - -### Generator Components -Each generator type has its own component: -- **GitGenerator**: For git-based generators -- **ListGenerator**: For list-based generators -- **ClusterGenerator**: For cluster-based generators -- **MatrixGenerator**: For matrix generators with sub-generators -- **UnionGenerator**: For union generators with sub-generators -- **MergeGenerator**: For merge generators with sub-generators -- **GenericGenerator**: For unknown generator types -- **SubGenerator**: For rendering sub-generators within matrix/union/merge generators - -## Usage - -The main entry point is `AppSetNavPage` which is used in the `ApplicationSetDetailsPage`: - -```tsx -import AppSetNavPage from '../appset/AppSetNavPage'; - -const ApplicationSetDetailsPage: React.FC = () => { - const { name, ns } = useParams<{ name: string; ns: string }>(); - - return ( - - ); -}; -``` - -## Styling - -Each component has its own SCSS file for styling: -- `AppSetNavPage.scss` -- `AppSetDetailsTab.scss` -- `GeneratorsTab.scss` -- `AppsTab.scss` -- `EventsTab.scss` -- `YAMLTab.scss` -- `generators/Generators.scss` - -## Benefits of This Structure - -1. **Modularity**: Each component has a single responsibility -2. **Reusability**: Components can be easily reused or modified -3. **Maintainability**: Easier to maintain and debug individual components -4. **Consistency**: Follows the same pattern as gitops-admin-plugin -5. **Scalability**: Easy to add new generator types or modify existing ones - - - diff --git a/src/gitops/components/appset/YAMLTab.tsx b/src/gitops/components/appset/YAMLTab.tsx deleted file mode 100644 index 5c84fb22..00000000 --- a/src/gitops/components/appset/YAMLTab.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react'; -import { ApplicationSetKind } from '../../models/ApplicationSetModel'; -import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; - -type YAMLTabProps = { - obj?: ApplicationSetKind; - namespace?: string; - name?: string; -}; - -const YAMLTab: React.FC = ({ obj, namespace, name }) => { - if (!obj) return null; - - return ( - - ); -}; - -export default YAMLTab; diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index 102d4489..8704fa86 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -129,11 +129,20 @@ const ApplicationList: React.FC = ({ return sortData(applications, sortBy, direction); }, [applications, sortBy, direction]); - // Filter by search query if present + // TODO: use alternate filter since it is deprecated. See DataTableView potentially + const [, filteredData, onFilterChange] = useListPageFilter(sortedApplications, filters); + + // Filter applications by project or appset before rendering rows + const filteredByOwner = React.useMemo( + () => filteredData.filter(filterApp(project, appset)), + [filteredData, project, appset], + ); + + // Filter by search query if present (after other filters) const filteredBySearch = React.useMemo(() => { - if (!searchQuery) return sortedApplications; + if (!searchQuery) return filteredByOwner; - return sortedApplications.filter((app) => { + return filteredByOwner.filter((app) => { const labels = app.metadata?.labels || {}; // Check if any label matches the search query return Object.entries(labels).some(([key, value]) => { @@ -141,23 +150,23 @@ const ApplicationList: React.FC = ({ return labelSelector.includes(searchQuery) || key.includes(searchQuery); }); }); - }, [sortedApplications, searchQuery]); - - // TODO: use alternate filter since it is deprecated. See DataTableView potentially - const [, filteredData, onFilterChange] = useListPageFilter(filteredBySearch, filters); - // Filter applications by project or appset before rendering rows - const filteredByOwner = React.useMemo( - () => filteredData.filter(filterApp(project, appset)), - [filteredData, project, appset], - ); - const rows = useApplicationRowsDV(filteredByOwner, namespace); + }, [filteredByOwner, searchQuery]); + const rows = useApplicationRowsDV(filteredBySearch, namespace); const empty = ( - + - There are no Argo CD Applications in {namespace ? 'this project' : 'all projects'}. + {searchQuery ? ( + <> + No Argo CD Applications match the label filter "{searchQuery}". +
+ Try removing the filter or selecting a different label to see more applications. + + ) : ( + `There are no Argo CD Applications in ${namespace ? 'this project' : 'all projects'}.` + )}
@@ -167,7 +176,7 @@ const ApplicationList: React.FC = ({ let currentActiveState = null; if (loadError) { currentActiveState = DataViewState.error; - } else if (filteredByOwner.length === 0) { + } else if (filteredBySearch.length === 0) { currentActiveState = DataViewState.empty; } return ( diff --git a/src/gitops/components/shared/ApplicationSetList.tsx b/src/gitops/components/shared/ApplicationSetList.tsx index a01c9a23..81984434 100644 --- a/src/gitops/components/shared/ApplicationSetList.tsx +++ b/src/gitops/components/shared/ApplicationSetList.tsx @@ -154,11 +154,13 @@ const ApplicationSetList: React.FC = ({ return sortData(applicationSets as ApplicationSetKind[], sortBy, direction, applications, appsLoaded); }, [applicationSets, sortBy, direction, applications, appsLoaded]); - // Filter by search query if present + const [data, filteredData, onFilterChange] = useListPageFilter(sortedApplicationSets, filters); + + // Filter by search query if present (after other filters) const filteredBySearch = React.useMemo(() => { - if (!searchQuery) return sortedApplicationSets; + if (!searchQuery) return filteredData; - return sortedApplicationSets.filter((appSet) => { + return filteredData.filter((appSet) => { const labels = appSet.metadata?.labels || {}; // Check if any label matches the search query return Object.entries(labels).some(([key, value]) => { @@ -166,18 +168,25 @@ const ApplicationSetList: React.FC = ({ return labelSelector.includes(searchQuery) || key.includes(searchQuery); }); }); - }, [sortedApplicationSets, searchQuery]); + }, [filteredData, searchQuery]); - const [data, filteredData, onFilterChange] = useListPageFilter(filteredBySearch, filters); - const rows = useApplicationSetRowsDV(filteredData, namespace, applications, appsLoaded); + const rows = useApplicationSetRowsDV(filteredBySearch, namespace, applications, appsLoaded); const empty = ( - + - There are no Argo CD ApplicationSets in {namespace ? 'this project' : 'all projects'}. + {searchQuery ? ( + <> + No Argo CD ApplicationSets match the label filter "{searchQuery}". +
+ Try removing the filter or selecting a different label to see more ApplicationSets. + + ) : ( + `There are no Argo CD ApplicationSets in ${namespace ? 'this project' : 'all projects'}.` + )}
@@ -201,7 +210,7 @@ const ApplicationSetList: React.FC = ({ let currentActiveState = null; if (loadError) { currentActiveState = DataViewState.error; - } else if (applicationSets.length === 0) { + } else if (filteredBySearch.length === 0) { currentActiveState = DataViewState.empty; } From bfc049146045ad968176e81436c3f04b5ef12f46 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 3 Oct 2025 23:46:08 -0400 Subject: [PATCH 13/16] resolve ApplicationSet compilation errors Signed-off-by: Atif Ali --- src/gitops/components/appset/AppSetDetailsTab.tsx | 4 ++-- src/gitops/components/appset/index.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/gitops/components/appset/AppSetDetailsTab.tsx b/src/gitops/components/appset/AppSetDetailsTab.tsx index 4cb2b990..18e0dcae 100644 --- a/src/gitops/components/appset/AppSetDetailsTab.tsx +++ b/src/gitops/components/appset/AppSetDetailsTab.tsx @@ -14,7 +14,7 @@ import { getAppSetGeneratorCount, getAppSetStatus } from '../../utils/gitops'; import { useK8sWatchResource, ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; import { ApplicationKind, ApplicationModel } from '../../models/ApplicationModel'; import { Conditions } from '../../utils/components/Conditions/Conditions'; -import Status from '../shared/Status/Status'; +import HealthStatus from '../../Statuses/HealthStatus'; import './AppSetDetailsTab.scss'; type AppSetDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & { @@ -70,7 +70,7 @@ const AppSetDetailsTab: React.FC = ({ obj }) => { title="Status" help="Current health status of the ApplicationSet." > - + Date: Mon, 6 Oct 2025 17:57:16 -0400 Subject: [PATCH 14/16] run yarn lint --fix Signed-off-by: Atif Ali --- .../topology/console/PodsOverview.tsx | 2 +- .../sidebar/DeploymentSideBarDetails.tsx | 1 - src/components/utils/gitops-utils.ts | 9 +- .../application/ApplicationSetDetailsPage.tsx | 9 +- .../components/appset/AppSetDetailsTab.tsx | 150 ++++---- .../components/appset/AppSetNavPage.tsx | 35 +- src/gitops/components/appset/AppsTab.tsx | 7 +- src/gitops/components/appset/EventsTab.tsx | 1 + src/gitops/components/appset/Generators.tsx | 9 +- .../components/appset/GeneratorsTab.tsx | 44 ++- .../appset/generators/ClusterGenerator.tsx | 31 +- .../appset/generators/GeneratorView.tsx | 12 +- .../appset/generators/GenericGenerator.tsx | 31 +- .../appset/generators/GitGenerator.tsx | 21 +- .../appset/generators/ListGenerator.tsx | 37 +- .../appset/generators/MatrixGenerator.tsx | 7 +- .../appset/generators/MergeGenerator.tsx | 18 +- .../appset/generators/UnionGenerator.tsx | 7 +- src/gitops/components/appset/index.ts | 10 +- .../components/shared/ApplicationList.tsx | 53 +-- .../components/shared/ApplicationSetList.tsx | 131 +++---- .../BaseDetailsSummary/BaseDetailsSummary.tsx | 6 +- .../components/Conditions/Conditions.tsx | 56 ++- .../ResourceDetailsAttributes.tsx | 337 +++++++++++++----- 24 files changed, 635 insertions(+), 389 deletions(-) diff --git a/src/components/topology/console/PodsOverview.tsx b/src/components/topology/console/PodsOverview.tsx index 8d93e328..0a0f8040 100644 --- a/src/components/topology/console/PodsOverview.tsx +++ b/src/components/topology/console/PodsOverview.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { Grid, GridItem } from '@patternfly/react-core'; import { Link } from 'react-router-dom-v5-compat'; import * as _ from 'lodash'; import { PodPhase, ResourceLink, StatusComponent } from '@openshift-console/dynamic-plugin-sdk'; import { ExtPodKind } from '@openshift-console/dynamic-plugin-sdk-internal/lib/extensions/console-types'; +import { Grid, GridItem } from '@patternfly/react-core'; import { PodTraffic } from './pod-traffic'; import { ContainerStatus } from './types'; diff --git a/src/components/topology/sidebar/DeploymentSideBarDetails.tsx b/src/components/topology/sidebar/DeploymentSideBarDetails.tsx index 53da22a8..699f3e8e 100644 --- a/src/components/topology/sidebar/DeploymentSideBarDetails.tsx +++ b/src/components/topology/sidebar/DeploymentSideBarDetails.tsx @@ -205,7 +205,6 @@ type DeploymentSideBarDetailsProps = { export const DeploymentSideBarDetails: React.FC = ({ rollout: d, - rolloutKind: rolloutKind, }) => { const { t } = useTranslation(); const model = getK8sModel(d); diff --git a/src/components/utils/gitops-utils.ts b/src/components/utils/gitops-utils.ts index 82a1c10d..a06ceaa6 100644 --- a/src/components/utils/gitops-utils.ts +++ b/src/components/utils/gitops-utils.ts @@ -41,8 +41,8 @@ export const getApplicationsListBaseURI = () => { export class RetryError extends Error {} export class TimeoutError extends Error { - constructor(url: any, ms: any, ...params: any[]) { - super(`Call to ${url} timed out after ${ms}ms.`); //å ...params); + constructor(url: any, ms: any) { + super(`Call to ${url} timed out after ${ms}ms.`); // Dumb hack to fix `instanceof TimeoutError` Object.setPrototypeOf(this, TimeoutError.prototype); } @@ -64,7 +64,7 @@ const getCSRFToken = () => .map((c) => c.slice(cookiePrefix.length)) .pop(); -export const validateStatus = async (response: Response, url, method, retry) => { +export const validateStatus = async (response: Response, url, method) => { console.log('VALIDATE STATUS - RESPONSE STATUS IS ' + response.status); console.log('VALIDATE STATUS - RESPONSE TEXT IS ' + response.text); console.log('VALIDATE STATUS - RESPONSE BODY IS ' + response.body); @@ -101,7 +101,6 @@ export const validateStatus = async (response: Response, url, method, retry) => // retry 409 conflict errors due to ClustResourceQuota / ResourceQuota // https://bugzilla.redhat.com/show_bug.cgi?id=1920699 if ( - retry && method === 'POST' && response.status === 409 && ['resourcequotas', 'clusterresourcequotas'].includes(json.details?.kind) @@ -129,7 +128,7 @@ export const validateStatus = async (response: Response, url, method, retry) => }); }; -export const coFetchInternal = async (url, options, timeout, retry) => { +export const coFetchInternal = async (url, options, timeout) => { const allOptions = _.defaultsDeep({}, initDefaults, options); if (allOptions.method !== 'GET') { allOptions.headers['X-CSRFToken'] = getCSRFToken(); diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index 212577fe..44b718c8 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -1,17 +1,12 @@ import * as React from 'react'; import { useParams } from 'react-router-dom-v5-compat'; + import AppSetNavPage from '../appset/AppSetNavPage'; const ApplicationSetDetailsPage: React.FC = () => { const { name, ns } = useParams<{ name: string; ns: string }>(); - return ( - - ); + return ; }; export default ApplicationSetDetailsPage; diff --git a/src/gitops/components/appset/AppSetDetailsTab.tsx b/src/gitops/components/appset/AppSetDetailsTab.tsx index 18e0dcae..9888c993 100644 --- a/src/gitops/components/appset/AppSetDetailsTab.tsx +++ b/src/gitops/components/appset/AppSetDetailsTab.tsx @@ -1,20 +1,18 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; -import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; -import { - Badge, - PageSection, - Title, - DescriptionList, - Flex, - FlexItem, -} from '@patternfly/react-core'; -import BaseDetailsSummary, { DetailsDescriptionGroup } from '../shared/BaseDetailsSummary/BaseDetailsSummary'; -import { getAppSetGeneratorCount, getAppSetStatus } from '../../utils/gitops'; -import { useK8sWatchResource, ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; + +import { ResourceLink, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Badge, DescriptionList, Flex, FlexItem, PageSection, Title } from '@patternfly/react-core'; + import { ApplicationKind, ApplicationModel } from '../../models/ApplicationModel'; -import { Conditions } from '../../utils/components/Conditions/Conditions'; +import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; import HealthStatus from '../../Statuses/HealthStatus'; +import { Conditions } from '../../utils/components/Conditions/Conditions'; +import { getAppSetGeneratorCount, getAppSetStatus } from '../../utils/gitops'; +import BaseDetailsSummary, { + DetailsDescriptionGroup, +} from '../shared/BaseDetailsSummary/BaseDetailsSummary'; + import './AppSetDetailsTab.scss'; type AppSetDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & { @@ -23,13 +21,7 @@ type AppSetDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & const AppSetDetailsTab: React.FC = ({ obj }) => { const namespace = obj?.metadata?.namespace; - if (!obj) return null; - const status = obj.status || {}; - const spec = obj.spec || {}; - const totalGenerators = getAppSetGeneratorCount(obj); - const appSetStatus = getAppSetStatus(obj); - // Get applications to count generated apps const [applications] = useK8sWatchResource({ groupVersionKind: { @@ -37,16 +29,24 @@ const AppSetDetailsTab: React.FC = ({ obj }) => { version: ApplicationModel.apiVersion, kind: ApplicationModel.kind, }, - namespace: namespace || obj.metadata?.namespace, + namespace: namespace || obj?.metadata?.namespace, isList: true, }); + if (!obj) return null; + + const status = obj.status || {}; + const spec = obj.spec || {}; + const totalGenerators = getAppSetGeneratorCount(obj); + const appSetStatus = getAppSetStatus(obj); + // Count applications owned by this ApplicationSet - const generatedAppsCount = applications?.filter(app => - app.metadata?.ownerReferences?.some(owner => - owner.kind === obj.kind && owner.name === obj.metadata?.name - ) - ).length || 0; + const generatedAppsCount = + applications?.filter((app) => + app.metadata?.ownerReferences?.some( + (owner) => owner.kind === obj.kind && owner.name === obj.metadata?.name, + ), + ).length || 0; return ( <> @@ -66,52 +66,60 @@ const AppSetDetailsTab: React.FC = ({ obj }) => { - - - - - - {generatedAppsCount} application{generatedAppsCount !== 1 ? 's' : ''} - - - - {totalGenerators} generator{totalGenerators !== 1 ? 's' : ''} - - - - - - - {spec.template?.spec?.source?.repoURL && ( - - - {spec.template.spec.source.repoURL} - - - )} + + + + + + + {generatedAppsCount} application{generatedAppsCount !== 1 ? 's' : ''} + + + + + + {totalGenerators} generator{totalGenerators !== 1 ? 's' : ''} + + + + + + + + {spec.template?.spec?.source?.repoURL && ( + + + {spec.template.spec.source.repoURL} + + + )} diff --git a/src/gitops/components/appset/AppSetNavPage.tsx b/src/gitops/components/appset/AppSetNavPage.tsx index d308da3a..16af576b 100644 --- a/src/gitops/components/appset/AppSetNavPage.tsx +++ b/src/gitops/components/appset/AppSetNavPage.tsx @@ -1,15 +1,18 @@ import * as React from 'react'; -import { useK8sWatchResource, HorizontalNav } from '@openshift-console/dynamic-plugin-sdk'; -import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; -import { Spinner, Bullseye } from '@patternfly/react-core'; -import DetailsPageHeader from '../shared/DetailsPageHeader/DetailsPageHeader'; + +import { HorizontalNav, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Bullseye, Spinner } from '@patternfly/react-core'; + import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; +import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import DetailsPageHeader from '../shared/DetailsPageHeader/DetailsPageHeader'; +import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; + import AppSetDetailsTab from './AppSetDetailsTab'; -import GeneratorsTab from './GeneratorsTab'; import AppsTab from './AppsTab'; import ApplicationSetEventsTab from './EventsTab'; -import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; +import GeneratorsTab from './GeneratorsTab'; type AppSetPageProps = { name: string; @@ -39,11 +42,11 @@ const AppSetNavPage: React.FC = ({ name, namespace, kind }) => name: t('Details'), component: AppSetDetailsTab, }, - { - href: 'yaml', - name: t('YAML'), - component: ResourceYAMLTab, - }, + { + href: 'yaml', + name: t('YAML'), + component: ResourceYAMLTab, + }, { href: 'generators', name: t('Generators'), @@ -54,11 +57,11 @@ const AppSetNavPage: React.FC = ({ name, namespace, kind }) => name: t('Applications'), component: AppsTab, }, - { - href: 'events', - name: t('Events'), - component: ApplicationSetEventsTab, - }, + { + href: 'events', + name: t('Events'), + component: ApplicationSetEventsTab, + }, ], [t], ); diff --git a/src/gitops/components/appset/AppsTab.tsx b/src/gitops/components/appset/AppsTab.tsx index 2843c148..467d1226 100644 --- a/src/gitops/components/appset/AppsTab.tsx +++ b/src/gitops/components/appset/AppsTab.tsx @@ -1,8 +1,11 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; -import { ApplicationSetKind } from '../../models/ApplicationSetModel'; + import { PageSection } from '@patternfly/react-core'; + +import { ApplicationSetKind } from '../../models/ApplicationSetModel'; import ApplicationList from '../shared/ApplicationList'; + import './AppsTab.scss'; type AppsTabProps = RouteComponentProps<{ ns: string; name: string }> & { @@ -15,7 +18,7 @@ const AppsTab: React.FC = ({ obj }) => { return ( - = ({ generators }) => { }; export default Generators; - - diff --git a/src/gitops/components/appset/GeneratorsTab.tsx b/src/gitops/components/appset/GeneratorsTab.tsx index c9f77bd1..03a16a6a 100644 --- a/src/gitops/components/appset/GeneratorsTab.tsx +++ b/src/gitops/components/appset/GeneratorsTab.tsx @@ -1,8 +1,12 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; -import { ApplicationSetKind } from '../../models/ApplicationSetModel'; + import { PageSection } from '@patternfly/react-core'; + +import { ApplicationSetKind } from '../../models/ApplicationSetModel'; + import Generators from './Generators'; + import './GeneratorsTab.scss'; type GeneratorsTabProps = RouteComponentProps<{ ns: string; name: string }> & { @@ -17,26 +21,30 @@ const GeneratorsTab: React.FC = ({ obj }) => { {obj.spec?.generators && obj.spec.generators.length > 0 ? ( ) : ( -
-
+ padding: '40px 20px', + color: '#8a8d90', + fontSize: '16px', + }} + > +
⚙️
diff --git a/src/gitops/components/appset/generators/ClusterGenerator.tsx b/src/gitops/components/appset/generators/ClusterGenerator.tsx index 60b2e2d9..2e8f4b0b 100644 --- a/src/gitops/components/appset/generators/ClusterGenerator.tsx +++ b/src/gitops/components/appset/generators/ClusterGenerator.tsx @@ -1,6 +1,13 @@ import * as React from 'react'; -import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core'; + +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, +} from '@patternfly/react-core'; import { ClusterIcon } from '@patternfly/react-icons'; + import GeneratorView from './GeneratorView'; interface ClusterGeneratorProps { @@ -15,16 +22,18 @@ const ClusterGenerator: React.FC = ({ generator }) => { Selector -
+
{JSON.stringify(generator.selector, null, 2)}
diff --git a/src/gitops/components/appset/generators/GeneratorView.tsx b/src/gitops/components/appset/generators/GeneratorView.tsx index c98112bc..b561a901 100644 --- a/src/gitops/components/appset/generators/GeneratorView.tsx +++ b/src/gitops/components/appset/generators/GeneratorView.tsx @@ -1,4 +1,6 @@ +import * as React from 'react'; import { ReactNode } from 'react'; + import { Card, CardBody, CardTitle, Divider, Icon } from '@patternfly/react-core'; type GeneratorViewProps = { @@ -7,16 +9,14 @@ type GeneratorViewProps = { children?: ReactNode; }; -const GeneratorView = ({ title, icon, children }: GeneratorViewProps) => ( +const GeneratorView: React.FC = ({ title, icon, children }) => ( -
+
{icon && {icon}} - {title} + {title}
- {children && ( - - )} + {children && } {children && {children}} diff --git a/src/gitops/components/appset/generators/GenericGenerator.tsx b/src/gitops/components/appset/generators/GenericGenerator.tsx index d62a0a26..bc52536b 100644 --- a/src/gitops/components/appset/generators/GenericGenerator.tsx +++ b/src/gitops/components/appset/generators/GenericGenerator.tsx @@ -1,6 +1,13 @@ import * as React from 'react'; -import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core'; + +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, +} from '@patternfly/react-core'; import { QuestionCircleIcon } from '@patternfly/react-icons'; + import GeneratorView from './GeneratorView'; interface GenericGeneratorProps { @@ -15,16 +22,18 @@ const GenericGenerator: React.FC = ({ gentype, generator Configuration -
+
{JSON.stringify(generator, null, 2)}
diff --git a/src/gitops/components/appset/generators/GitGenerator.tsx b/src/gitops/components/appset/generators/GitGenerator.tsx index e1c60c86..3aed2e33 100644 --- a/src/gitops/components/appset/generators/GitGenerator.tsx +++ b/src/gitops/components/appset/generators/GitGenerator.tsx @@ -1,6 +1,13 @@ import * as React from 'react'; -import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core'; + +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, +} from '@patternfly/react-core'; import { GitAltIcon } from '@patternfly/react-icons'; + import GeneratorView from './GeneratorView'; interface GitGeneratorProps { @@ -8,8 +15,8 @@ interface GitGeneratorProps { } const GitGenerator: React.FC = ({ generator }) => { - const generatorType = generator.files ? "File" : "Directory"; - + const generatorType = generator.files ? 'File' : 'Directory'; + return ( } title={`git (${generatorType})`}> @@ -28,13 +35,17 @@ const GitGenerator: React.FC = ({ generator }) => { {generator.directories && ( Directories - {generator.directories.length} directory(ies) + + {generator.directories.length} directory(ies) + )} {generator.files && ( Files - {generator.files.length} file(s) + + {generator.files.length} file(s) + )} diff --git a/src/gitops/components/appset/generators/ListGenerator.tsx b/src/gitops/components/appset/generators/ListGenerator.tsx index 34be1854..73fa6393 100644 --- a/src/gitops/components/appset/generators/ListGenerator.tsx +++ b/src/gitops/components/appset/generators/ListGenerator.tsx @@ -1,6 +1,15 @@ import * as React from 'react'; -import { ExpandableSection, DataList, DataListItem, DataListCell, DataListItemRow, DataListItemCells } from '@patternfly/react-core'; + +import { + DataList, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + ExpandableSection, +} from '@patternfly/react-core'; import { ListIcon } from '@patternfly/react-icons'; + import GeneratorView from './GeneratorView'; interface ListGeneratorProps { @@ -10,21 +19,21 @@ interface ListGeneratorProps { const ListGenerator: React.FC = ({ generator }) => { const [isExpanded, setIsExpanded] = React.useState(false); - const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => { - setIsExpanded(isExpanded); + const onToggle = (_event: React.MouseEvent, expanded: boolean) => { + setIsExpanded(expanded); }; const displayValue = (value: any) => { - if (value === undefined) return "null"; - else if (typeof value === "object") return JSON.stringify(value); + if (value === undefined) return 'null'; + else if (typeof value === 'object') return JSON.stringify(value); else return value; }; return ( } title="List"> - {generator.elements && generator.elements.length > 0 && ( @@ -33,11 +42,13 @@ const ListGenerator: React.FC = ({ generator }) => { ( - - {key}: {displayValue(val)} - - ))} + dataListCells={Object.entries(item).map( + ([key, val]: [string, any], colIndex: number) => ( + + {key}: {displayValue(val)} + + ), + )} /> diff --git a/src/gitops/components/appset/generators/MatrixGenerator.tsx b/src/gitops/components/appset/generators/MatrixGenerator.tsx index 5bf1461a..25140392 100644 --- a/src/gitops/components/appset/generators/MatrixGenerator.tsx +++ b/src/gitops/components/appset/generators/MatrixGenerator.tsx @@ -1,8 +1,11 @@ import * as React from 'react'; + import { ThLargeIcon } from '@patternfly/react-icons'; -import GeneratorView from './GeneratorView'; + import Generators from '../Generators'; +import GeneratorView from './GeneratorView'; + interface MatrixGeneratorProps { generator: any; } @@ -12,7 +15,7 @@ const MatrixGenerator: React.FC = ({ generator }) => { <> } title="Matrix" />
-
+
diff --git a/src/gitops/components/appset/generators/MergeGenerator.tsx b/src/gitops/components/appset/generators/MergeGenerator.tsx index 23b76ba3..3ae91358 100644 --- a/src/gitops/components/appset/generators/MergeGenerator.tsx +++ b/src/gitops/components/appset/generators/MergeGenerator.tsx @@ -1,9 +1,17 @@ import * as React from 'react'; -import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core'; + +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, +} from '@patternfly/react-core'; import { ObjectGroupIcon } from '@patternfly/react-icons'; -import GeneratorView from './GeneratorView'; + import Generators from '../Generators'; +import GeneratorView from './GeneratorView'; + interface MergeGeneratorProps { generator: any; } @@ -16,13 +24,15 @@ const MergeGenerator: React.FC = ({ generator }) => { Merge Keys - {generator.mergeKeys.join(', ')} + + {generator.mergeKeys.join(', ')} + )}
-
+
diff --git a/src/gitops/components/appset/generators/UnionGenerator.tsx b/src/gitops/components/appset/generators/UnionGenerator.tsx index 64d4c3c5..356ea0ff 100644 --- a/src/gitops/components/appset/generators/UnionGenerator.tsx +++ b/src/gitops/components/appset/generators/UnionGenerator.tsx @@ -1,8 +1,11 @@ import * as React from 'react'; + import { ThIcon } from '@patternfly/react-icons'; -import GeneratorView from './GeneratorView'; + import Generators from '../Generators'; +import GeneratorView from './GeneratorView'; + interface UnionGeneratorProps { generator: any; } @@ -12,7 +15,7 @@ const UnionGenerator: React.FC = ({ generator }) => { <> } title="Union" />
-
+
diff --git a/src/gitops/components/appset/index.ts b/src/gitops/components/appset/index.ts index 92686264..f0212259 100644 --- a/src/gitops/components/appset/index.ts +++ b/src/gitops/components/appset/index.ts @@ -1,15 +1,15 @@ -export { default as AppSetNavPage } from './AppSetNavPage'; export { default as AppSetDetailsTab } from './AppSetDetailsTab'; -export { default as GeneratorsTab } from './GeneratorsTab'; +export { default as AppSetNavPage } from './AppSetNavPage'; export { default as AppsTab } from './AppsTab'; export { default as EventsTab } from './EventsTab'; export { default as Generators } from './Generators'; +export { default as GeneratorsTab } from './GeneratorsTab'; // Export generator components +export { default as ClusterGenerator } from './generators/ClusterGenerator'; +export { default as GenericGenerator } from './generators/GenericGenerator'; export { default as GitGenerator } from './generators/GitGenerator'; export { default as ListGenerator } from './generators/ListGenerator'; -export { default as ClusterGenerator } from './generators/ClusterGenerator'; export { default as MatrixGenerator } from './generators/MatrixGenerator'; -export { default as UnionGenerator } from './generators/UnionGenerator'; export { default as MergeGenerator } from './generators/MergeGenerator'; -export { default as GenericGenerator } from './generators/GenericGenerator'; +export { default as UnionGenerator } from './generators/UnionGenerator'; diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index 8704fa86..84de42c3 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -1,13 +1,3 @@ -import { - DataViewTable, - DataViewTh, - DataViewTr, -} from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; -import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks'; -import { Spinner, Flex, FlexItem, EmptyState, EmptyStateBody } from '@patternfly/react-core'; -import { CubesIcon } from '@patternfly/react-icons'; -import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; -import DataView, { DataViewState } from '@patternfly/react-data-view/dist/esm/DataView'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom-v5-compat'; @@ -25,6 +15,16 @@ import { useK8sWatchResource, useListPageFilter, } from '@openshift-console/dynamic-plugin-sdk'; +import { EmptyState, EmptyStateBody, Flex, FlexItem, Spinner } from '@patternfly/react-core'; +import { + DataViewTable, + DataViewTh, + DataViewTr, +} from '@patternfly/react-data-view/dist/dynamic/DataViewTable'; +import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks'; +import DataView, { DataViewState } from '@patternfly/react-data-view/dist/esm/DataView'; +import { CubesIcon } from '@patternfly/react-icons'; +import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; import { useApplicationActionsProvider } from '../..//hooks/useApplicationActionsProvider'; import RevisionFragment from '../..//Revision/Revision'; @@ -108,10 +108,10 @@ const ApplicationList: React.FC = ({ () => COLUMNS_KEYS_INDEXES.findIndex((item) => item.key === sortBy), [COLUMNS_KEYS_INDEXES, sortBy], ); - + // Get search query from URL parameters const searchQuery = searchParams.get('q') || ''; - + const getSortParams = (columnIndex: number): ThProps['sort'] => ({ sortBy: { index: sortByIndex, @@ -128,20 +128,20 @@ const ApplicationList: React.FC = ({ const sortedApplications = React.useMemo(() => { return sortData(applications, sortBy, direction); }, [applications, sortBy, direction]); - + // TODO: use alternate filter since it is deprecated. See DataTableView potentially const [, filteredData, onFilterChange] = useListPageFilter(sortedApplications, filters); - + // Filter applications by project or appset before rendering rows const filteredByOwner = React.useMemo( () => filteredData.filter(filterApp(project, appset)), [filteredData, project, appset], ); - + // Filter by search query if present (after other filters) const filteredBySearch = React.useMemo(() => { if (!searchQuery) return filteredByOwner; - + return filteredByOwner.filter((app) => { const labels = app.metadata?.labels || {}; // Check if any label matches the search query @@ -156,16 +156,23 @@ const ApplicationList: React.FC = ({ - + {searchQuery ? ( <> - No Argo CD Applications match the label filter "{searchQuery}". + No Argo CD Applications match the label filter{' '} + "{searchQuery}".
Try removing the filter or selecting a different label to see more applications. ) : ( - `There are no Argo CD Applications in ${namespace ? 'this project' : 'all projects'}.` + `There are no Argo CD Applications in ${ + namespace ? 'this project' : 'all projects' + }.` )}
@@ -202,11 +209,7 @@ const ApplicationList: React.FC = ({ /> )} - +
@@ -491,4 +494,4 @@ export const filters: RowFilter[] = [ }, ]; -export default ApplicationList; \ No newline at end of file +export default ApplicationList; diff --git a/src/gitops/components/shared/ApplicationSetList.tsx b/src/gitops/components/shared/ApplicationSetList.tsx index 81984434..86cdab54 100644 --- a/src/gitops/components/shared/ApplicationSetList.tsx +++ b/src/gitops/components/shared/ApplicationSetList.tsx @@ -25,29 +25,26 @@ import DataView, { DataViewState } from '@patternfly/react-data-view/dist/esm/Da import { CubesIcon } from '@patternfly/react-icons'; import { Tbody, Td, Tr } from '@patternfly/react-table'; +import DevPreviewBadge from '../../../components/import/badges/DevPreviewBadge'; import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; -import { ApplicationSetStatus } from '../../utils/constants'; import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; -import { getAppSetStatus, getAppSetGeneratorCount } from '../../utils/gitops'; import ActionsDropdown from '../../utils/components/ActionDropDown/ActionDropDown'; -import { modelToGroupVersionKind, modelToRef } from '../../utils/utils'; -import DevPreviewBadge from '../../../components/import/badges/DevPreviewBadge'; - // Import status icons for consistency with ApplicationList import { HealthDegradedIcon, HealthHealthyIcon, HealthUnknownIcon, } from '../../utils/components/Icons/Icons'; - - +import { ApplicationSetStatus } from '../../utils/constants'; +import { getAppSetGeneratorCount, getAppSetStatus } from '../../utils/gitops'; +import { modelToGroupVersionKind, modelToRef } from '../../utils/utils'; const formatCreationTimestamp = (timestamp: string): string => { if (!timestamp) return '-'; const date = new Date(timestamp); const now = new Date(); const diffInMinutes = (now.getTime() - date.getTime()) / (1000 * 60); - + if (diffInMinutes < 60) { return `${Math.floor(diffInMinutes)}m ago`; } else if (diffInMinutes < 60 * 24) { @@ -63,20 +60,24 @@ const formatCreationTimestamp = (timestamp: string): string => { }; // Helper function to get generated applications count -const getGeneratedAppsCount = (appSet: ApplicationSetKind, applications: any[], appsLoaded: boolean): number => { +const getGeneratedAppsCount = ( + appSet: ApplicationSetKind, + applications: any[], + appsLoaded: boolean, +): number => { if (!applications || !appsLoaded) return 0; - + return applications.filter((app: any) => { if (!app.metadata?.ownerReferences) return false; - return app.metadata.ownerReferences.some((owner: any) => - owner.kind === 'ApplicationSet' && owner.name === appSet.metadata.name + return app.metadata.ownerReferences.some( + (owner: any) => owner.kind === 'ApplicationSet' && owner.name === appSet.metadata.name, ); }).length; }; const ApplicationSetStatusFragment: React.FC<{ status: string }> = ({ status }) => { let targetIcon: React.ReactNode; - + switch (status) { case ApplicationSetStatus.HEALTHY: targetIcon = ; @@ -133,10 +134,10 @@ const ApplicationSetList: React.FC = ({ const [searchParams, setSearchParams] = useSearchParams(); const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams }); - + // Get search query from URL parameters const searchQuery = searchParams.get('q') || ''; - + const getSortParams = (columnId: string, columnIndex: number) => ({ sortBy: { index: columnIndex, @@ -151,15 +152,21 @@ const ApplicationSetList: React.FC = ({ const columnsDV = useColumnsDV(namespace, getSortParams); const sortedApplicationSets = React.useMemo(() => { - return sortData(applicationSets as ApplicationSetKind[], sortBy, direction, applications, appsLoaded); + return sortData( + applicationSets as ApplicationSetKind[], + sortBy, + direction, + applications, + appsLoaded, + ); }, [applicationSets, sortBy, direction, applications, appsLoaded]); - + const [data, filteredData, onFilterChange] = useListPageFilter(sortedApplicationSets, filters); - + // Filter by search query if present (after other filters) const filteredBySearch = React.useMemo(() => { if (!searchQuery) return filteredData; - + return filteredData.filter((appSet) => { const labels = appSet.metadata?.labels || {}; // Check if any label matches the search query @@ -169,23 +176,34 @@ const ApplicationSetList: React.FC = ({ }); }); }, [filteredData, searchQuery]); - + const rows = useApplicationSetRowsDV(filteredBySearch, namespace, applications, appsLoaded); const empty = ( - + {searchQuery ? ( <> - No Argo CD ApplicationSets match the label filter "{searchQuery}". + No Argo CD ApplicationSets match the label filter{' '} + "{searchQuery}" + .
- Try removing the filter or selecting a different label to see more ApplicationSets. + Try removing the filter or selecting a different label to see more + ApplicationSets. ) : ( - `There are no Argo CD ApplicationSets in ${namespace ? 'this project' : 'all projects'}.` + `There are no Argo CD ApplicationSets in ${ + namespace ? 'this project' : 'all projects' + }.` )}
@@ -217,8 +235,8 @@ const ApplicationSetList: React.FC = ({ return (
{showTitle == undefined && ( - } hideFavoriteButton={false} > @@ -252,16 +270,17 @@ const ApplicationSetActionsCell: React.FC<{ appSet: ApplicationSetKind }> = ({ a const [actions] = useApplicationSetActionsProvider(appSet); return (
- +
); }; -const useApplicationSetRowsDV = (applicationSetsList, namespace, applications, appsLoaded): DataViewTr[] => { +const useApplicationSetRowsDV = ( + applicationSetsList, + namespace, + applications, + appsLoaded, +): DataViewTr[] => { const rows: DataViewTr[] = []; applicationSetsList.forEach((appSet: ApplicationSetKind, index: number) => { rows.push([ @@ -294,27 +313,15 @@ const useApplicationSetRowsDV = (applicationSetsList, namespace, applications, a }, { id: 'generated-apps-' + index, - cell: ( -
- {getGeneratedAppsCount(appSet, applications, appsLoaded).toString()} -
- ), + cell:
{getGeneratedAppsCount(appSet, applications, appsLoaded).toString()}
, }, { id: 'generators-' + index, - cell: ( -
- {getAppSetGeneratorCount(appSet).toString()} -
- ), + cell:
{getAppSetGeneratorCount(appSet).toString()}
, }, { id: 'created-at-' + index, - cell: ( -
- {formatCreationTimestamp(appSet.metadata.creationTimestamp)} -
- ), + cell:
{formatCreationTimestamp(appSet.metadata.creationTimestamp)}
, }, { id: 'actions-' + index, @@ -384,16 +391,16 @@ const useColumnsDV = (namespace, getSortParams) => { sort: getSortParams('generators', 3 + i), }, }, - { - id: 'created-at', - cell: 'Created At', - props: { - key: 'created-at', - 'aria-label': 'created at', - className: 'pf-m-width-15', - sort: getSortParams('created-at', 4 + i), - }, - }, + { + id: 'created-at', + cell: 'Created At', + props: { + key: 'created-at', + 'aria-label': 'created at', + className: 'pf-m-width-15', + sort: getSortParams('created-at', 4 + i), + }, + }, { id: 'actions', cell: '', @@ -428,7 +435,7 @@ export const sortData = ( sortBy: string | undefined, direction: 'asc' | 'desc' | undefined, applications: any[] = [], - appsLoaded: boolean = false, + appsLoaded = false, ) => { if (!sortBy || !direction) return data; @@ -457,10 +464,10 @@ export const sortData = ( aValue = getAppSetGeneratorCount(a); bValue = getAppSetGeneratorCount(b); break; - case 'created-at': - aValue = new Date(a.metadata?.creationTimestamp || 0).getTime(); - bValue = new Date(b.metadata?.creationTimestamp || 0).getTime(); - break; + case 'created-at': + aValue = new Date(a.metadata?.creationTimestamp || 0).getTime(); + bValue = new Date(b.metadata?.creationTimestamp || 0).getTime(); + break; default: return 0; } diff --git a/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx b/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx index 1e357ce9..bbc13f4a 100644 --- a/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx +++ b/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx @@ -3,7 +3,11 @@ import classNames from 'classnames'; import { OwnerReferences } from '@gitops/utils/components/OwnerReferences/owner-references'; import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; -import { kindForReference, useObjectModifyPermissions, getSelectorSearchURL } from '@gitops/utils/utils'; +import { + getSelectorSearchURL, + kindForReference, + useObjectModifyPermissions, +} from '@gitops/utils/utils'; import { K8sModel, K8sResourceKind, diff --git a/src/gitops/utils/components/Conditions/Conditions.tsx b/src/gitops/utils/components/Conditions/Conditions.tsx index 730868ae..0c5681ab 100644 --- a/src/gitops/utils/components/Conditions/Conditions.tsx +++ b/src/gitops/utils/components/Conditions/Conditions.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; + import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; import { CamelCaseWrap, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; -import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; -export const Conditions: React.FC = ({ - conditions -}) => { +export const Conditions: React.FC = ({ conditions }) => { const { t } = useGitOpsTranslation(); const getStatusLabel = (status: string) => { @@ -22,10 +21,7 @@ export const Conditions: React.FC = ({ return ( <> {conditions?.length ? ( - +
@@ -36,30 +32,28 @@ export const Conditions: React.FC = ({ - {conditions?.map?.( - (condition: any, i: number) => ( - ( + + + + - - - - - - ), - )} + + + + + + ))}
{t('public~Type')}
+ + + {getStatusLabel(condition.status)} + - - - - {getStatusLabel(condition.status)} - - - - - - {condition.message?.trim() || '-'} -
+ + + {condition.message?.trim() || '-'} +
) : ( diff --git a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx index e47f8d14..ea9cdb21 100644 --- a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx +++ b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx @@ -1,19 +1,21 @@ import * as React from 'react'; -import { - DescriptionList, - DescriptionListGroup, +import * as _ from 'lodash'; + +import { ResourceLink, Timestamp, useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; +import { useAnnotationsModal, useLabelsModal } from '@openshift-console/dynamic-plugin-sdk'; +import { + Badge, + DescriptionList, DescriptionListDescription, + DescriptionListGroup, DescriptionListTermHelpText, DescriptionListTermHelpTextButton, - LabelGroup, - Label, - Badge, - Popover + Label, + LabelGroup, + Popover, } from '@patternfly/react-core'; import { PencilAltIcon } from '@patternfly/react-icons'; -import { ResourceLink, Timestamp, useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; -import { useLabelsModal, useAnnotationsModal } from '@openshift-console/dynamic-plugin-sdk'; -import * as _ from 'lodash'; + import { getAppSetGeneratorCount } from '../../../utils/gitops'; interface ResourceDetailsAttributesProps { @@ -48,7 +50,7 @@ const ResourceDetailsAttributes: React.FC = ({ showAppProject = false, showRepository = false, }) => { - const launchLabelsModal = useLabelsModal(resource); + const launchLabelsModal = useLabelsModal(resource); const launchAnnotationsModal = useAnnotationsModal(resource); // Check if user has permission to update the resource @@ -73,17 +75,36 @@ const ResourceDetailsAttributes: React.FC = ({ {/* Name */} - Name
} + Name
} bodyContent={
- Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. + Name must be unique within a namespace. Is required when creating resources, + although some resources may allow a client to request the generation of an + appropriate name automatically. Name is primarily intended for creation + idempotence and configuration definition. Cannot be updated.
-
+
Application {'>'} metadata {'>'} name
@@ -94,25 +115,43 @@ const ResourceDetailsAttributes: React.FC = ({ - - {metadata.name} - + {metadata.name} {/* Namespace */} - Namespace
} + Namespace
} bodyContent={
- Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty. + Namespace defines the space within which each name must be unique. An empty + namespace is equivalent to the "default" namespace, but + "default" is the canonical representation. Not all objects are required + to be scoped to a namespace - the value of this field for those objects will be + empty.
- Must be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces + Must be a DNS_LABEL. Cannot be updated. More info:{' '} + + https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces +
-
+
Application {'>'} metadata {'>'} namespace
@@ -131,17 +170,34 @@ const ResourceDetailsAttributes: React.FC = ({ {/* Labels */} - Labels
} + Labels
} bodyContent={
- Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. + Map of string keys and values that can be used to organize and categorize (scope + and select) objects. May match selectors of replication controllers and services.
-
+
Application {'>'} metadata {'>'} labels
@@ -155,7 +211,15 @@ const ResourceDetailsAttributes: React.FC = ({
{canUpdate && ( - } bodyContent={
- Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. + Annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects.
-
+
Application {'>'} metadata {'>'} annotations
@@ -240,7 +324,9 @@ const ResourceDetailsAttributes: React.FC = ({
{canUpdate && ( - } bodyContent={
- CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + CreationTimestamp is a timestamp representing the server time when this object was + created. It is not guaranteed to be set in happens-before order across separate + operations. Clients may not set this value. It is represented in RFC3339 form and + is in UTC.
Populated by the system. Read-only. Null for lists.
-
+
Application {'>'} metadata {'>'} creationTimestamp
@@ -301,14 +408,26 @@ const ResourceDetailsAttributes: React.FC = ({ {showOwner && ( - Owner
} + Owner
} bodyContent={
- List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller. + List of objects depended by this object. If ALL objects in the list have been + deleted, this object will be garbage collected. If this object is managed by a + controller, then an entry in this list will point to this controller, with the + controller field set to true. There cannot be more than one managing controller.
-
+
Application {'>'} metadata {'>'} ownerReferences
@@ -321,9 +440,9 @@ const ResourceDetailsAttributes: React.FC = ({ {metadata.ownerReferences && metadata.ownerReferences.length > 0 ? ( - ) : ( 'No owner' @@ -336,14 +455,21 @@ const ResourceDetailsAttributes: React.FC = ({ {showStatus && ( - Status
} + Status
} bodyContent={
-
- Current status of the resource -
-
+
Current status of the resource
+
Application {'>'} status
@@ -355,7 +481,9 @@ const ResourceDetailsAttributes: React.FC = ({ - Healthy + + Healthy + )} @@ -364,14 +492,21 @@ const ResourceDetailsAttributes: React.FC = ({ {showGeneratedApps && ( - Generated Apps
} + Generated Apps
} bodyContent={
-
- Number of applications generated by this ApplicationSet -
-
+
Number of applications generated by this ApplicationSet
+
ApplicationSet {'>'} status {'>'} applications
@@ -383,7 +518,9 @@ const ResourceDetailsAttributes: React.FC = ({ - 3 applications + + 3 applications + )} @@ -392,14 +529,21 @@ const ResourceDetailsAttributes: React.FC = ({ {showGenerators && ( - Generators
} + Generators
} bodyContent={
-
- Number of generators configured in this ApplicationSet -
-
+
Number of generators configured in this ApplicationSet
+
ApplicationSet {'>'} spec {'>'} generators
@@ -411,7 +555,9 @@ const ResourceDetailsAttributes: React.FC = ({ - {totalGenerators} generator{totalGenerators !== 1 ? 's' : ''} + + {totalGenerators} generator{totalGenerators !== 1 ? 's' : ''} + )} @@ -420,14 +566,21 @@ const ResourceDetailsAttributes: React.FC = ({ {showAppProject && ( - App Project
} + App Project
} bodyContent={
-
- Argo CD project that this ApplicationSet belongs to -
-
+
Argo CD project that this ApplicationSet belongs to
+
ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} project
@@ -439,7 +592,10 @@ const ResourceDetailsAttributes: React.FC = ({ - AP default + + AP + {' '} + default )} @@ -448,14 +604,21 @@ const ResourceDetailsAttributes: React.FC = ({ {showRepository && ( - Repository
} + Repository
} bodyContent={
-
- Git repository URL where the ApplicationSet configuration is stored -
-
+
Git repository URL where the ApplicationSet configuration is stored
+
ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} source {'>'} repoURL
@@ -467,7 +630,11 @@ const ResourceDetailsAttributes: React.FC = ({ - + https://github.com/aal/309/argocd-test-nested.git From ae8036516482267391a18bab2fc93c7c81dcbb4e Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 8 Oct 2025 16:27:57 -0400 Subject: [PATCH 15/16] cleanup Signed-off-by: Atif Ali --- plugin-metadata.ts | 2 +- .../ApplicationSetDetailsPage.scss | 219 ------------------ .../application-details-title.scss | 14 -- .../components/appset/AppSetNavPage.tsx | 4 +- .../ApplicationSetDetailsPage.tsx | 2 +- src/gitops/components/appset/AppsTab.tsx | 16 +- src/gitops/components/appset/EventsTab.scss | 10 - src/gitops/components/appset/EventsTab.tsx | 17 -- src/gitops/components/appset/index.ts | 1 - 9 files changed, 10 insertions(+), 275 deletions(-) delete mode 100644 src/gitops/components/application/ApplicationSetDetailsPage.scss rename src/gitops/components/{application => appset}/ApplicationSetDetailsPage.tsx (86%) delete mode 100644 src/gitops/components/appset/EventsTab.scss delete mode 100644 src/gitops/components/appset/EventsTab.tsx diff --git a/plugin-metadata.ts b/plugin-metadata.ts index 8993c161..b29b56da 100644 --- a/plugin-metadata.ts +++ b/plugin-metadata.ts @@ -16,7 +16,7 @@ const metadata: ConsolePluginBuildMetadata = { ApplicationList: "./gitops/components/application/ApplicationListTab.tsx", ApplicationDetails: "./gitops/components/application/ApplicationNavPage.tsx", ApplicationSetList: "./gitops/components/application/ApplicationSetListTab.tsx", - ApplicationSetDetailsPage: "./gitops/components/application/ApplicationSetDetailsPage.tsx", + ApplicationSetDetailsPage: "./gitops/components/appset/ApplicationSetDetailsPage.tsx", yamlApplicationTemplates: "./gitops/components/application/templates/index.ts" } }; diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.scss b/src/gitops/components/application/ApplicationSetDetailsPage.scss deleted file mode 100644 index 825e82d6..00000000 --- a/src/gitops/components/application/ApplicationSetDetailsPage.scss +++ /dev/null @@ -1,219 +0,0 @@ -.application-set-details-page { - &__main-section { - // PatternFly page main section styles - } - - &__body { - // PatternFly flex layout styles - } - - &__pane-body { - // Console pane body styles - } - - &__grid { - // PatternFly grid styles - } - - &__grid-item { - // PatternFly grid item styles - } - - &__header { - margin-bottom: 24px; - padding-left: 24px; - padding-top: 24px; - - &-title { - font-size: 20px; - font-weight: 600; - margin-bottom: 16px; - } - } - - &__content { - padding-left: 24px; - } - - &__conditions { - margin-top: 32px; - - &-title { - font-weight: 700; - font-size: 24px; - margin-bottom: 20px; - margin-top: 8px; - } - - &-table { - width: 100%; - border-top: 1px solid #393F44; - margin-bottom: 0; - - &-header { - display: flex; - font-weight: 600; - font-size: 16px; - padding: 16px 0 8px 0; - - &-cell { - text-align: left; - - &--type { - flex: 2; - padding-left: 0; - } - - &--status { - flex: 1; - } - - &--updated { - flex: 2; - } - - &--reason { - flex: 2; - } - - &--message { - flex: 4; - } - } - } - - &-row { - display: flex; - font-size: 15px; - padding: 16px 0; - align-items: flex-start; - border-top: 1px solid #393F44; - - &:first-child { - border-top: none; - } - - &-cell { - text-align: left; - - &--type { - flex: 2; - padding-left: 0; - } - - &--status { - flex: 1; - } - - &--updated { - flex: 2; - display: flex; - align-items: center; - } - - &--reason { - flex: 2; - } - - &--message { - flex: 4; - } - } - } - } - } - - &__generators { - &-container { - display: flex; - flex-direction: column; - gap: 16px; - } - - &-item { - border: 1px solid #393F44; - border-radius: 8px; - padding: 16px; - background-color: #212427; - - &-header { - display: flex; - align-items: center; - margin-bottom: 12px; - - &-icon { - width: 24px; - height: 24px; - background-color: #73bcf7; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - margin-right: 8px; - font-size: 12px; - font-weight: bold; - color: #003a70; - } - - &-title { - font-weight: 600; - font-size: 16px; - } - } - - &-content { - display: flex; - flex-direction: column; - gap: 8px; - - &-row { - display: flex; - align-items: center; - - &-label { - font-weight: 500; - min-width: 80px; - color: #8a8d90; - } - - &-value { - color: #73bcf7; - text-decoration: underline; - cursor: pointer; - } - } - } - } - } - - &__yaml-editor { - &-header { - &-buttons { - // YAML editor header buttons styles - } - - &-shortcuts { - // YAML editor shortcuts styles - - &-link { - // YAML editor shortcuts link styles - } - } - } - - &-content { - background: #1e1e1e; - color: #d4d4d4; - font-family: monospace; - font-size: 14px; - border-radius: 4px; - padding: 0; - - pre { - margin: 0; - padding: 16px; - overflow: auto; - } - } - } -} diff --git a/src/gitops/components/application/application-details-title.scss b/src/gitops/components/application/application-details-title.scss index f3acee6f..9f26ff35 100644 --- a/src/gitops/components/application/application-details-title.scss +++ b/src/gitops/components/application/application-details-title.scss @@ -1,18 +1,4 @@ // Application Details Title Styles -.argocd-application-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: 4px; - background-color: #E9654B; - color: white; - font-size: 16px; - font-weight: bold; - margin-right: var(--pf-v6-global--spacer--md); -} - .co-resource-item__resource-name { font-size: var(--pf-v6-global--font-size--2xl); font-weight: var(--pf-v6-global--font-weight--bold); diff --git a/src/gitops/components/appset/AppSetNavPage.tsx b/src/gitops/components/appset/AppSetNavPage.tsx index 16af576b..4b4b6fd2 100644 --- a/src/gitops/components/appset/AppSetNavPage.tsx +++ b/src/gitops/components/appset/AppSetNavPage.tsx @@ -7,11 +7,11 @@ import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetA import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; import DetailsPageHeader from '../shared/DetailsPageHeader/DetailsPageHeader'; +import EventsTab from '../shared/EventsTab/EventsTab'; import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; import AppSetDetailsTab from './AppSetDetailsTab'; import AppsTab from './AppsTab'; -import ApplicationSetEventsTab from './EventsTab'; import GeneratorsTab from './GeneratorsTab'; type AppSetPageProps = { @@ -60,7 +60,7 @@ const AppSetNavPage: React.FC = ({ name, namespace, kind }) => { href: 'events', name: t('Events'), - component: ApplicationSetEventsTab, + component: EventsTab, }, ], [t], diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/appset/ApplicationSetDetailsPage.tsx similarity index 86% rename from src/gitops/components/application/ApplicationSetDetailsPage.tsx rename to src/gitops/components/appset/ApplicationSetDetailsPage.tsx index 44b718c8..33211f88 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/appset/ApplicationSetDetailsPage.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useParams } from 'react-router-dom-v5-compat'; -import AppSetNavPage from '../appset/AppSetNavPage'; +import AppSetNavPage from './AppSetNavPage'; const ApplicationSetDetailsPage: React.FC = () => { const { name, ns } = useParams<{ name: string; ns: string }>(); diff --git a/src/gitops/components/appset/AppsTab.tsx b/src/gitops/components/appset/AppsTab.tsx index 467d1226..db78c9f6 100644 --- a/src/gitops/components/appset/AppsTab.tsx +++ b/src/gitops/components/appset/AppsTab.tsx @@ -1,8 +1,6 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; -import { PageSection } from '@patternfly/react-core'; - import { ApplicationSetKind } from '../../models/ApplicationSetModel'; import ApplicationList from '../shared/ApplicationList'; @@ -17,14 +15,12 @@ const AppsTab: React.FC = ({ obj }) => { if (!obj || !namespace) return null; return ( - - - + ); }; diff --git a/src/gitops/components/appset/EventsTab.scss b/src/gitops/components/appset/EventsTab.scss deleted file mode 100644 index 142aef31..00000000 --- a/src/gitops/components/appset/EventsTab.scss +++ /dev/null @@ -1,10 +0,0 @@ -.application-set-details-page { - &__events-container { - display: flex; - flex-direction: column; - gap: 12px; - } -} - - - diff --git a/src/gitops/components/appset/EventsTab.tsx b/src/gitops/components/appset/EventsTab.tsx deleted file mode 100644 index 24848383..00000000 --- a/src/gitops/components/appset/EventsTab.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import { RouteComponentProps } from 'react-router'; - -import { ApplicationSetKind } from '../../models/ApplicationSetModel'; -import EventsTab from '../shared/EventsTab/EventsTab'; - -type EventsTabProps = RouteComponentProps<{ ns: string; name: string }> & { - obj?: ApplicationSetKind; -}; - -const ApplicationSetEventsTab: React.FC = ({ obj }) => { - if (!obj) return null; - - return ; -}; - -export default ApplicationSetEventsTab; diff --git a/src/gitops/components/appset/index.ts b/src/gitops/components/appset/index.ts index f0212259..e035b6c0 100644 --- a/src/gitops/components/appset/index.ts +++ b/src/gitops/components/appset/index.ts @@ -1,7 +1,6 @@ export { default as AppSetDetailsTab } from './AppSetDetailsTab'; export { default as AppSetNavPage } from './AppSetNavPage'; export { default as AppsTab } from './AppsTab'; -export { default as EventsTab } from './EventsTab'; export { default as Generators } from './Generators'; export { default as GeneratorsTab } from './GeneratorsTab'; From d1273e815faa1b7da77d95c6394da96e1700d831 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 16 Oct 2025 20:08:35 -0400 Subject: [PATCH 16/16] fix sorting order icons && filter disappears Signed-off-by: Atif Ali --- .../components/shared/ApplicationList.tsx | 12 +++- .../components/shared/ApplicationSetList.tsx | 67 +++++++++++-------- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index 84de42c3..ec4ef5e8 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -130,7 +130,7 @@ const ApplicationList: React.FC = ({ }, [applications, sortBy, direction]); // TODO: use alternate filter since it is deprecated. See DataTableView potentially - const [, filteredData, onFilterChange] = useListPageFilter(sortedApplications, filters); + const [data, filteredData, onFilterChange] = useListPageFilter(sortedApplications, filters); // Filter applications by project or appset before rendering rows const filteredByOwner = React.useMemo( @@ -152,6 +152,11 @@ const ApplicationList: React.FC = ({ }); }, [filteredByOwner, searchQuery]); const rows = useApplicationRowsDV(filteredBySearch, namespace); + + // Check if there are applications owned by this ApplicationSet initially (before search) + const hasOwnedApplications = React.useMemo(() => { + return sortedApplications.some(filterApp(project, appset)); + }, [sortedApplications, project, appset]); const empty = ( @@ -200,12 +205,13 @@ const ApplicationList: React.FC = ({ )} - {!hideNameLabelFilters && ( + {!hideNameLabelFilters && hasOwnedApplications && ( )} diff --git a/src/gitops/components/shared/ApplicationSetList.tsx b/src/gitops/components/shared/ApplicationSetList.tsx index 86cdab54..0a3d67f1 100644 --- a/src/gitops/components/shared/ApplicationSetList.tsx +++ b/src/gitops/components/shared/ApplicationSetList.tsx @@ -23,7 +23,7 @@ import { import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks'; import DataView, { DataViewState } from '@patternfly/react-data-view/dist/esm/DataView'; import { CubesIcon } from '@patternfly/react-icons'; -import { Tbody, Td, Tr } from '@patternfly/react-table'; +import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; import DevPreviewBadge from '../../../components/import/badges/DevPreviewBadge'; import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; @@ -132,20 +132,37 @@ const ApplicationSetList: React.FC = ({ const { t } = useTranslation('plugin__gitops-plugin'); + const initIndex: number = namespace ? 0 : 1; + const COLUMNS_KEYS_INDEXES = React.useMemo( + () => [ + { key: 'name', index: 0 }, + ...(!namespace ? [{ key: 'namespace', index: 1 }] : []), + { key: 'status', index: 1 + initIndex }, + { key: 'generated-apps', index: 2 + initIndex }, + { key: 'generators', index: 3 + initIndex }, + { key: 'created-at', index: 4 + initIndex }, + ], + [namespace, initIndex], + ); + const [searchParams, setSearchParams] = useSearchParams(); const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams }); + const sortByIndex = React.useMemo( + () => COLUMNS_KEYS_INDEXES.findIndex((item) => item.key === sortBy), + [COLUMNS_KEYS_INDEXES, sortBy], + ); // Get search query from URL parameters const searchQuery = searchParams.get('q') || ''; - const getSortParams = (columnId: string, columnIndex: number) => ({ + const getSortParams = (columnIndex: number): ThProps['sort'] => ({ sortBy: { - index: columnIndex, + index: sortByIndex, direction, - defaultDirection: 'asc' as const, + defaultDirection: 'asc', }, - onSort: (_event: any, index: number, dir: 'asc' | 'desc') => { - onSort(_event, columnId, dir); + onSort: (_event: any, index: number, dir) => { + onSort(_event, COLUMNS_KEYS_INDEXES[index].key, dir); }, columnIndex, }); @@ -179,6 +196,11 @@ const ApplicationSetList: React.FC = ({ const rows = useApplicationSetRowsDV(filteredBySearch, namespace, applications, appsLoaded); + // Check if there are ApplicationSets initially (before search) + const hasApplicationSets = React.useMemo(() => { + return sortedApplicationSets.length > 0; + }, [sortedApplicationSets]); + const empty = ( @@ -246,7 +268,7 @@ const ApplicationSetList: React.FC = ({ )} - {!hideNameLabelFilters && ( + {!hideNameLabelFilters && hasApplicationSets && ( { - const i: number = namespace ? 1 : 0; +const useColumnsDV = (namespace, getSortParams): DataViewTh[] => { + const i: number = namespace ? 0 : 1; const { t } = useTranslation('plugin__gitops-plugin'); const columns: DataViewTh[] = [ { - id: 'name', cell: t('plugin__gitops-plugin~Name'), props: { - key: 'name', 'aria-label': 'name', - className: 'pf-m-width-20', - sort: getSortParams('name', 0), + className: 'pf-m-width-25', + sort: getSortParams(0), }, }, ...(!namespace ? [ { - id: 'namespace', cell: 'Namespace', props: { - key: 'namespace', 'aria-label': 'namespace', className: 'pf-m-width-15', - sort: getSortParams('namespace', 1), + sort: getSortParams(1), }, }, ] : []), { - id: 'status', cell: 'Health Status', props: { - key: 'status', 'aria-label': 'health status', className: 'pf-m-width-15', - sort: getSortParams('status', 1 + i), + sort: getSortParams(1 + i), }, }, { - id: 'generated-apps', cell: 'Generated Apps', props: { - key: 'generated-apps', 'aria-label': 'generated apps', className: 'pf-m-width-15', - sort: getSortParams('generated-apps', 2 + i), + sort: getSortParams(2 + i), }, }, { - id: 'generators', cell: 'Generators', props: { - key: 'generators', 'aria-label': 'generators', className: 'pf-m-width-15', - sort: getSortParams('generators', 3 + i), + sort: getSortParams(3 + i), }, }, { - id: 'created-at', cell: 'Created At', props: { - key: 'created-at', 'aria-label': 'created at', className: 'pf-m-width-15', - sort: getSortParams('created-at', 4 + i), + sort: getSortParams(4 + i), }, }, { - id: 'actions', cell: '', props: { 'aria-label': 'actions' }, },