From a72977fe304fbd18ce9f2cb426f92ae85fbcbdf8 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 27 Aug 2025 17:57:24 -0400 Subject: [PATCH 01/24] 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 | 33 ++ 5 files changed, 437 insertions(+) create mode 100644 src/gitops/components/application/ApplicationDetailsTitle.tsx create mode 100644 src/gitops/components/application/ApplicationSetDetailsPage.tsx create mode 100644 src/gitops/components/application/application-details-title.scss diff --git a/console-extensions.json b/console-extensions.json index 38741aaf..ba79943a 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -388,5 +388,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 f73672b4..e143a506 100644 --- a/plugin-metadata.ts +++ b/plugin-metadata.ts @@ -15,6 +15,7 @@ const metadata: ConsolePluginBuildMetadata = { "topology": "./components/topology", ApplicationList: "./gitops/components/application/ApplicationListTab.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 new file mode 100644 index 00000000..1c8151e9 --- /dev/null +++ b/src/gitops/components/application/application-details-title.scss @@ -0,0 +1,33 @@ +// 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); + color: var(--pf-v6-global--Color--100); +} + +.co-actions { + display: flex; + align-items: center; + gap: var(--pf-v6-global--spacer--sm); +} + +// Breadcrumb styles +.pf-c-breadcrumb.co-breadcrumb { + padding: var(--pf-v6-global--spacer--md) var(--pf-v6-global--spacer--lg); + background-color: var(--pf-v6-global--BackgroundColor--100); + border-bottom: 1px solid var(--pf-v6-global--BorderColor--100); +} From 578ae9900575d5b3094248c61080edcd7e5005c1 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 27 Aug 2025 18:12:35 -0400 Subject: [PATCH 02/24] fix AS icon color and style Signed-off-by: Atif Ali --- .../application/ApplicationDetailsTitle.tsx | 4 ++-- .../application-details-title.scss | 23 ++++++++----------- 2 files changed, 11 insertions(+), 16 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} diff --git a/src/gitops/components/application/application-details-title.scss b/src/gitops/components/application/application-details-title.scss index 1c8151e9..57598334 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); @@ -31,3 +17,12 @@ background-color: var(--pf-v6-global--BackgroundColor--100); border-bottom: 1px solid var(--pf-v6-global--BorderColor--100); } + +// Ensure AS icon matches ApplicationSet list page color +.co-m-resource-icon { + &.co-m-resource-icon--lg { + // Force the ApplicationSet color from console-extensions.json + background-color: #E9654B !important; + color: white !important; + } +} From da2294562b0d187c8250e86cec92298cf5f39801 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/24] label styling Signed-off-by: Atif Ali <atali@redhat.com> --- .../application/ApplicationSetDetailsPage.tsx | 70 +++++++++++++------ .../application-details-title.scss | 45 ++++++++++++ 2 files changed, 94 insertions(+), 21 deletions(-) diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index b2d8746d..3e19405f 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -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 } 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,7 @@ const ApplicationSetDetailsPage: React.FC = () => { }); const [actions] = useApplicationSetActionsProvider(appSet); + const launchLabelsModal = useLabelsModal(appSet); if (loadError) return <div>Error loading ApplicationSet details.</div>; if (!loaded || !appSet) return <Spinner />; @@ -98,7 +101,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="green">NS</Badge> {metadata.namespace} + <ResourceLink kind="Namespace" name={metadata.namespace} /> </div> </div> </dd> @@ -108,28 +111,53 @@ const ApplicationSetDetailsPage: React.FC = () => { <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 className="pf-v6-l-split__item"> + <Button + variant="link" + icon={<PencilAltIcon />} + onClick={launchLabelsModal} + style={{ padding: 0 }} + aria-label="Edit labels" + > + Edit + </Button> + </div> </div> </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"> - {_.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> - )} - </div> + <div + style={{ + display: 'flex', + flexDirection: 'column', + background: 'var(--pf-v6-global--BackgroundColor--200, #212427)', + border: '1px solid var(--pf-v6-global--BorderColor--200, #383f45)', + borderRadius: 'var(--pf-v6-global--BorderRadius--sm, 3px)', + padding: '16px', + minHeight: '60px', + marginTop: '8px', + width: 'fit-content', + maxWidth: '100%', + }} + > + {_.isEmpty(labelItems) ? ( + <span className="text-muted">No labels</span> + ) : ( + <LabelGroup + style={{ + display: 'flex', + flexDirection: 'column', + gap: '8px', + margin: 0, + width: '100%', + }} + > + {Object.entries(labelItems).map(([key, value]) => ( + <Label key={key} color="grey"> + {key}={value} + </Label> + ))} + </LabelGroup> + )} </div> </dd> </div> diff --git a/src/gitops/components/application/application-details-title.scss b/src/gitops/components/application/application-details-title.scss index 57598334..9f26ff35 100644 --- a/src/gitops/components/application/application-details-title.scss +++ b/src/gitops/components/application/application-details-title.scss @@ -26,3 +26,48 @@ color: white !important; } } + +// Ensure NS badge is green +.pf-v6-c-badge { + &.pf-m-green { + background-color: var(--pf-v6-global--success-color--100) !important; + color: var(--pf-v6-global--Color--100) !important; + } +} + +// Labels container styling - ensure it's visible and vertical +.labels-container { + background-color: var(--pf-v6-global--BackgroundColor--200) !important; + border: 1px solid var(--pf-v6-global--BorderColor--200) !important; + border-radius: var(--pf-v6-global--BorderRadius--sm) !important; + padding: var(--pf-v6-global--spacer--md) !important; + min-height: 60px !important; + display: flex !important; + flex-direction: column !important; + gap: var(--pf-v6-global--spacer--sm) !important; + margin-top: var(--pf-v6-global--spacer--sm) !important; +} + +// Labels styling - force vertical layout +.labels-container .pf-v6-c-label-group { + display: flex !important; + flex-direction: column !important; + gap: var(--pf-v6-global--spacer--sm) !important; + margin: 0 !important; + width: 100% !important; +} + +.labels-container .pf-v6-c-label-group .pf-v6-c-label { + display: inline-flex !important; + align-items: center !important; + margin: 0 !important; + font-size: var(--pf-v6-global--font-size--sm) !important; + font-weight: var(--pf-v6-global--font-weight--normal) !important; + line-height: 1.2 !important; + padding: var(--pf-v6-global--spacer--xs) var(--pf-v6-global--spacer--sm) !important; + border-radius: var(--pf-v6-global--BorderRadius--sm) !important; + background-color: var(--pf-v6-global--BackgroundColor--300) !important; + color: var(--pf-v6-global--Color--100) !important; + border: 1px solid var(--pf-v6-global--BorderColor--300) !important; + width: fit-content !important; +} From cc9ca59c3b510bec4278822bf5b7b5a86edc0c09 Mon Sep 17 00:00:00 2001 From: Atif Ali <atali@redhat.com> Date: Wed, 27 Aug 2025 19:42:58 -0400 Subject: [PATCH 04/24] more label styling Signed-off-by: Atif Ali <atali@redhat.com> --- .../application/ApplicationSetDetailsPage.tsx | 137 ++++++++++-------- 1 file changed, 76 insertions(+), 61 deletions(-) diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index 3e19405f..6260629d 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -108,56 +108,70 @@ const ApplicationSetDetailsPage: React.FC = () => { </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 className="pf-v6-l-split__item"> - <Button - variant="link" - icon={<PencilAltIcon />} - onClick={launchLabelsModal} - style={{ padding: 0 }} - aria-label="Edit labels" - > - Edit - </Button> - </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 - style={{ - display: 'flex', - flexDirection: 'column', - background: 'var(--pf-v6-global--BackgroundColor--200, #212427)', - border: '1px solid var(--pf-v6-global--BorderColor--200, #383f45)', - borderRadius: 'var(--pf-v6-global--BorderRadius--sm, 3px)', - padding: '16px', - minHeight: '60px', - marginTop: '8px', - width: 'fit-content', - maxWidth: '100%', - }} - > - {_.isEmpty(labelItems) ? ( - <span className="text-muted">No labels</span> - ) : ( - <LabelGroup + <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%' }}> + <button + onClick={launchLabelsModal} style={{ - display: 'flex', - flexDirection: 'column', - gap: '8px', - margin: 0, - width: '100%', + padding: 0, + fontSize: 13, + fontWeight: 400, + color: '#fff', + background: 'none', + border: 'none', + display: 'inline-flex', + alignItems: 'center', + cursor: 'pointer', }} + className="co-resource-item__action-edit custom-edit-link" + aria-label="Edit labels" > - {Object.entries(labelItems).map(([key, value]) => ( - <Label key={key} color="grey"> - {key}={value} - </Label> - ))} - </LabelGroup> - )} + Edit <PencilAltIcon style={{ marginLeft: 4, fontSize: 13, color: '#fff' }} /> + </button> + <style>{` + .custom-edit-link:hover { + text-decoration: underline; + } + `}</style> + </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> + ) : ( + <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> @@ -165,6 +179,21 @@ const ApplicationSetDetailsPage: React.FC = () => { <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> @@ -209,25 +238,11 @@ const ApplicationSetDetailsPage: React.FC = () => { <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"> - <div className="pf-v6-l-split__item pf-m-fill">Generators</div> - </div> - </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"> - <Badge isRead color="blue">1 generators</Badge> - </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_AppProject"> <div className="pf-v6-l-split pf-v6-u-w-100"> <div className="pf-v6-l-split__item pf-m-fill">App Project</div> </div> </dt> + <div className="pf-v6-l-split__item pf-m-fill">Created at</div> <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"> From e57ff53fd482703711fdeda45ba2c7beb81f49a1 Mon Sep 17 00:00:00 2001 From: Atif Ali <atali@redhat.com> Date: Wed, 27 Aug 2025 21:37:06 -0400 Subject: [PATCH 05/24] add annotation && Generators && more formatting Signed-off-by: Atif Ali <atali@redhat.com> --- .../application/ApplicationSetDetailsPage.tsx | 150 +++++++++++------- 1 file changed, 96 insertions(+), 54 deletions(-) diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index 6260629d..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, @@ -21,7 +21,7 @@ import { PencilAltIcon } from '@patternfly/react-icons'; import * as _ from 'lodash'; import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; import ApplicationDetailsTitle from './ApplicationDetailsTitle'; -import { useLabelsModal } from '@openshift-console/dynamic-plugin-sdk'; +import { useLabelsModal, useAnnotationsModal } from '@openshift-console/dynamic-plugin-sdk'; import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; @@ -41,6 +41,7 @@ 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 />; @@ -53,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"> @@ -114,29 +118,23 @@ const ApplicationSetDetailsPage: React.FC = () => { <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%' }}> - <button - onClick={launchLabelsModal} + <a style={{ - padding: 0, fontSize: 13, - fontWeight: 400, - color: '#fff', - background: 'none', - border: 'none', + color: '#73bcf7', + textDecoration: 'underline', + cursor: 'pointer', display: 'inline-flex', alignItems: 'center', - cursor: 'pointer', }} - className="co-resource-item__action-edit custom-edit-link" + 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: '#fff' }} /> - </button> - <style>{` - .custom-edit-link:hover { - text-decoration: underline; - } - `}</style> + Edit <PencilAltIcon style={{ marginLeft: 4, fontSize: 13, color: '#73bcf7' }} /> + </a> </div> <div style={{ @@ -176,6 +174,37 @@ const ApplicationSetDetailsPage: React.FC = () => { </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"> @@ -236,17 +265,33 @@ 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"> + <div className="pf-v6-l-split__item pf-m-fill">Generators</div> + </div> + </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"> + <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"> <div className="pf-v6-l-split__item pf-m-fill">App Project</div> </div> </dt> - <div className="pf-v6-l-split__item pf-m-fill">Created at</div> <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> @@ -272,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 7ed27128944605cfe203e203d4bcb381877fa121 Mon Sep 17 00:00:00 2001 From: Atif Ali <atali@redhat.com> Date: Wed, 27 Aug 2025 21:55:13 -0400 Subject: [PATCH 06/24] 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 0e2d0c861c136e3912dc10fa6ff910dfe8743ce4 Mon Sep 17 00:00:00 2001 From: Atif Ali <atali@redhat.com> Date: Wed, 27 Aug 2025 22:11:53 -0400 Subject: [PATCH 07/24] add filter logic for generated apps to display on appset details page Signed-off-by: Atif Ali <atali@redhat.com> --- .../components/shared/ApplicationList.tsx | 70 +++++-------------- 1 file changed, 17 insertions(+), 53 deletions(-) diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index 0a3674d3..4aac609b 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -1,3 +1,10 @@ +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 } from '@patternfly/react-core'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom-v5-compat'; @@ -15,17 +22,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, Tr } from '@patternfly/react-table'; import { useApplicationActionsProvider } from '../..//hooks/useApplicationActionsProvider'; import RevisionFragment from '../..//Revision/Revision'; @@ -74,7 +70,7 @@ const ApplicationList: React.FC<ApplicationProps> = ({ hideNameLabelFilters, showTitle, }) => { - const [applications, loaded, loadError] = useK8sWatchResource<K8sResourceCommon[]>({ + const [applications, loaded] = useK8sWatchResource<K8sResourceCommon[]>({ isList: true, groupVersionKind: { group: 'argoproj.io', @@ -106,39 +102,10 @@ 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 rows = useApplicationRowsDV(filteredData, namespace); - const empty = ( - <Tbody> - <Tr key="loading" ouiaId="table-tr-loading"> - <Td colSpan={columnsDV.length}> - <EmptyState headingLevel="h4" icon={CubesIcon} titleText="No Argo CD Applications"> - <EmptyStateBody> - There are no Argo CD Applications in {namespace ? 'this project' : 'all projects'}. - </EmptyStateBody> - </EmptyState> - </Td> - </Tr> - </Tbody> - ); - const error = loadError && ( - <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." - /> - </Td> - </Tr> - </Tbody> - ); - let currentActiveState = null; - if (loadError) { - currentActiveState = DataViewState.error; - } else if (applications.length === 0) { - currentActiveState = DataViewState.empty; - } + 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]); + const rows = useApplicationRowsDV(filteredByOwner, namespace); return ( <div> {showTitle == undefined && (project == undefined || appset == undefined) && ( @@ -155,19 +122,16 @@ const ApplicationList: React.FC<ApplicationProps> = ({ <ListPageBody> {!hideNameLabelFilters && ( <ListPageFilter - data={data.filter(filterApp(project, appset))} + data={filteredByOwner} loaded={loaded} rowFilters={filters} onFilterChange={onFilterChange} /> )} - <DataView activeState={currentActiveState}> - <DataViewTable - rows={rows} - columns={columnsDV} - bodyStates={loadError ? { error } : { empty }} - /> - </DataView> + <DataViewTable + columns={columnsDV} + rows={rows} + /> </ListPageBody> </div> ); From 94cb0acc741f646ece74af855ea0b5171fe7ca5f Mon Sep 17 00:00:00 2001 From: Atif Ali <atali@redhat.com> Date: Thu, 28 Aug 2025 14:09:00 -0400 Subject: [PATCH 08/24] restore Applist Signed-off-by: Atif Ali <atali@redhat.com> --- .../components/shared/ApplicationList.tsx | 72 ++++++++++++++----- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index 4aac609b..3f617fb5 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -1,10 +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 } from '@patternfly/react-core'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom-v5-compat'; @@ -22,6 +15,17 @@ 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, Tr } from '@patternfly/react-table'; import { useApplicationActionsProvider } from '../..//hooks/useApplicationActionsProvider'; import RevisionFragment from '../..//Revision/Revision'; @@ -70,7 +74,7 @@ const ApplicationList: React.FC<ApplicationProps> = ({ hideNameLabelFilters, showTitle, }) => { - const [applications, loaded] = useK8sWatchResource<K8sResourceCommon[]>({ + const [applications, loaded, loadError] = useK8sWatchResource<K8sResourceCommon[]>({ isList: true, groupVersionKind: { group: 'argoproj.io', @@ -102,10 +106,39 @@ 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 [, 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]); - const rows = useApplicationRowsDV(filteredByOwner, namespace); + const [data, filteredData, onFilterChange] = useListPageFilter(sortedApplications, filters); + const rows = useApplicationRowsDV(filteredData, namespace); + const empty = ( + <Tbody> + <Tr key="loading" ouiaId="table-tr-loading"> + <Td colSpan={columnsDV.length}> + <EmptyState headingLevel="h4" icon={CubesIcon} titleText="No Argo CD Applications"> + <EmptyStateBody> + There are no Argo CD Applications in {namespace ? 'this project' : 'all projects'}. + </EmptyStateBody> + </EmptyState> + </Td> + </Tr> + </Tbody> + ); + const error = loadError && ( + <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." + /> + </Td> + </Tr> + </Tbody> + ); + let currentActiveState = null; + if (loadError) { + currentActiveState = DataViewState.error; + } else if (applications.length === 0) { + currentActiveState = DataViewState.empty; + } return ( <div> {showTitle == undefined && (project == undefined || appset == undefined) && ( @@ -122,16 +155,19 @@ const ApplicationList: React.FC<ApplicationProps> = ({ <ListPageBody> {!hideNameLabelFilters && ( <ListPageFilter - data={filteredByOwner} + data={data.filter(filterApp(project, appset))} loaded={loaded} rowFilters={filters} onFilterChange={onFilterChange} /> )} - <DataViewTable - columns={columnsDV} - rows={rows} - /> + <DataView activeState={currentActiveState}> + <DataViewTable + rows={rows} + columns={columnsDV} + bodyStates={loadError ? { error } : { empty }} + /> + </DataView> </ListPageBody> </div> ); @@ -418,4 +454,4 @@ export const filters: RowFilter[] = [ }, ]; -export default ApplicationList; +export default ApplicationList; \ No newline at end of file From b35833bb274271f927ec053421e87482b1403d03 Mon Sep 17 00:00:00 2001 From: Atif Ali <atali@redhat.com> Date: Thu, 28 Aug 2025 14:18:45 -0400 Subject: [PATCH 09/24] reapply filter logic keeping the empty state when no apps are available Signed-off-by: Atif Ali <atali@redhat.com> --- src/gitops/components/shared/ApplicationList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index 3f617fb5..2581eb08 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -107,7 +107,9 @@ const ApplicationList: React.FC<ApplicationProps> = ({ }, [applications, sortBy, direction]); // TODO: use alternate filter since it is deprecated. See DataTableView potentially const [data, filteredData, onFilterChange] = useListPageFilter(sortedApplications, filters); - const rows = useApplicationRowsDV(filteredData, namespace); + // 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); const empty = ( <Tbody> <Tr key="loading" ouiaId="table-tr-loading"> @@ -136,7 +138,7 @@ const ApplicationList: React.FC<ApplicationProps> = ({ let currentActiveState = null; if (loadError) { currentActiveState = DataViewState.error; - } else if (applications.length === 0) { + } else if (filteredByOwner.length === 0) { currentActiveState = DataViewState.empty; } return ( From 250b6cf552501c32f1d326c3d9fd998ef2c44a2f Mon Sep 17 00:00:00 2001 From: Atif Ali <atali@redhat.com> Date: Thu, 28 Aug 2025 14:51:33 -0400 Subject: [PATCH 10/24] rename ApplicationDetailsTitle and move it to where DetailsPageTitle Signed-off-by: Atif Ali <atali@redhat.com> --- .../application/ApplicationSetDetailsPage.tsx | 7 ++- .../ResourceDetailsTitle.tsx} | 49 +++++++++++-------- 2 files changed, 33 insertions(+), 23 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..4f949916 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -20,7 +20,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 +61,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 */} 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 2f2e70d13649fa4d7d62e00a029b84b3889976cf Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 28 Aug 2025 15:11:04 -0400 Subject: [PATCH 11/24] remove border on the pages Signed-off-by: Atif Ali --- .../application/ApplicationSetDetailsPage.tsx | 46 +++++++------------ 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index 4f949916..55bb7f09 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, @@ -81,11 +77,10 @@ const ApplicationSetDetailsPage: React.FC = () => {
- - - ApplicationSet details - - +
+

ApplicationSet details

+
+
@@ -352,8 +347,7 @@ const ApplicationSetDetailsPage: React.FC = () => {
)} -
-
+
@@ -386,14 +380,12 @@ const ApplicationSetDetailsPage: React.FC = () => { Generators} className="pf-v6-c-tab-content"> -
-
-
- - - Generators - - +
+
+
+

Generators

+
+
{appSet.spec?.generators?.map((generator: any, index: number) => { const generatorType = Object.keys(generator)[0]; @@ -518,11 +510,9 @@ const ApplicationSetDetailsPage: React.FC = () => {
)}
- - +
-
Applications} className="pf-v6-c-tab-content"> @@ -542,11 +532,10 @@ const ApplicationSetDetailsPage: React.FC = () => {
- - - Events - - +
+

Events

+
+
{status.conditions && status.conditions.length > 0 ? (
{status.conditions.map((condition: any, index: number) => ( @@ -623,8 +612,7 @@ const ApplicationSetDetailsPage: React.FC = () => {
)} -
-
+
From f2447e5ddf53599a6c3008ebef343d0a176536bf Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 28 Aug 2025 15:20:49 -0400 Subject: [PATCH 12/24] fix formatting Signed-off-by: Atif Ali --- .../application/ApplicationSetDetailsPage.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index 55bb7f09..02a5b138 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -77,10 +77,10 @@ const ApplicationSetDetailsPage: React.FC = () => {
-
-

ApplicationSet details

+
+

Argo CD ApplicationSet details

-
+
@@ -382,10 +382,10 @@ const ApplicationSetDetailsPage: React.FC = () => { Generators} className="pf-v6-c-tab-content">
-
+

Generators

-
+
{appSet.spec?.generators?.map((generator: any, index: number) => { const generatorType = Object.keys(generator)[0]; @@ -532,10 +532,10 @@ const ApplicationSetDetailsPage: React.FC = () => {
-
+

Events

-
+
{status.conditions && status.conditions.length > 0 ? (
{status.conditions.map((condition: any, index: number) => ( From 0d25b8595f3408d7da6cffe6da5f06f57f26f7b6 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 28 Aug 2025 15:36:55 -0400 Subject: [PATCH 13/24] pull common attributes to a common component Signed-off-by: Atif Ali --- .../application/ApplicationSetDetailsPage.tsx | 259 +-------------- .../ResourceDetailsAttributes.tsx | 310 ++++++++++++++++++ 2 files changed, 321 insertions(+), 248 deletions(-) create mode 100644 src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index 02a5b138..ed6acd53 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -5,22 +5,14 @@ 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'; const ApplicationSetDetailsPage: React.FC = () => { const { name, ns } = useParams<{ name: string; ns: string }>(); @@ -37,8 +29,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,11 +40,6 @@ 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 (
{

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 && ( diff --git a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx new file mode 100644 index 00000000..6159f599 --- /dev/null +++ b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx @@ -0,0 +1,310 @@ +import * as React from 'react'; +import { DescriptionList, LabelGroup, Label, Badge } from '@patternfly/react-core'; +import { PencilAltIcon } from '@patternfly/react-icons'; +import { ResourceLink, Timestamp } 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); + + const labelItems = metadata.labels || {}; + const annotationItems = metadata.annotations || {}; + const countAnnotations = Object.keys(annotationItems).length; + + return ( + + {/* Name */} +
+
+
+
Name
+
+
+
+
+
{metadata.name}
+
+
+
+ + {/* Namespace */} +
+
+
+
Namespace
+
+
+
+
+
+ +
+
+
+
+ + {/* Labels */} +
+
+ Labels +
+
+
+ +
+ {_.isEmpty(labelItems) ? ( + No labels + ) : ( + + {Object.entries(labelItems).map(([key, value]) => ( + + ))} + + )} +
+
+
+
+ + {/* Annotations */} + + + {/* Created at */} +
+
+
+
Created at
+
+
+
+
+
+ +
+
+
+
+ + {/* Owner - conditional */} + {showOwner && ( +
+
+
+
Owner
+
+
+
+
+
+ {metadata.ownerReferences && metadata.ownerReferences.length > 0 ? ( + + ) : ( + 'No owner' + )} +
+
+
+
+ )} + + {/* Status - conditional */} + {showStatus && ( +
+
+
+
Status
+
+
+
+
+
+ Healthy +
+
+
+
+ )} + + {/* Generated Apps - conditional */} + {showGeneratedApps && ( +
+
+
+
Generated Apps
+
+
+
+
+
+ 3 applications +
+
+
+
+ )} + + {/* Generators - conditional */} + {showGenerators && ( +
+
+
+
Generators
+
+
+
+
+
+ 1 generators +
+
+
+
+ )} + + {/* App Project - conditional */} + {showAppProject && ( +
+
+
+
App Project
+
+
+
+
+
+ AP default +
+
+
+
+ )} + + {/* Repository - conditional */} + {showRepository && ( +
+
+
+
Repository
+
+
+
+ +
+
+ )} +
+ ); +}; + +export default ResourceDetailsAttributes; From 6a0fdf9e594b79060ca077986f915b8c8fa38f13 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 28 Aug 2025 17:26:01 -0400 Subject: [PATCH 14/24] use pf Signed-off-by: Atif Ali --- .../ResourceDetailsAttributes.tsx | 448 ++++++++++++------ 1 file changed, 300 insertions(+), 148 deletions(-) diff --git a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx index 6159f599..f25a9dd7 100644 --- a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx +++ b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx @@ -1,5 +1,15 @@ import * as React from 'react'; -import { DescriptionList, LabelGroup, Label, Badge } from '@patternfly/react-core'; +import { + DescriptionList, + DescriptionListGroup, + DescriptionListDescription, + DescriptionListTermHelpText, + DescriptionListTermHelpTextButton, + LabelGroup, + Label, + Badge, + Popover +} from '@patternfly/react-core'; import { PencilAltIcon } from '@patternfly/react-icons'; import { ResourceLink, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; import { useLabelsModal, useAnnotationsModal } from '@openshift-console/dynamic-plugin-sdk'; @@ -47,41 +57,88 @@ const ResourceDetailsAttributes: React.FC = ({ return ( {/* Name */} -
-
-
-
Name
-
-
-
-
-
{metadata.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
-
-
-
-
-
- -
-
-
-
+ + + 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 -
-
+ + + 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 + + + + - -
+ + {/* Annotations */} -
-
- Annotations -
-
+ + + Annotations
} + bodyContent={ +
+ } + > + + Annotations + + + + - -
+ + - {/* Created at */} -
-
-
-
Created at
-
-
-
-
-
- + {/* 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
-
-
-
-
-
- {metadata.ownerReferences && metadata.ownerReferences.length > 0 ? ( - - ) : ( - 'No 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. +
+
+ Application {'>'} metadata {'>'} ownerReferences +
+
+ } + > + + Owner + + + + + {metadata.ownerReferences && metadata.ownerReferences.length > 0 ? ( + + ) : ( + 'No owner' + )} + + )} {/* Status - conditional */} {showStatus && ( -
-
-
-
Status
-
-
-
-
-
- Healthy -
-
-
-
+ + + Status
} + bodyContent={ +
+
+ Current status of the resource +
+
+ Application {'>'} status +
+
+ } + > + + Status + + + + + Healthy + + )} {/* Generated Apps - conditional */} {showGeneratedApps && ( -
-
-
-
Generated Apps
-
-
-
-
-
- 3 applications -
-
-
-
+ + + Generated Apps} + bodyContent={ +
+
+ Number of applications generated by this ApplicationSet +
+
+ ApplicationSet {'>'} status {'>'} applications +
+
+ } + > + + Generated Apps + +
+
+ + 3 applications + +
)} {/* Generators - conditional */} {showGenerators && ( -
-
-
-
Generators
-
-
-
-
-
- 1 generators -
-
-
-
+ + + Generators} + bodyContent={ +
+
+ Number of generators configured in this ApplicationSet +
+
+ ApplicationSet {'>'} spec {'>'} generators +
+
+ } + > + + Generators + +
+
+ + 1 generators + +
)} {/* App Project - conditional */} {showAppProject && ( -
-
-
-
App Project
-
-
-
-
-
- AP default -
-
-
-
+ + + App Project} + bodyContent={ +
+
+ Argo CD project that this ApplicationSet belongs to +
+
+ ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} project +
+
+ } + > + + App Project + +
+
+ + AP default + +
)} {/* Repository - conditional */} {showRepository && ( -
-
-
-
Repository
-
-
-
- -
-
+ + + 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 + + +
)} ); From 427ee35b6a4616c70d640f106eaed313e3594fd1 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 28 Aug 2025 17:34:38 -0400 Subject: [PATCH 15/24] Replaced inline styles with CSS classes Signed-off-by: Atif Ali --- .../ApplicationSetDetailsPage.scss | 219 ++++++++++++++++++ .../application/ApplicationSetDetailsPage.tsx | 151 ++++++------ 2 files changed, 284 insertions(+), 86 deletions(-) create mode 100644 src/gitops/components/application/ApplicationSetDetailsPage.scss 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 ed6acd53..832c36b0 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -13,6 +13,7 @@ import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetA import ResourceDetailsTitle from '../../utils/components/DetailsPageTitle/ResourceDetailsTitle'; 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 }>(); @@ -41,7 +42,7 @@ const ApplicationSetDetailsPage: React.FC = () => { }; return ( -
+
{ /> {/* Main Content */} -
+
{/* Tabs Section */} -
+
Details} className="pf-v6-c-tab-content"> -
-
-
-
-

Argo CD ApplicationSet details

+
+
+
+
+

Argo CD ApplicationSet details

-
+
{ {/* 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 && ( -
- )} ))}
@@ -117,10 +113,10 @@ const ApplicationSetDetailsPage: React.FC = () => { YAML} className="pf-v6-c-tab-content"> -
-
-
-
+
+
+
+
@@ -131,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)
)} @@ -207,10 +186,10 @@ const ApplicationSetDetailsPage: React.FC = () => { )} {generatorType === 'clusterDecisionResource' && ( -
+
{generatorData.configMapRef && ( -
- ConfigMap: +
+ ConfigMap: {generatorData.configMapRef.name}
)} @@ -218,7 +197,7 @@ const ApplicationSetDetailsPage: React.FC = () => { )} {generatorType === 'matrix' && ( -
+
Matrix generator with {Object.keys(generatorData).length} generators
@@ -226,10 +205,10 @@ const ApplicationSetDetailsPage: React.FC = () => { )} {generatorType === 'clusters' && ( -
+
{generatorData.selector && ( -
- Selector: +
+ Selector: {JSON.stringify(generatorData.selector)} @@ -279,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) => ( From 54982bc69667d7bdf1a8b3bdd55737a5ef66e03b Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 28 Aug 2025 18:12:42 -0400 Subject: [PATCH 16/24] enable or disable the edit buttons according to permission Signed-off-by: Atif Ali --- .../ResourceDetailsAttributes.tsx | 96 +++++++++++-------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx index f25a9dd7..334f3efd 100644 --- a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx +++ b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx @@ -11,7 +11,7 @@ import { Popover } from '@patternfly/react-core'; import { PencilAltIcon } from '@patternfly/react-icons'; -import { ResourceLink, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { ResourceLink, Timestamp, useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; import { useLabelsModal, useAnnotationsModal } from '@openshift-console/dynamic-plugin-sdk'; import * as _ from 'lodash'; @@ -47,9 +47,19 @@ 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 + // 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; @@ -140,25 +150,27 @@ const ResourceDetailsAttributes: React.FC = ({
- + {canUpdate && ( + + )}
= ({ From 9932d9f6cd218c898f58c6ee449dd3e950922e7d Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 28 Aug 2025 18:29:19 -0400 Subject: [PATCH 17/24] proper logic for matrix genertar Signed-off-by: Atif Ali --- .../application/ApplicationSetDetailsPage.tsx | 367 +++++++++++++++++- .../ResourceDetailsAttributes.tsx | 6 +- 2 files changed, 370 insertions(+), 3 deletions(-) diff --git a/src/gitops/components/application/ApplicationSetDetailsPage.tsx b/src/gitops/components/application/ApplicationSetDetailsPage.tsx index 832c36b0..1d00cd78 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -198,9 +198,122 @@ const ApplicationSetDetailsPage: React.FC = () => { {generatorType === 'matrix' && (
-
- Matrix generator with {Object.keys(generatorData).length} generators +
+ Matrix generator with {generatorData.generators?.length || 0} generators
+ {generatorData.generators?.map((subGenerator: any, subIndex: number) => { + const subGeneratorType = Object.keys(subGenerator)[0]; + const subGeneratorData = subGenerator[subGeneratorType]; + + return ( +
+
+
+ {subGeneratorType.charAt(0).toUpperCase()} +
+ {subGeneratorType} Generator +
+ + {/* Render sub-generator details */} + {subGeneratorType === 'git' && ( +
+ {subGeneratorData.repoURL && ( +
+ Repository: + + {subGeneratorData.repoURL} + +
+ )} + {subGeneratorData.revision && ( +
+ Revision: + + {subGeneratorData.revision} + +
+ )} + {subGeneratorData.directories && ( +
+ Directories: + + {subGeneratorData.directories.length} directory(ies) + +
+ )} +
+ )} + + {subGeneratorType === 'list' && ( +
+ {subGeneratorData.elements && ( +
+ Elements: + + {subGeneratorData.elements.length} element(s) + +
+ )} +
+ )} + + {subGeneratorType === 'clusters' && ( +
+ {subGeneratorData.selector && ( +
+ Selector: + + {JSON.stringify(subGeneratorData.selector)} + +
+ )} +
+ )} + + {subGeneratorType === 'clusterDecisionResource' && ( +
+ {subGeneratorData.configMapRef && ( +
+ ConfigMap: + + {subGeneratorData.configMapRef.name} + +
+ )} +
+ )} +
+ ); + })}
)} @@ -216,6 +329,256 @@ const ApplicationSetDetailsPage: React.FC = () => { )}
)} + + {generatorType === 'union' && ( +
+
+ Union generator with {generatorData.generators?.length || 0} generators +
+ {generatorData.generators?.map((subGenerator: any, subIndex: number) => { + const subGeneratorType = Object.keys(subGenerator)[0]; + const subGeneratorData = subGenerator[subGeneratorType]; + + return ( +
+
+
+ {subGeneratorType.charAt(0).toUpperCase()} +
+ {subGeneratorType} Generator +
+ + {/* Render sub-generator details (same as matrix) */} + {subGeneratorType === 'git' && ( +
+ {subGeneratorData.repoURL && ( +
+ Repository: + + {subGeneratorData.repoURL} + +
+ )} + {subGeneratorData.revision && ( +
+ Revision: + + {subGeneratorData.revision} + +
+ )} + {subGeneratorData.directories && ( +
+ Directories: + + {subGeneratorData.directories.length} directory(ies) + +
+ )} +
+ )} + + {subGeneratorType === 'list' && ( +
+ {subGeneratorData.elements && ( +
+ Elements: + + {subGeneratorData.elements.length} element(s) + +
+ )} +
+ )} + + {subGeneratorType === 'clusters' && ( +
+ {subGeneratorData.selector && ( +
+ Selector: + + {JSON.stringify(subGeneratorData.selector)} + +
+ )} +
+ )} + + {subGeneratorType === 'clusterDecisionResource' && ( +
+ {subGeneratorData.configMapRef && ( +
+ ConfigMap: + + {subGeneratorData.configMapRef.name} + +
+ )} +
+ )} +
+ ); + })} +
+ )} + + {generatorType === 'merge' && ( +
+
+ Merge generator with {generatorData.generators?.length || 0} generators +
+ {generatorData.mergeKeys && ( +
+ Merge Keys: + + {generatorData.mergeKeys.join(', ')} + +
+ )} + {generatorData.generators?.map((subGenerator: any, subIndex: number) => { + const subGeneratorType = Object.keys(subGenerator)[0]; + const subGeneratorData = subGenerator[subGeneratorType]; + + return ( +
+
+
+ {subGeneratorType.charAt(0).toUpperCase()} +
+ {subGeneratorType} Generator +
+ + {/* Render sub-generator details (same as matrix) */} + {subGeneratorType === 'git' && ( +
+ {subGeneratorData.repoURL && ( +
+ Repository: + + {subGeneratorData.repoURL} + +
+ )} + {subGeneratorData.revision && ( +
+ Revision: + + {subGeneratorData.revision} + +
+ )} + {subGeneratorData.directories && ( +
+ Directories: + + {subGeneratorData.directories.length} directory(ies) + +
+ )} +
+ )} + + {subGeneratorType === 'list' && ( +
+ {subGeneratorData.elements && ( +
+ Elements: + + {subGeneratorData.elements.length} element(s) + +
+ )} +
+ )} + + {subGeneratorType === 'clusters' && ( +
+ {subGeneratorData.selector && ( +
+ Selector: + + {JSON.stringify(subGeneratorData.selector)} + +
+ )} +
+ )} + + {subGeneratorType === 'clusterDecisionResource' && ( +
+ {subGeneratorData.configMapRef && ( +
+ ConfigMap: + + {subGeneratorData.configMapRef.name} + +
+ )} +
+ )} +
+ ); + })} +
+ )}
); })} 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 dadf8da954037039b4e82482a9c0a13f25a05878 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 29 Aug 2025 02:23:45 -0400 Subject: [PATCH 18/24] refactor and restructure code Signed-off-by: Atif Ali --- .../application/ApplicationSetDetailsPage.tsx | 727 +----------------- .../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 + 25 files changed, 1077 insertions(+), 721 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 1d00cd78..212577fe 100644 --- a/src/gitops/components/application/ApplicationSetDetailsPage.tsx +++ b/src/gitops/components/application/ApplicationSetDetailsPage.tsx @@ -1,731 +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 {generatorData.generators?.length || 0} generators -
- {generatorData.generators?.map((subGenerator: any, subIndex: number) => { - const subGeneratorType = Object.keys(subGenerator)[0]; - const subGeneratorData = subGenerator[subGeneratorType]; - - return ( -
-
-
- {subGeneratorType.charAt(0).toUpperCase()} -
- {subGeneratorType} Generator -
- - {/* Render sub-generator details */} - {subGeneratorType === 'git' && ( -
- {subGeneratorData.repoURL && ( -
- Repository: - - {subGeneratorData.repoURL} - -
- )} - {subGeneratorData.revision && ( -
- Revision: - - {subGeneratorData.revision} - -
- )} - {subGeneratorData.directories && ( -
- Directories: - - {subGeneratorData.directories.length} directory(ies) - -
- )} -
- )} - - {subGeneratorType === 'list' && ( -
- {subGeneratorData.elements && ( -
- Elements: - - {subGeneratorData.elements.length} element(s) - -
- )} -
- )} - - {subGeneratorType === 'clusters' && ( -
- {subGeneratorData.selector && ( -
- Selector: - - {JSON.stringify(subGeneratorData.selector)} - -
- )} -
- )} - - {subGeneratorType === 'clusterDecisionResource' && ( -
- {subGeneratorData.configMapRef && ( -
- ConfigMap: - - {subGeneratorData.configMapRef.name} - -
- )} -
- )} -
- ); - })} -
- )} - - {generatorType === 'clusters' && ( -
- {generatorData.selector && ( -
- Selector: - - {JSON.stringify(generatorData.selector)} - -
- )} -
- )} - - {generatorType === 'union' && ( -
-
- Union generator with {generatorData.generators?.length || 0} generators -
- {generatorData.generators?.map((subGenerator: any, subIndex: number) => { - const subGeneratorType = Object.keys(subGenerator)[0]; - const subGeneratorData = subGenerator[subGeneratorType]; - - return ( -
-
-
- {subGeneratorType.charAt(0).toUpperCase()} -
- {subGeneratorType} Generator -
- - {/* Render sub-generator details (same as matrix) */} - {subGeneratorType === 'git' && ( -
- {subGeneratorData.repoURL && ( -
- Repository: - - {subGeneratorData.repoURL} - -
- )} - {subGeneratorData.revision && ( -
- Revision: - - {subGeneratorData.revision} - -
- )} - {subGeneratorData.directories && ( -
- Directories: - - {subGeneratorData.directories.length} directory(ies) - -
- )} -
- )} - - {subGeneratorType === 'list' && ( -
- {subGeneratorData.elements && ( -
- Elements: - - {subGeneratorData.elements.length} element(s) - -
- )} -
- )} - - {subGeneratorType === 'clusters' && ( -
- {subGeneratorData.selector && ( -
- Selector: - - {JSON.stringify(subGeneratorData.selector)} - -
- )} -
- )} - - {subGeneratorType === 'clusterDecisionResource' && ( -
- {subGeneratorData.configMapRef && ( -
- ConfigMap: - - {subGeneratorData.configMapRef.name} - -
- )} -
- )} -
- ); - })} -
- )} - - {generatorType === 'merge' && ( -
-
- Merge generator with {generatorData.generators?.length || 0} generators -
- {generatorData.mergeKeys && ( -
- Merge Keys: - - {generatorData.mergeKeys.join(', ')} - -
- )} - {generatorData.generators?.map((subGenerator: any, subIndex: number) => { - const subGeneratorType = Object.keys(subGenerator)[0]; - const subGeneratorData = subGenerator[subGeneratorType]; - - return ( -
-
-
- {subGeneratorType.charAt(0).toUpperCase()} -
- {subGeneratorType} Generator -
- - {/* Render sub-generator details (same as matrix) */} - {subGeneratorType === 'git' && ( -
- {subGeneratorData.repoURL && ( -
- Repository: - - {subGeneratorData.repoURL} - -
- )} - {subGeneratorData.revision && ( -
- Revision: - - {subGeneratorData.revision} - -
- )} - {subGeneratorData.directories && ( -
- Directories: - - {subGeneratorData.directories.length} directory(ies) - -
- )} -
- )} - - {subGeneratorType === 'list' && ( -
- {subGeneratorData.elements && ( -
- Elements: - - {subGeneratorData.elements.length} element(s) - -
- )} -
- )} - - {subGeneratorType === 'clusters' && ( -
- {subGeneratorData.selector && ( -
- Selector: - - {JSON.stringify(subGeneratorData.selector)} - -
- )} -
- )} - - {subGeneratorType === 'clusterDecisionResource' && ( -
- {subGeneratorData.configMapRef && ( -
- ConfigMap: - - {subGeneratorData.configMapRef.name} - -
- )} -
- )} -
- ); - })} -
- )} -
- ); - })} - - {(!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'; From cf265032d27bba5859b49a07ee05c8aa37e2b2b7 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 29 Aug 2025 04:41:45 -0400 Subject: [PATCH 19/24] show YAML page Signed-off-by: Atif Ali --- .../components/appset/AppSetDetailsTab.tsx | 3 +- .../components/appset/AppSetNavPage.scss | 30 +++++++++ .../components/appset/AppSetNavPage.tsx | 13 ++-- 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 | 65 +++++++++---------- src/gitops/components/appset/YAMLTab.tsx | 8 ++- 8 files changed, 82 insertions(+), 46 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..85e30046 100644 --- a/src/gitops/components/appset/AppSetNavPage.scss +++ b/src/gitops/components/appset/AppSetNavPage.scss @@ -14,6 +14,36 @@ &__pane-body { flex: 1; padding: 20px; + /* Enable outer scrolling like native resource YAML pages */ overflow-y: auto; + + /* Make the YAML editor area tall and page-scrolling */ + .pf-v6-c-code-editor, + .pf-c-code-editor { + min-height: 70vh; + border: 0 !important; + box-shadow: none !important; + outline: none !important; + } + .pf-v6-c-code-editor__main, + .pf-c-code-editor__main { + min-height: 70vh; + 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-c-code-editor__main::before, + .pf-c-code-editor__main::after { + content: none !important; + display: none !important; + } + .monaco-editor, + .monaco-editor .overflow-guard { + min-height: 70vh; + height: 100% !important; + } } + } diff --git a/src/gitops/components/appset/AppSetNavPage.tsx b/src/gitops/components/appset/AppSetNavPage.tsx index 42eef4e5..2eaae665 100644 --- a/src/gitops/components/appset/AppSetNavPage.tsx +++ b/src/gitops/components/appset/AppSetNavPage.tsx @@ -25,7 +25,7 @@ type AppSetPageProps = { const AppSetNavPage: React.FC = ({ name, namespace, kind }) => { const [activeTabKey, setActiveTabKey] = React.useState(0); - + const [appSet, loaded, loadError] = useK8sWatchResource({ groupVersionKind: { group: 'argoproj.io', @@ -45,10 +45,15 @@ 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); }; + const isYamlTab = activeTabKey === 1; + return (
= ({ name, namespace, kind }) => name={name} namespace={namespace} actions={actions} - iconText="AS" + iconText={isYamlTab ? '' : 'AS'} iconTitle="Argo CD ApplicationSet" resourcePrefix="Argo CD" + showDevPreviewBadge={!isYamlTab} /> -
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..a249274f 100644 --- a/src/gitops/components/appset/YAMLTab.scss +++ b/src/gitops/components/appset/YAMLTab.scss @@ -1,51 +1,44 @@ .application-set-details-page { &__yaml-editor { + display: flex; + flex-direction: column; + height: 100%; 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; + /* Remove PF CodeEditor focus/bottom accent borders inside the YAML area */ + .pf-v6-c-code-editor, + .pf-c-code-editor { + border: 0 !important; + box-shadow: none !important; + outline: none !important; + } + .pf-v6-c-code-editor:focus-within, + .pf-c-code-editor:focus-within { + box-shadow: none !important; + outline: none !important; } } &__yaml-editor-content { - padding: 16px; - max-height: 600px; - overflow-y: auto; + flex: 1; + min-height: 0; + overflow: hidden; /* prevent page-level scroll */ - 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; + /* Force the editor to fill and scroll internally */ + .pf-v6-c-code-editor, + .pf-c-code-editor { + height: 100% !important; + } + .pf-v6-c-code-editor__main, + .pf-c-code-editor__main { + height: 100% !important; + } + .monaco-editor, + .monaco-editor .overflow-guard { + height: 100% !important; } } } diff --git a/src/gitops/components/appset/YAMLTab.tsx b/src/gitops/components/appset/YAMLTab.tsx index 42f2ace5..9e0a5a4d 100644 --- a/src/gitops/components/appset/YAMLTab.tsx +++ b/src/gitops/components/appset/YAMLTab.tsx @@ -1,8 +1,10 @@ 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 { RouteComponentProps } from 'react-router'; -type YAMLTabProps = { +type YAMLTabProps = RouteComponentProps<{ ns: string; name: string }> & { obj?: ApplicationSetKind; namespace?: string; name?: string; @@ -12,7 +14,9 @@ const YAMLTab: React.FC = ({ obj }) => { if (!obj) return null; return ( - + }> + + ); }; From 2602a6989f1f7cd2f2565263232ff9a484f70b2f Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 29 Aug 2025 05:00:56 -0400 Subject: [PATCH 20/24] show YAML page better Signed-off-by: Atif Ali --- .../components/appset/AppSetNavPage.tsx | 6 +- src/gitops/components/appset/YAMLTab.scss | 66 ++++++++----------- src/gitops/components/appset/YAMLTab.tsx | 12 ++-- 3 files changed, 37 insertions(+), 47 deletions(-) diff --git a/src/gitops/components/appset/AppSetNavPage.tsx b/src/gitops/components/appset/AppSetNavPage.tsx index 2eaae665..8fce8130 100644 --- a/src/gitops/components/appset/AppSetNavPage.tsx +++ b/src/gitops/components/appset/AppSetNavPage.tsx @@ -52,8 +52,6 @@ const AppSetNavPage: React.FC = ({ name, namespace, kind }) => setActiveTabKey(tabIndex); }; - const isYamlTab = activeTabKey === 1; - return (
= ({ name, namespace, kind }) => name={name} namespace={namespace} actions={actions} - iconText={isYamlTab ? '' : 'AS'} + iconText="AS" iconTitle="Argo CD ApplicationSet" resourcePrefix="Argo CD" - showDevPreviewBadge={!isYamlTab} + showDevPreviewBadge={true} />
diff --git a/src/gitops/components/appset/YAMLTab.scss b/src/gitops/components/appset/YAMLTab.scss index a249274f..c80b3ff3 100644 --- a/src/gitops/components/appset/YAMLTab.scss +++ b/src/gitops/components/appset/YAMLTab.scss @@ -1,44 +1,34 @@ -.application-set-details-page { - &__yaml-editor { - display: flex; - flex-direction: column; - height: 100%; - background: #1a1d21; - border: 1px solid #393F44; - border-radius: 8px; - overflow: hidden; +.yaml-tab-container { + // Remove height constraint to let ResourceYAMLEditor handle its own layout + min-height: 500px; - /* Remove PF CodeEditor focus/bottom accent borders inside the YAML area */ - .pf-v6-c-code-editor, - .pf-c-code-editor { - border: 0 !important; - box-shadow: none !important; - outline: none !important; - } - .pf-v6-c-code-editor:focus-within, - .pf-c-code-editor:focus-within { - box-shadow: none !important; - outline: none !important; - } + // Remove blue border from ResourceYAMLEditor + .pf-v6-c-code-editor, + .pf-c-code-editor { + border: 0 !important; + box-shadow: none !important; + outline: none !important; } - &__yaml-editor-content { - flex: 1; - min-height: 0; - overflow: hidden; /* prevent page-level scroll */ + .pf-v6-c-code-editor:focus-within, + .pf-c-code-editor:focus-within { + box-shadow: none !important; + outline: none !important; + } + + // Ensure the editor can expand properly + .pf-v6-c-code-editor, + .pf-c-code-editor { + min-height: 400px !important; + } + + .pf-v6-c-code-editor__main, + .pf-c-code-editor__main { + min-height: 400px !important; + } - /* Force the editor to fill and scroll internally */ - .pf-v6-c-code-editor, - .pf-c-code-editor { - height: 100% !important; - } - .pf-v6-c-code-editor__main, - .pf-c-code-editor__main { - height: 100% !important; - } - .monaco-editor, - .monaco-editor .overflow-guard { - height: 100% !important; - } + .monaco-editor, + .monaco-editor .overflow-guard { + min-height: 400px !important; } } diff --git a/src/gitops/components/appset/YAMLTab.tsx b/src/gitops/components/appset/YAMLTab.tsx index 9e0a5a4d..422e36d3 100644 --- a/src/gitops/components/appset/YAMLTab.tsx +++ b/src/gitops/components/appset/YAMLTab.tsx @@ -2,9 +2,9 @@ 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 { RouteComponentProps } from 'react-router'; +import './YAMLTab.scss'; -type YAMLTabProps = RouteComponentProps<{ ns: string; name: string }> & { +type YAMLTabProps = { obj?: ApplicationSetKind; namespace?: string; name?: string; @@ -14,9 +14,11 @@ const YAMLTab: React.FC = ({ obj }) => { if (!obj) return null; return ( - }> - - +
+ }> + + +
); }; From 6b53d02527f885b0ebe580fe52c7f0f0ef2b3fe6 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 29 Aug 2025 05:45:08 -0400 Subject: [PATCH 21/24] complete YAML page and cleanup Signed-off-by: Atif Ali --- .../components/appset/AppSetNavPage.scss | 15 ++--- src/gitops/components/appset/YAMLTab.scss | 63 +++++++++++++------ 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/gitops/components/appset/AppSetNavPage.scss b/src/gitops/components/appset/AppSetNavPage.scss index 85e30046..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,36 +14,33 @@ &__pane-body { flex: 1; padding: 20px; - /* Enable outer scrolling like native resource YAML pages */ - overflow-y: auto; + overflow-y: auto; // Restore scrolling for other tabs - /* Make the YAML editor area tall and page-scrolling */ + /* Remove min-height constraints that force outer scrolling */ .pf-v6-c-code-editor, .pf-c-code-editor { - min-height: 70vh; + height: 100% !important; border: 0 !important; box-shadow: none !important; outline: none !important; } .pf-v6-c-code-editor__main, .pf-c-code-editor__main { - min-height: 70vh; + 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-c-code-editor__main::before, + .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 { - min-height: 70vh; height: 100% !important; } } - } diff --git a/src/gitops/components/appset/YAMLTab.scss b/src/gitops/components/appset/YAMLTab.scss index c80b3ff3..5b90037b 100644 --- a/src/gitops/components/appset/YAMLTab.scss +++ b/src/gitops/components/appset/YAMLTab.scss @@ -1,13 +1,13 @@ .yaml-tab-container { - // Remove height constraint to let ResourceYAMLEditor handle its own layout - min-height: 500px; + // 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 - // Remove blue border from ResourceYAMLEditor - .pf-v6-c-code-editor, - .pf-c-code-editor { - border: 0 !important; - box-shadow: none !important; - outline: none !important; + // 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, @@ -16,19 +16,46 @@ outline: none !important; } - // Ensure the editor can expand properly - .pf-v6-c-code-editor, - .pf-c-code-editor { - min-height: 400px !important; + .monaco-editor .scrollbar .slider { + border: 0 !important; + } + + .monaco-editor .scrollbar .slider:hover { + border: 0 !important; } - .pf-v6-c-code-editor__main, - .pf-c-code-editor__main { - min-height: 400px !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, - .monaco-editor .overflow-guard { - min-height: 400px !important; + .monaco-editor .scrollbar .slider::before, + .monaco-editor .scrollbar .slider::after { + display: none !important; } } From b5a781653c7a92dcb23ca34b2c4b5c37b2cf2675 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 29 Aug 2025 06:02:08 -0400 Subject: [PATCH 22/24] implement edit appset Signed-off-by: Atif Ali --- src/gitops/components/appset/AppSetNavPage.tsx | 11 +++++++++++ src/gitops/hooks/useApplicationSetActionsProvider.tsx | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/gitops/components/appset/AppSetNavPage.tsx b/src/gitops/components/appset/AppSetNavPage.tsx index 8fce8130..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,6 +25,7 @@ type AppSetPageProps = { }; const AppSetNavPage: React.FC = ({ name, namespace, kind }) => { + const location = useLocation(); const [activeTabKey, setActiveTabKey] = React.useState(0); const [appSet, loaded, loadError] = useK8sWatchResource({ @@ -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 ( diff --git a/src/gitops/hooks/useApplicationSetActionsProvider.tsx b/src/gitops/hooks/useApplicationSetActionsProvider.tsx index 067db7bf..699f19df 100644 --- a/src/gitops/hooks/useApplicationSetActionsProvider.tsx +++ b/src/gitops/hooks/useApplicationSetActionsProvider.tsx @@ -64,8 +64,9 @@ export const useApplicationSetActionsProvider: UseApplicationSetActionsProvider namespace: applicationSet?.metadata?.namespace, }, cta: () => { + // Navigate to the same page with a query parameter to switch to YAML tab navigate( - `/k8s/ns/${applicationSet.metadata.namespace}/${applicationSetModelRef}/${applicationSet.metadata.name}/yaml`, + `/k8s/ns/${applicationSet.metadata.namespace}/${applicationSetModelRef}/${applicationSet.metadata.name}?tab=yaml`, ); }, }, From 3074a59fa6dd85cf48004a653403fb4cdb9be65a Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 29 Aug 2025 06:17:46 -0400 Subject: [PATCH 23/24] add fav icon and fix formatting Signed-off-by: Atif Ali --- .../components/appset/FavoriteButton.tsx | 162 ++++++++++++++++++ .../components/shared/ApplicationSetList.tsx | 6 +- .../DetailsPageTitle/ResourceDetailsTitle.tsx | 4 +- 3 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 src/gitops/components/appset/FavoriteButton.tsx 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/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 91873068d2e150b9f8738b66846edcc1c50b19d3 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 9 Sep 2025 17:22:36 -0400 Subject: [PATCH 24/24] more Details formating Signed-off-by: Atif Ali --- src/gitops/components/appset/AppsTab.scss | 2 ++ src/gitops/components/appset/EventsTab.scss | 3 +++ src/gitops/components/appset/Generators.tsx | 2 ++ src/gitops/components/appset/GeneratorsTab.scss | 2 ++ src/gitops/components/appset/README.md | 3 +++ src/gitops/components/appset/generators/Generators.scss | 3 +++ 6 files changed, 15 insertions(+) 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/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; } } + + +