diff --git a/console-extensions.json b/console-extensions.json index 6d599193..c0fc0d8b 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -406,5 +406,49 @@ "$codeRef": "yamlApplicationTemplates.defaultApplicationSetYamlTemplate" } } + }, + { + "type": "console.page/resource/details", + "flags": { + "required": [ + "APPLICATIONSET" + ] + }, + "properties": { + "model": { + "group": "argoproj.io", + "kind": "ApplicationSet", + "version": "v1alpha1" + }, + "component": { + "$codeRef": "ApplicationSetDetailsPage" + } + } + }, + { + "type": "console.page/resource/search", + "properties": { + "model": { + "group": "argoproj.io", + "version": "v1alpha1", + "kind": "ApplicationSet" + }, + "component": { + "$codeRef": "ApplicationSetList" + } + } + }, + { + "type": "console.page/resource/search", + "properties": { + "model": { + "group": "argoproj.io", + "version": "v1alpha1", + "kind": "Application" + }, + "component": { + "$codeRef": "ApplicationList" + } + } } ] diff --git a/plugin-metadata.ts b/plugin-metadata.ts index 7bf50d72..b29b56da 100644 --- a/plugin-metadata.ts +++ b/plugin-metadata.ts @@ -16,6 +16,7 @@ const metadata: ConsolePluginBuildMetadata = { ApplicationList: "./gitops/components/application/ApplicationListTab.tsx", ApplicationDetails: "./gitops/components/application/ApplicationNavPage.tsx", ApplicationSetList: "./gitops/components/application/ApplicationSetListTab.tsx", + ApplicationSetDetailsPage: "./gitops/components/appset/ApplicationSetDetailsPage.tsx", yamlApplicationTemplates: "./gitops/components/application/templates/index.ts" } }; diff --git a/src/components/topology/console/PodsOverview.tsx b/src/components/topology/console/PodsOverview.tsx index 8d93e328..0a0f8040 100644 --- a/src/components/topology/console/PodsOverview.tsx +++ b/src/components/topology/console/PodsOverview.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import { Grid, GridItem } from '@patternfly/react-core'; import { Link } from 'react-router-dom-v5-compat'; import * as _ from 'lodash'; import { PodPhase, ResourceLink, StatusComponent } from '@openshift-console/dynamic-plugin-sdk'; import { ExtPodKind } from '@openshift-console/dynamic-plugin-sdk-internal/lib/extensions/console-types'; +import { Grid, GridItem } from '@patternfly/react-core'; import { PodTraffic } from './pod-traffic'; import { ContainerStatus } from './types'; diff --git a/src/components/topology/sidebar/DeploymentSideBarDetails.tsx b/src/components/topology/sidebar/DeploymentSideBarDetails.tsx index 53da22a8..699f3e8e 100644 --- a/src/components/topology/sidebar/DeploymentSideBarDetails.tsx +++ b/src/components/topology/sidebar/DeploymentSideBarDetails.tsx @@ -205,7 +205,6 @@ type DeploymentSideBarDetailsProps = { export const DeploymentSideBarDetails: React.FC = ({ rollout: d, - rolloutKind: rolloutKind, }) => { const { t } = useTranslation(); const model = getK8sModel(d); diff --git a/src/components/utils/gitops-utils.ts b/src/components/utils/gitops-utils.ts index 82a1c10d..a06ceaa6 100644 --- a/src/components/utils/gitops-utils.ts +++ b/src/components/utils/gitops-utils.ts @@ -41,8 +41,8 @@ export const getApplicationsListBaseURI = () => { export class RetryError extends Error {} export class TimeoutError extends Error { - constructor(url: any, ms: any, ...params: any[]) { - super(`Call to ${url} timed out after ${ms}ms.`); //å ...params); + constructor(url: any, ms: any) { + super(`Call to ${url} timed out after ${ms}ms.`); // Dumb hack to fix `instanceof TimeoutError` Object.setPrototypeOf(this, TimeoutError.prototype); } @@ -64,7 +64,7 @@ const getCSRFToken = () => .map((c) => c.slice(cookiePrefix.length)) .pop(); -export const validateStatus = async (response: Response, url, method, retry) => { +export const validateStatus = async (response: Response, url, method) => { console.log('VALIDATE STATUS - RESPONSE STATUS IS ' + response.status); console.log('VALIDATE STATUS - RESPONSE TEXT IS ' + response.text); console.log('VALIDATE STATUS - RESPONSE BODY IS ' + response.body); @@ -101,7 +101,6 @@ export const validateStatus = async (response: Response, url, method, retry) => // retry 409 conflict errors due to ClustResourceQuota / ResourceQuota // https://bugzilla.redhat.com/show_bug.cgi?id=1920699 if ( - retry && method === 'POST' && response.status === 409 && ['resourcequotas', 'clusterresourcequotas'].includes(json.details?.kind) @@ -129,7 +128,7 @@ export const validateStatus = async (response: Response, url, method, retry) => }); }; -export const coFetchInternal = async (url, options, timeout, retry) => { +export const coFetchInternal = async (url, options, timeout) => { const allOptions = _.defaultsDeep({}, initDefaults, options); if (allOptions.method !== 'GET') { allOptions.headers['X-CSRFToken'] = getCSRFToken(); diff --git a/src/gitops/components/appset/AppSetDetailsTab.scss b/src/gitops/components/appset/AppSetDetailsTab.scss new file mode 100644 index 00000000..72581e3e --- /dev/null +++ b/src/gitops/components/appset/AppSetDetailsTab.scss @@ -0,0 +1,130 @@ +.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; + } + } +} + +// Force dashed border styling for labels in ApplicationSet details +.pf-c-description-list__description .co-label-group { + border: 1px dashed var(--pf-v6-global--BorderColor--200) !important; + border-radius: var(--pf-v6-global--BorderRadius--sm) !important; + padding: var(--pf-v6-global--spacer--sm) !important; + min-height: var(--pf-v6-global--spacer--lg) !important; + display: flex !important; + align-items: center !important; + width: fit-content !important; + max-width: 100% !important; +} + diff --git a/src/gitops/components/appset/AppSetDetailsTab.tsx b/src/gitops/components/appset/AppSetDetailsTab.tsx new file mode 100644 index 00000000..9888c993 --- /dev/null +++ b/src/gitops/components/appset/AppSetDetailsTab.tsx @@ -0,0 +1,139 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; + +import { ResourceLink, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Badge, DescriptionList, Flex, FlexItem, PageSection, Title } from '@patternfly/react-core'; + +import { ApplicationKind, ApplicationModel } from '../../models/ApplicationModel'; +import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; +import HealthStatus from '../../Statuses/HealthStatus'; +import { Conditions } from '../../utils/components/Conditions/Conditions'; +import { getAppSetGeneratorCount, getAppSetStatus } from '../../utils/gitops'; +import BaseDetailsSummary, { + DetailsDescriptionGroup, +} from '../shared/BaseDetailsSummary/BaseDetailsSummary'; + +import './AppSetDetailsTab.scss'; + +type AppSetDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: ApplicationSetKind; +}; + +const AppSetDetailsTab: React.FC = ({ obj }) => { + const namespace = obj?.metadata?.namespace; + + // Get applications to count generated apps + const [applications] = useK8sWatchResource({ + groupVersionKind: { + group: ApplicationModel.apiGroup, + version: ApplicationModel.apiVersion, + kind: ApplicationModel.kind, + }, + namespace: namespace || obj?.metadata?.namespace, + isList: true, + }); + + if (!obj) return null; + + const status = obj.status || {}; + const spec = obj.spec || {}; + const totalGenerators = getAppSetGeneratorCount(obj); + const appSetStatus = getAppSetStatus(obj); + + // Count applications owned by this ApplicationSet + const generatedAppsCount = + applications?.filter((app) => + app.metadata?.ownerReferences?.some( + (owner) => owner.kind === obj.kind && owner.name === obj.metadata?.name, + ), + ).length || 0; + + return ( + <> + + + Argo CD ApplicationSet details + + + + + + + + + + + + + + + + + {generatedAppsCount} application{generatedAppsCount !== 1 ? 's' : ''} + + + + + + {totalGenerators} generator{totalGenerators !== 1 ? 's' : ''} + + + + + + + + {spec.template?.spec?.source?.repoURL && ( + + + {spec.template.spec.source.repoURL} + + + )} + + + + + + + + + Conditions + + + + + ); +}; + +export default AppSetDetailsTab; diff --git a/src/gitops/components/appset/AppSetNavPage.tsx b/src/gitops/components/appset/AppSetNavPage.tsx new file mode 100644 index 00000000..4b4b6fd2 --- /dev/null +++ b/src/gitops/components/appset/AppSetNavPage.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; + +import { HorizontalNav, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Bullseye, Spinner } from '@patternfly/react-core'; + +import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; +import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import DetailsPageHeader from '../shared/DetailsPageHeader/DetailsPageHeader'; +import EventsTab from '../shared/EventsTab/EventsTab'; +import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; + +import AppSetDetailsTab from './AppSetDetailsTab'; +import AppsTab from './AppsTab'; +import GeneratorsTab from './GeneratorsTab'; + +type AppSetPageProps = { + name: string; + namespace: string; + kind: string; +}; + +const AppSetNavPage: React.FC = ({ name, namespace, kind }) => { + const { t } = useGitOpsTranslation(); + const [appSet, loaded] = useK8sWatchResource({ + groupVersionKind: { + group: 'argoproj.io', + version: 'v1alpha1', + kind: 'ApplicationSet', + }, + kind, + name, + namespace, + }); + + const [actions] = useApplicationSetActionsProvider(appSet); + + const pages = React.useMemo( + () => [ + { + href: '', + name: t('Details'), + component: AppSetDetailsTab, + }, + { + href: 'yaml', + name: t('YAML'), + component: ResourceYAMLTab, + }, + { + href: 'generators', + name: t('Generators'), + component: GeneratorsTab, + }, + { + href: 'applications', + name: t('Applications'), + component: AppsTab, + }, + { + href: 'events', + name: t('Events'), + component: EventsTab, + }, + ], + [t], + ); + + return ( + <> + + {loaded ? ( +
+ +
+ ) : ( + + + + )} + + ); +}; + +export default AppSetNavPage; diff --git a/src/gitops/components/appset/ApplicationSetDetailsPage.tsx b/src/gitops/components/appset/ApplicationSetDetailsPage.tsx new file mode 100644 index 00000000..33211f88 --- /dev/null +++ b/src/gitops/components/appset/ApplicationSetDetailsPage.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; + +import AppSetNavPage from './AppSetNavPage'; + +const ApplicationSetDetailsPage: React.FC = () => { + const { name, ns } = useParams<{ name: string; ns: string }>(); + + return ; +}; + +export default ApplicationSetDetailsPage; diff --git a/src/gitops/components/appset/AppsTab.scss b/src/gitops/components/appset/AppsTab.scss new file mode 100644 index 00000000..95bbf403 --- /dev/null +++ b/src/gitops/components/appset/AppsTab.scss @@ -0,0 +1,8 @@ +.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..db78c9f6 --- /dev/null +++ b/src/gitops/components/appset/AppsTab.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; + +import { ApplicationSetKind } from '../../models/ApplicationSetModel'; +import ApplicationList from '../shared/ApplicationList'; + +import './AppsTab.scss'; + +type AppsTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: ApplicationSetKind; +}; + +const AppsTab: React.FC = ({ obj }) => { + const namespace = obj?.metadata?.namespace; + if (!obj || !namespace) return null; + + return ( + + ); +}; + +export default AppsTab; diff --git a/src/gitops/components/appset/Generators.tsx b/src/gitops/components/appset/Generators.tsx new file mode 100644 index 00000000..aaa154df --- /dev/null +++ b/src/gitops/components/appset/Generators.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; + +import ClusterGenerator from './generators/ClusterGenerator'; +import GenericGenerator from './generators/GenericGenerator'; +import GitGenerator from './generators/GitGenerator'; +import ListGenerator from './generators/ListGenerator'; +import MatrixGenerator from './generators/MatrixGenerator'; +import MergeGenerator from './generators/MergeGenerator'; +import UnionGenerator from './generators/UnionGenerator'; + +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..7ba21b3f --- /dev/null +++ b/src/gitops/components/appset/GeneratorsTab.scss @@ -0,0 +1,9 @@ +.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..03a16a6a --- /dev/null +++ b/src/gitops/components/appset/GeneratorsTab.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; + +import { PageSection } from '@patternfly/react-core'; + +import { ApplicationSetKind } from '../../models/ApplicationSetModel'; + +import Generators from './Generators'; + +import './GeneratorsTab.scss'; + +type GeneratorsTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: ApplicationSetKind; +}; + +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/generators/ClusterGenerator.tsx b/src/gitops/components/appset/generators/ClusterGenerator.tsx new file mode 100644 index 00000000..2e8f4b0b --- /dev/null +++ b/src/gitops/components/appset/generators/ClusterGenerator.tsx @@ -0,0 +1,47 @@ +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..b561a901 --- /dev/null +++ b/src/gitops/components/appset/generators/GeneratorView.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +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: React.FC = ({ title, icon, children }) => ( + + +
+ {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..48fb3927 --- /dev/null +++ b/src/gitops/components/appset/generators/Generators.scss @@ -0,0 +1,58 @@ +.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..bc52536b --- /dev/null +++ b/src/gitops/components/appset/generators/GenericGenerator.tsx @@ -0,0 +1,46 @@ +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..3aed2e33 --- /dev/null +++ b/src/gitops/components/appset/generators/GitGenerator.tsx @@ -0,0 +1,56 @@ +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..73fa6393 --- /dev/null +++ b/src/gitops/components/appset/generators/ListGenerator.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; + +import { + DataList, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + ExpandableSection, +} 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, expanded: boolean) => { + setIsExpanded(expanded); + }; + + 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..25140392 --- /dev/null +++ b/src/gitops/components/appset/generators/MatrixGenerator.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import { ThLargeIcon } from '@patternfly/react-icons'; + +import Generators from '../Generators'; + +import GeneratorView from './GeneratorView'; + +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..3ae91358 --- /dev/null +++ b/src/gitops/components/appset/generators/MergeGenerator.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; + +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, +} from '@patternfly/react-core'; +import { ObjectGroupIcon } from '@patternfly/react-icons'; + +import Generators from '../Generators'; + +import GeneratorView from './GeneratorView'; + +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..356ea0ff --- /dev/null +++ b/src/gitops/components/appset/generators/UnionGenerator.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import { ThIcon } from '@patternfly/react-icons'; + +import Generators from '../Generators'; + +import GeneratorView from './GeneratorView'; + +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..e035b6c0 --- /dev/null +++ b/src/gitops/components/appset/index.ts @@ -0,0 +1,14 @@ +export { default as AppSetDetailsTab } from './AppSetDetailsTab'; +export { default as AppSetNavPage } from './AppSetNavPage'; +export { default as AppsTab } from './AppsTab'; +export { default as Generators } from './Generators'; +export { default as GeneratorsTab } from './GeneratorsTab'; + +// Export generator components +export { default as ClusterGenerator } from './generators/ClusterGenerator'; +export { default as GenericGenerator } from './generators/GenericGenerator'; +export { default as GitGenerator } from './generators/GitGenerator'; +export { default as ListGenerator } from './generators/ListGenerator'; +export { default as MatrixGenerator } from './generators/MatrixGenerator'; +export { default as MergeGenerator } from './generators/MergeGenerator'; +export { default as UnionGenerator } from './generators/UnionGenerator'; diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index 4c994961..ec4ef5e8 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -15,7 +15,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, @@ -109,6 +108,10 @@ const ApplicationList: React.FC = ({ () => COLUMNS_KEYS_INDEXES.findIndex((item) => item.key === sortBy), [COLUMNS_KEYS_INDEXES, sortBy], ); + + // Get search query from URL parameters + const searchQuery = searchParams.get('q') || ''; + const getSortParams = (columnIndex: number): ThProps['sort'] => ({ sortBy: { index: sortByIndex, @@ -125,43 +128,67 @@ const ApplicationList: React.FC = ({ const sortedApplications = React.useMemo(() => { return sortData(applications, sortBy, direction); }, [applications, sortBy, direction]); + // TODO: use alternate filter since it is deprecated. See DataTableView potentially const [data, 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); + + // Filter by search query if present (after other filters) + const filteredBySearch = React.useMemo(() => { + if (!searchQuery) return filteredByOwner; + + return filteredByOwner.filter((app) => { + const labels = app.metadata?.labels || {}; + // Check if any label matches the search query + return Object.entries(labels).some(([key, value]) => { + const labelSelector = `${key}=${value}`; + return labelSelector.includes(searchQuery) || key.includes(searchQuery); + }); + }); + }, [filteredByOwner, searchQuery]); + const rows = useApplicationRowsDV(filteredBySearch, namespace); + + // Check if there are applications owned by this ApplicationSet initially (before search) + const hasOwnedApplications = React.useMemo(() => { + return sortedApplications.some(filterApp(project, appset)); + }, [sortedApplications, project, appset]); const empty = ( - + - There are no Argo CD Applications in {namespace ? 'this project' : 'all projects'}. + {searchQuery ? ( + <> + No Argo CD Applications match the label filter{' '} + "{searchQuery}". +
+ Try removing the filter or selecting a different label to see more applications. + + ) : ( + `There are no Argo CD Applications in ${ + namespace ? 'this project' : 'all projects' + }.` + )}
); - const error = loadError && ( - - - - - - - - ); let currentActiveState = null; if (loadError) { currentActiveState = DataViewState.error; - } else if (filteredByOwner.length === 0) { + } else if (filteredBySearch.length === 0) { currentActiveState = DataViewState.empty; } return ( @@ -178,20 +205,17 @@ const ApplicationList: React.FC = ({ )} - {!hideNameLabelFilters && ( + {!hideNameLabelFilters && hasOwnedApplications && ( )} - + diff --git a/src/gitops/components/shared/ApplicationSetList.tsx b/src/gitops/components/shared/ApplicationSetList.tsx index c0862df6..0a3d67f1 100644 --- a/src/gitops/components/shared/ApplicationSetList.tsx +++ b/src/gitops/components/shared/ApplicationSetList.tsx @@ -23,31 +23,28 @@ import { import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks'; import DataView, { DataViewState } from '@patternfly/react-data-view/dist/esm/DataView'; import { CubesIcon } from '@patternfly/react-icons'; -import { Tbody, Td, Tr } from '@patternfly/react-table'; +import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; +import DevPreviewBadge from '../../../components/import/badges/DevPreviewBadge'; import { useApplicationSetActionsProvider } from '../../hooks/useApplicationSetActionsProvider'; -import { ApplicationSetStatus } from '../../utils/constants'; import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; -import { getAppSetStatus, getAppSetGeneratorCount } from '../../utils/gitops'; import ActionsDropdown from '../../utils/components/ActionDropDown/ActionDropDown'; -import { modelToGroupVersionKind, modelToRef } from '../../utils/utils'; -import DevPreviewBadge from '../../../components/import/badges/DevPreviewBadge'; - // Import status icons for consistency with ApplicationList import { HealthDegradedIcon, HealthHealthyIcon, HealthUnknownIcon, } from '../../utils/components/Icons/Icons'; - - +import { ApplicationSetStatus } from '../../utils/constants'; +import { getAppSetGeneratorCount, getAppSetStatus } from '../../utils/gitops'; +import { modelToGroupVersionKind, modelToRef } from '../../utils/utils'; const formatCreationTimestamp = (timestamp: string): string => { if (!timestamp) return '-'; const date = new Date(timestamp); const now = new Date(); const diffInMinutes = (now.getTime() - date.getTime()) / (1000 * 60); - + if (diffInMinutes < 60) { return `${Math.floor(diffInMinutes)}m ago`; } else if (diffInMinutes < 60 * 24) { @@ -63,20 +60,24 @@ const formatCreationTimestamp = (timestamp: string): string => { }; // Helper function to get generated applications count -const getGeneratedAppsCount = (appSet: ApplicationSetKind, applications: any[], appsLoaded: boolean): number => { +const getGeneratedAppsCount = ( + appSet: ApplicationSetKind, + applications: any[], + appsLoaded: boolean, +): number => { if (!applications || !appsLoaded) return 0; - + return applications.filter((app: any) => { if (!app.metadata?.ownerReferences) return false; - return app.metadata.ownerReferences.some((owner: any) => - owner.kind === 'ApplicationSet' && owner.name === appSet.metadata.name + return app.metadata.ownerReferences.some( + (owner: any) => owner.kind === 'ApplicationSet' && owner.name === appSet.metadata.name, ); }).length; }; const ApplicationSetStatusFragment: React.FC<{ status: string }> = ({ status }) => { let targetIcon: React.ReactNode; - + switch (status) { case ApplicationSetStatus.HEALTHY: targetIcon = ; @@ -131,34 +132,101 @@ const ApplicationSetList: React.FC = ({ const { t } = useTranslation('plugin__gitops-plugin'); + const initIndex: number = namespace ? 0 : 1; + const COLUMNS_KEYS_INDEXES = React.useMemo( + () => [ + { key: 'name', index: 0 }, + ...(!namespace ? [{ key: 'namespace', index: 1 }] : []), + { key: 'status', index: 1 + initIndex }, + { key: 'generated-apps', index: 2 + initIndex }, + { key: 'generators', index: 3 + initIndex }, + { key: 'created-at', index: 4 + initIndex }, + ], + [namespace, initIndex], + ); + const [searchParams, setSearchParams] = useSearchParams(); const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams }); - const getSortParams = (columnId: string, columnIndex: number) => ({ + const sortByIndex = React.useMemo( + () => COLUMNS_KEYS_INDEXES.findIndex((item) => item.key === sortBy), + [COLUMNS_KEYS_INDEXES, sortBy], + ); + + // Get search query from URL parameters + const searchQuery = searchParams.get('q') || ''; + + const getSortParams = (columnIndex: number): ThProps['sort'] => ({ sortBy: { - index: columnIndex, + index: sortByIndex, direction, - defaultDirection: 'asc' as const, + defaultDirection: 'asc', }, - onSort: (_event: any, index: number, dir: 'asc' | 'desc') => { - onSort(_event, columnId, dir); + onSort: (_event: any, index: number, dir) => { + onSort(_event, COLUMNS_KEYS_INDEXES[index].key, dir); }, columnIndex, }); const columnsDV = useColumnsDV(namespace, getSortParams); const sortedApplicationSets = React.useMemo(() => { - return sortData(applicationSets as ApplicationSetKind[], sortBy, direction, applications, appsLoaded); + return sortData( + applicationSets as ApplicationSetKind[], + sortBy, + direction, + applications, + appsLoaded, + ); }, [applicationSets, sortBy, direction, applications, appsLoaded]); + const [data, filteredData, onFilterChange] = useListPageFilter(sortedApplicationSets, filters); - const rows = useApplicationSetRowsDV(filteredData, namespace, applications, appsLoaded); + + // Filter by search query if present (after other filters) + const filteredBySearch = React.useMemo(() => { + if (!searchQuery) return filteredData; + + return filteredData.filter((appSet) => { + const labels = appSet.metadata?.labels || {}; + // Check if any label matches the search query + return Object.entries(labels).some(([key, value]) => { + const labelSelector = `${key}=${value}`; + return labelSelector.includes(searchQuery) || key.includes(searchQuery); + }); + }); + }, [filteredData, searchQuery]); + + const rows = useApplicationSetRowsDV(filteredBySearch, namespace, applications, appsLoaded); + + // Check if there are ApplicationSets initially (before search) + const hasApplicationSets = React.useMemo(() => { + return sortedApplicationSets.length > 0; + }, [sortedApplicationSets]); const empty = ( - + - There are no Argo CD ApplicationSets in {namespace ? 'this project' : 'all projects'}. + {searchQuery ? ( + <> + No Argo CD ApplicationSets match the label filter{' '} + "{searchQuery}" + . +
+ Try removing the filter or selecting a different label to see more + ApplicationSets. + + ) : ( + `There are no Argo CD ApplicationSets in ${ + namespace ? 'this project' : 'all projects' + }.` + )}
@@ -182,21 +250,25 @@ const ApplicationSetList: React.FC = ({ let currentActiveState = null; if (loadError) { currentActiveState = DataViewState.error; - } else if (applicationSets.length === 0) { + } else if (filteredBySearch.length === 0) { currentActiveState = DataViewState.empty; } return (
{showTitle == undefined && ( - }> + } + hideFavoriteButton={false} + > Create ApplicationSet )} - {!hideNameLabelFilters && ( + {!hideNameLabelFilters && hasApplicationSets && ( = ({ a const [actions] = useApplicationSetActionsProvider(appSet); return (
- +
); }; -const useApplicationSetRowsDV = (applicationSetsList, namespace, applications, appsLoaded): DataViewTr[] => { +const useApplicationSetRowsDV = ( + applicationSetsList, + namespace, + applications, + appsLoaded, +): DataViewTr[] => { const rows: DataViewTr[] = []; applicationSetsList.forEach((appSet: ApplicationSetKind, index: number) => { rows.push([ @@ -258,24 +331,20 @@ const useApplicationSetRowsDV = (applicationSetsList, namespace, applications, a : []), { id: getAppSetStatus(appSet), - cell: ( -
- -
- ), + cell: , }, { id: 'generated-apps-' + index, - cell: getGeneratedAppsCount(appSet, applications, appsLoaded).toString(), + cell:
{getGeneratedAppsCount(appSet, applications, appsLoaded).toString()}
, }, { id: 'generators-' + index, - cell: getAppSetGeneratorCount(appSet).toString(), + cell:
{getAppSetGeneratorCount(appSet).toString()}
, + }, + { + id: 'created-at-' + index, + cell:
{formatCreationTimestamp(appSet.metadata.creationTimestamp)}
, }, - { - id: 'created-at-' + index, - cell: formatCreationTimestamp(appSet.metadata.creationTimestamp), - }, { id: 'actions-' + index, cell: , @@ -286,76 +355,63 @@ const useApplicationSetRowsDV = (applicationSetsList, namespace, applications, a return rows; }; -const useColumnsDV = (namespace, getSortParams) => { - const i: number = namespace ? 1 : 0; +const useColumnsDV = (namespace, getSortParams): DataViewTh[] => { + const i: number = namespace ? 0 : 1; const { t } = useTranslation('plugin__gitops-plugin'); const columns: DataViewTh[] = [ { - id: 'name', cell: t('plugin__gitops-plugin~Name'), props: { - key: 'name', 'aria-label': 'name', - className: 'pf-m-width-20', - sort: getSortParams('name', 0), + className: 'pf-m-width-25', + sort: getSortParams(0), }, }, ...(!namespace ? [ { - id: 'namespace', cell: 'Namespace', props: { - key: 'namespace', 'aria-label': 'namespace', - className: 'pf-m-width-12', - sort: getSortParams('namespace', 1), + className: 'pf-m-width-15', + sort: getSortParams(1), }, }, ] : []), { - id: 'status', cell: 'Health Status', props: { - key: 'status', 'aria-label': 'health status', - className: 'pf-m-width-12', - sort: getSortParams('status', 1 + i), + className: 'pf-m-width-15', + sort: getSortParams(1 + i), }, }, { - id: 'generated-apps', cell: 'Generated Apps', props: { - key: 'generated-apps', 'aria-label': 'generated apps', - className: 'pf-m-width-12', - sort: getSortParams('generated-apps', 2 + i), + className: 'pf-m-width-15', + sort: getSortParams(2 + i), }, }, { - id: 'generators', cell: 'Generators', props: { - key: 'generators', 'aria-label': 'generators', - className: 'pf-m-width-12', - sort: getSortParams('generators', 3 + i), + className: 'pf-m-width-15', + sort: getSortParams(3 + i), + }, + }, + { + cell: 'Created At', + props: { + 'aria-label': 'created at', + className: 'pf-m-width-15', + sort: getSortParams(4 + i), }, }, - { - id: 'created-at', - cell: 'Created At', - props: { - key: 'created-at', - 'aria-label': 'created at', - className: 'pf-m-width-15', - sort: getSortParams('created-at', 4 + i), - }, - }, { - id: 'actions', cell: '', props: { 'aria-label': 'actions' }, }, @@ -388,7 +444,7 @@ export const sortData = ( sortBy: string | undefined, direction: 'asc' | 'desc' | undefined, applications: any[] = [], - appsLoaded: boolean = false, + appsLoaded = false, ) => { if (!sortBy || !direction) return data; @@ -417,10 +473,10 @@ export const sortData = ( aValue = getAppSetGeneratorCount(a); bValue = getAppSetGeneratorCount(b); break; - case 'created-at': - aValue = new Date(a.metadata?.creationTimestamp || 0).getTime(); - bValue = new Date(b.metadata?.creationTimestamp || 0).getTime(); - break; + case 'created-at': + aValue = new Date(a.metadata?.creationTimestamp || 0).getTime(); + bValue = new Date(b.metadata?.creationTimestamp || 0).getTime(); + break; default: return 0; } diff --git a/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx b/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx index 544eb95e..bbc13f4a 100644 --- a/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx +++ b/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx @@ -3,7 +3,11 @@ import classNames from 'classnames'; import { OwnerReferences } from '@gitops/utils/components/OwnerReferences/owner-references'; import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; -import { kindForReference, useObjectModifyPermissions } from '@gitops/utils/utils'; +import { + getSelectorSearchURL, + kindForReference, + useObjectModifyPermissions, +} from '@gitops/utils/utils'; import { K8sModel, K8sResourceKind, @@ -58,7 +62,8 @@ type LabelProps = { }; const LabelL: React.SFC = ({ kind, name, value, expand }) => { - const href = `/search?kind=${kind}&q=${value ? encodeURIComponent(`${name}=${value}`) : name}`; + const selector = value ? `${name}=${value}` : name; + const href = getSelectorSearchURL('', kind, selector); const kindOf = `co-m-${kindForReference(kind.toLowerCase())}`; const klass = classNames(kindOf, { 'co-m-expand': expand }, 'co-label'); return ( diff --git a/src/gitops/components/shared/ResourceYAMLTab/ResourceYAMLTab.tsx b/src/gitops/components/shared/ResourceYAMLTab/ResourceYAMLTab.tsx index 4e5eab81..70e5b4fa 100644 --- a/src/gitops/components/shared/ResourceYAMLTab/ResourceYAMLTab.tsx +++ b/src/gitops/components/shared/ResourceYAMLTab/ResourceYAMLTab.tsx @@ -23,7 +23,7 @@ const ResourceYAMLTab: React.FC = ({ obj }) => { } > - +
); diff --git a/src/gitops/utils/components/Conditions/Conditions.tsx b/src/gitops/utils/components/Conditions/Conditions.tsx new file mode 100644 index 00000000..0c5681ab --- /dev/null +++ b/src/gitops/utils/components/Conditions/Conditions.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; + +import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; +import { CamelCaseWrap, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; + +export const Conditions: React.FC = ({ conditions }) => { + const { t } = useGitOpsTranslation(); + + const getStatusLabel = (status: string) => { + switch (status) { + case 'True': + return t('public~True'); + case 'False': + return t('public~False'); + default: + return status; + } + }; + + return ( + <> + {conditions?.length ? ( + + + + + + + + + + + + {conditions?.map?.((condition: any, i: number) => ( + + + + + + + + ))} + +
{t('public~Type')}{t('public~Status')}{t('public~Updated')}{t('public~Reason')}{t('public~Message')}
+ + + {getStatusLabel(condition.status)} + + + + + + {condition.message?.trim() || '-'} +
+ ) : ( +
+
{t('public~No conditions found')}
+
+ )} + + ); +}; +Conditions.displayName = 'Conditions'; + +export type ConditionsProps = { + conditions: any; + title?: string; + subTitle?: string; +}; + +export default Conditions; diff --git a/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx new file mode 100644 index 00000000..ea9cdb21 --- /dev/null +++ b/src/gitops/utils/components/ResourceDetails/ResourceDetailsAttributes.tsx @@ -0,0 +1,647 @@ +import * as React from 'react'; +import * as _ from 'lodash'; + +import { ResourceLink, Timestamp, useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; +import { useAnnotationsModal, useLabelsModal } from '@openshift-console/dynamic-plugin-sdk'; +import { + Badge, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTermHelpText, + DescriptionListTermHelpTextButton, + Label, + LabelGroup, + Popover, +} from '@patternfly/react-core'; +import { PencilAltIcon } from '@patternfly/react-icons'; + +import { getAppSetGeneratorCount } from '../../../utils/gitops'; + +interface ResourceDetailsAttributesProps { + metadata: { + name?: string; + namespace?: string; + labels?: Record; + annotations?: Record; + creationTimestamp?: string; + ownerReferences?: Array<{ + name: string; + kind: string; + apiVersion: string; + }>; + }; + resource: any; // The full resource object for modal hooks + showOwner?: boolean; + showStatus?: boolean; + showGeneratedApps?: boolean; + showGenerators?: boolean; + showAppProject?: boolean; + showRepository?: boolean; +} + +const ResourceDetailsAttributes: React.FC = ({ + metadata, + resource, + showOwner = true, + showStatus = false, + showGeneratedApps = false, + showGenerators = false, + showAppProject = false, + showRepository = false, +}) => { + const launchLabelsModal = useLabelsModal(resource); + const launchAnnotationsModal = useAnnotationsModal(resource); + + // Check if user has permission to update the resource + // This enables/disables the Labels and Annotations edit buttons based on user permissions + const [canUpdate] = useAccessReview({ + group: 'argoproj.io', + verb: 'patch', + resource: 'applicationsets', + name: metadata.name, + namespace: metadata.namespace, + }); + + // 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; + + return ( + + {/* Name */} + + + Name} + bodyContent={ +
+
+ Name must be unique within a namespace. Is required when creating resources, + although some resources may allow a client to request the generation of an + appropriate name automatically. Name is primarily intended for creation + idempotence and configuration definition. Cannot be updated. +
+ +
+ Application {'>'} metadata {'>'} name +
+
+ } + > + + Name + +
+
+ {metadata.name} +
+ + {/* Namespace */} + + + Namespace} + bodyContent={ +
+
+ Namespace defines the space within which each name must be unique. An empty + namespace is equivalent to the "default" namespace, but + "default" is the canonical representation. Not all objects are required + to be scoped to a namespace - the value of this field for those objects will be + empty. +
+
+ Must be a DNS_LABEL. Cannot be updated. More info:{' '} + + https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces + +
+
+ Application {'>'} metadata {'>'} namespace +
+
+ } + > + + Namespace + +
+
+ + + +
+ + {/* Labels */} + + + Labels} + bodyContent={ +
+
+ Map of string keys and values that can be used to organize and categorize (scope + and select) objects. May match selectors of replication controllers and services. +
+ +
+ Application {'>'} metadata {'>'} labels +
+
+ } + > + + Labels + +
+
+ +
+ {canUpdate && ( + + )} +
+ {_.isEmpty(labelItems) ? ( + No labels + ) : ( + + {Object.entries(labelItems).map(([key, value]) => ( + + ))} + + )} +
+
+
+
+ + {/* Annotations */} + + + Annotations} + bodyContent={ +
+
+ Annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. +
+ +
+ Application {'>'} metadata {'>'} annotations +
+
+ } + > + + Annotations + +
+
+ + + +
+ + {/* Created at */} + + + Created at} + bodyContent={ +
+
+ CreationTimestamp is a timestamp representing the server time when this object was + created. It is not guaranteed to be set in happens-before order across separate + operations. Clients may not set this value. It is represented in RFC3339 form and + is in UTC. +
+
+ Populated by the system. Read-only. Null for lists. +
+ +
+ Application {'>'} metadata {'>'} creationTimestamp +
+
+ } + > + + Created at + +
+
+ + + +
+ + {/* Owner - conditional */} + {showOwner && ( + + + Owner} + bodyContent={ +
+
+ List of objects depended by this object. If ALL objects in the list have been + deleted, this object will be garbage collected. If this object is managed by a + controller, then an entry in this list will point to this controller, with the + controller field set to true. There cannot be more than one managing controller. +
+
+ Application {'>'} metadata {'>'} ownerReferences +
+
+ } + > + + Owner + +
+
+ + {metadata.ownerReferences && metadata.ownerReferences.length > 0 ? ( + + ) : ( + 'No owner' + )} + +
+ )} + + {/* Status - conditional */} + {showStatus && ( + + + Status} + bodyContent={ +
+
Current status of the resource
+
+ Application {'>'} status +
+
+ } + > + + Status + +
+
+ + + Healthy + + +
+ )} + + {/* Generated Apps - conditional */} + {showGeneratedApps && ( + + + Generated Apps} + bodyContent={ +
+
Number of applications generated by this ApplicationSet
+
+ ApplicationSet {'>'} status {'>'} applications +
+
+ } + > + + Generated Apps + +
+
+ + + 3 applications + + +
+ )} + + {/* Generators - conditional */} + {showGenerators && ( + + + Generators} + bodyContent={ +
+
Number of generators configured in this ApplicationSet
+
+ ApplicationSet {'>'} spec {'>'} generators +
+
+ } + > + + Generators + +
+
+ + + {totalGenerators} generator{totalGenerators !== 1 ? 's' : ''} + + +
+ )} + + {/* App Project - conditional */} + {showAppProject && ( + + + App Project} + bodyContent={ +
+
Argo CD project that this ApplicationSet belongs to
+
+ ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} project +
+
+ } + > + + App Project + +
+
+ + + AP + {' '} + default + +
+ )} + + {/* Repository - conditional */} + {showRepository && ( + + + Repository} + bodyContent={ +
+
Git repository URL where the ApplicationSet configuration is stored
+
+ ApplicationSet {'>'} spec {'>'} template {'>'} spec {'>'} source {'>'} repoURL +
+
+ } + > + + Repository + +
+
+ + + https://github.com/aal/309/argocd-test-nested.git + + +
+ )} +
+ ); +}; + +export default ResourceDetailsAttributes;