diff --git a/ui/src/app/applications/components/application-conditions/application-conditions.tsx b/ui/src/app/applications/components/application-conditions/application-conditions.tsx index 317b135eef..9a0402ffdf 100644 --- a/ui/src/app/applications/components/application-conditions/application-conditions.tsx +++ b/ui/src/app/applications/components/application-conditions/application-conditions.tsx @@ -2,14 +2,24 @@ import * as React from 'react'; import {Timestamp} from '../../../shared/components'; import * as models from '../../../shared/models'; -import {getConditionCategory} from '../utils'; +import {getAppSetConditionCategory, getConditionCategory} from '../utils'; import './application-conditions.scss'; -export const ApplicationConditions = ({conditions}: {conditions: models.ApplicationCondition[]}) => { +interface ApplicationConditionsProps { + conditions: models.ApplicationCondition[]; + title?: string; +} + +interface ApplicationSetConditionsProps { + conditions: models.ApplicationSetCondition[]; + title?: string; +} + +export const ApplicationConditions = ({conditions, title = 'Application conditions'}: ApplicationConditionsProps) => { return (
-

Application conditions

+

{title}

{(conditions.length === 0 &&

Application is healthy

) || (
{conditions.map((condition, index) => ( @@ -30,3 +40,33 @@ export const ApplicationConditions = ({conditions}: {conditions: models.Applicat
); }; + +export const ApplicationSetConditions = ({conditions, title = 'ApplicationSet conditions'}: ApplicationSetConditionsProps) => { + return ( +
+

{title}

+ {(conditions.length === 0 &&

ApplicationSet is healthy

) || ( +
+ {conditions.map((condition, index) => ( +
+
+
+ {condition.type} + {condition.status && ({condition.status})} +
+
+ {condition.message} +
+
+ +
+
+
+ ))} +
+ )} +
+ ); +}; diff --git a/ui/src/app/applications/components/application-details/application-details.tsx b/ui/src/app/applications/components/application-details/application-details.tsx index 4f478d2ba1..084da93ab4 100644 --- a/ui/src/app/applications/components/application-details/application-details.tsx +++ b/ui/src/app/applications/components/application-details/application-details.tsx @@ -12,13 +12,15 @@ import {AppContext, Context, ContextApis} from '../../../shared/context'; import * as appModels from '../../../shared/models'; import {AppDetailsPreferences, AppsDetailsViewKey, AppsDetailsViewType, services} from '../../../shared/services'; -import {ApplicationConditions} from '../application-conditions/application-conditions'; +import {ApplicationConditions, ApplicationSetConditions} from '../application-conditions/application-conditions'; import {ApplicationDeploymentHistory} from '../application-deployment-history/application-deployment-history'; import {ApplicationOperationState} from '../application-operation-state/application-operation-state'; import {PodGroupType, PodView} from '../application-pod-view/pod-view'; import {ApplicationResourceTree, ResourceTreeNode} from '../application-resource-tree/application-resource-tree'; import {ApplicationStatusPanel} from '../application-status-panel/application-status-panel'; +import {ApplicationSetStatusPanel} from '../application-status-panel/appset-status-panel'; import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-panel'; +import {isApp} from '../utils'; import {ResourceDetails} from '../resource-details/resource-details'; import * as AppUtils from '../utils'; import {ApplicationResourceList} from './application-resource-list'; @@ -647,8 +649,9 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat }) ) }> - {({application, tree, pref}: {application: appModels.Application; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => { + {({application, tree, pref}: {application: appModels.AbstractApplication; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => { tree.nodes = tree.nodes || []; + const isApplication = isApp(application); const treeFilter = getTreeFilter(pref.resourceFilter); const setFilter = (items: string[]) => { appContext.navigation.goto('.', {resource: items.join(',')}, {replace: true}); @@ -656,20 +659,25 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat }; const clearFilter = () => setFilter([]); const refreshing = application.metadata.annotations && application.metadata.annotations[appModels.AnnotationRefreshKey]; - const appNodesByName = groupAppNodesByKey(application, tree); + const appNodesByName = isApplication ? groupAppNodesByKey(application as appModels.Application, tree) : new Map(); const selectedItem = (selectedNodeKey && appNodesByName.get(selectedNodeKey)) || null; const isAppSelected = selectedItem === application; const selectedNode = !isAppSelected && (selectedItem as appModels.ResourceNode); - const operationState = application.status.operationState; - const hydrateOperationState = application.status.sourceHydrator.currentOperation; - const conditions = application.status.conditions || []; + const operationState = isApplication ? (application as appModels.Application).status.operationState : undefined; + const hydrateOperationState = isApplication ? (application as appModels.Application).status.sourceHydrator?.currentOperation : undefined; + const conditions = application.status?.conditions || []; const syncResourceKey = new URLSearchParams(props.history.location.search).get('deploy'); const tab = new URLSearchParams(props.history.location.search).get('tab'); - const source = getAppDefaultSource(application); + const source = isApplication ? getAppDefaultSource(application as appModels.Application) : undefined; const showToolTip = pref?.userHelpTipMsgs.find(usrMsg => usrMsg.appName === application.metadata.name); const resourceNodes = (): any[] => { + if (!isApplication) { + // For ApplicationSets, use tree nodes directly + return tree.nodes.map(node => ({...node, orphaned: false})); + } + const app = application as appModels.Application; const statusByKey = new Map(); - application.status.resources.forEach(res => statusByKey.set(AppUtils.nodeKey(res), res)); + app.status.resources.forEach(res => statusByKey.set(AppUtils.nodeKey(res), res)); const resources = new Map(); tree.nodes .map(node => ({...node, orphaned: false})) @@ -705,6 +713,63 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat : [] })); }; + + // Helper to get ApplicationResourceTree props based on resource type + const getResourceTreeProps = () => { + const commonProps = { + nodeFilter: (node: ResourceTreeNode) => filterTreeNode(node, treeFilter), + selectedNodeFullName: selectedNodeKey, + showCompactNodes: pref.groupNodes, + userMsgs: pref.userHelpTipMsgs, + tree, + onClearFilter: clearFilter, + onGroupdNodeClick: (nodeIds: string[]) => openGroupNodeDetails(nodeIds), + zoom: pref.zoom, + appContext: {...appContext, apis: appContext} as unknown as AppContext, + nameDirection: state.truncateNameOnRight, + nameWrap: state.showFullNodeName, + filters: pref.resourceFilter, + setTreeFilterGraph: setFilterGraph, + updateUsrHelpTipMsgs: updateHelpTipState, + setShowCompactNodes, + setNodeExpansion: (node: string, isExpanded: boolean) => setNodeExpansion(node, isExpanded), + getNodeExpansion: (node: string) => getNodeExpansion(node) + }; + + if (isApplication) { + return { + ...commonProps, + onNodeClick: (fullName: string) => selectNode(fullName), + nodeMenu: (node: ResourceTreeNode) => + AppUtils.renderResourceMenu(node, application as appModels.Application, tree, appContext, appChanged.current, () => + getApplicationActionMenu(application as appModels.Application, false) + ), + app: application as appModels.Application, + showOrphanedResources: pref.orphanedResources, + useNetworkingHierarchy: pref.view === 'network', + podGroupCount: pref.podGroupCount + }; + } else { + return { + ...commonProps, + onNodeClick: (fullName: string) => { + // For ApplicationSets, navigate to Application details if clicking an Application node + const parts = fullName.split('/'); + const [group, kind, namespace, name] = parts; + if (group === 'argoproj.io' && kind === 'Application' && namespace && name) { + appContext.navigation.goto(`/applications/${namespace}/${name}`); + } else { + selectNode(fullName); + } + }, + app: application, + showOrphanedResources: false, + useNetworkingHierarchy: false, + podGroupCount: 0 + }; + } + }; + const {Tree, Pods, Network, List} = AppsDetailsViewKey; const zoomNum = (pref.zoom * 100).toFixed(0); const setZoom = (s: number) => { @@ -759,7 +824,7 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat }); setState(prevState => ({...prevState, collapsedNodes: collapsedNodesList})); } else { - const managedKeys = new Set(application.status.resources.map(AppUtils.nodeKey)); + const managedKeys = isApplication ? new Set((application as appModels.Application).status.resources.map(AppUtils.nodeKey)) : new Set(); nodes.forEach(node => { if (!((node.parentRefs || []).length === 0 || managedKeys.has(AppUtils.nodeKey(node)))) { node.parentRefs.forEach(parent => { @@ -784,9 +849,9 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat const activeStatusExt = state.statusExtensionsMap[selectedExtension]; const activeTopBarActionMenuExt = state.topBarActionMenuExtsMap[selectedExtension]; - if (state.extensionsMap[pref.view] != null) { + if (isApplication && state.extensionsMap[pref.view] != null) { const extension = state.extensionsMap[pref.view]; - if (!extension.shouldDisplay(application)) { + if (!extension.shouldDisplay(application as appModels.Application)) { appContext.navigation.goto('.', {view: Tree}); } } @@ -799,16 +864,27 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat topBarTitle={getPageTitle(pref.view)} toolbar={{ breadcrumbs: [ - {title: 'Applications', path: '/applications'}, + { + title: isApplication ? 'Applications' : 'ApplicationSets', + path: isApplication ? '/applications' : '/applicationsets' + }, {title: } ], actionMenu: { - items: [ - ...getApplicationActionMenu(application, true), - ...(state.topBarActionMenuExts - ?.filter(ext => ext.shouldDisplay?.(application)) - .map(ext => renderActionMenuItem(ext, tree, application, setExtensionPanelVisible)) || []) - ] + items: isApplication + ? [ + ...getApplicationActionMenu(application as appModels.Application, true), + ...(state.topBarActionMenuExts + ?.filter(ext => ext.shouldDisplay?.(application as appModels.Application)) + .map(ext => renderActionMenuItem(ext, tree, application as appModels.Application, setExtensionPanelVisible)) || []) + ] + : [ + { + title: 'AppSet Details', + iconClassName: 'fa fa-info-circle', + action: () => selectNode(appFullName) + } + ] }, tools: ( @@ -821,22 +897,26 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat services.viewPreferences.updatePreferences({appDetails: {...pref, view: Tree}}); }} /> - { - appContext.navigation.goto('.', {view: Pods}); - services.viewPreferences.updatePreferences({appDetails: {...pref, view: Pods}}); - }} - /> - { - appContext.navigation.goto('.', {view: Network}); - services.viewPreferences.updatePreferences({appDetails: {...pref, view: Network}}); - }} - /> + {isApplication && ( + <> + { + appContext.navigation.goto('.', {view: Pods}); + services.viewPreferences.updatePreferences({appDetails: {...pref, view: Pods}}); + }} + /> + { + appContext.navigation.goto('.', {view: Network}); + services.viewPreferences.updatePreferences({appDetails: {...pref, view: Network}}); + }} + /> + + )} - {state.extensions && + {isApplication && + state.extensions && (state.extensions || []) - .filter(ext => ext.shouldDisplay(application)) + .filter(ext => ext.shouldDisplay(application as appModels.Application)) .map(ext => (
- selectNode(appFullName, 0, 'diff')} - showOperation={() => setOperationStatusVisible(true)} - showHydrateOperation={() => setHydrateOperationStatusVisible(true)} - showConditions={() => setConditionsStatusVisible(true)} - showExtension={id => setExtensionPanelVisible(id)} - showMetadataInfo={revision => setState(prevState => ({...prevState, revision}))} - /> + {isApplication ? ( + selectNode(appFullName, 0, 'diff')} + showOperation={() => setOperationStatusVisible(true)} + showHydrateOperation={() => setHydrateOperationStatusVisible(true)} + showConditions={() => setConditionsStatusVisible(true)} + showExtension={id => setExtensionPanelVisible(id)} + showMetadataInfo={revision => setState(prevState => ({...prevState, revision}))} + /> + ) : ( + setConditionsStatusVisible(true)} + /> + )}
{refreshing &&

Refreshing

} @@ -950,52 +1038,26 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat
{zoomNum}%
- filterTreeNode(node, treeFilter)} - selectedNodeFullName={selectedNodeKey} - onNodeClick={fullName => selectNode(fullName)} - nodeMenu={node => - AppUtils.renderResourceMenu(node, application, tree, appContext, appChanged.current, () => - getApplicationActionMenu(application, false) - ) - } - showCompactNodes={pref.groupNodes} - userMsgs={pref.userHelpTipMsgs} - tree={tree} - app={application} - showOrphanedResources={pref.orphanedResources} - useNetworkingHierarchy={pref.view === 'network'} - onClearFilter={clearFilter} - onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)} - zoom={pref.zoom} - podGroupCount={pref.podGroupCount} - appContext={{...appContext, apis: appContext} as unknown as AppContext} - nameDirection={state.truncateNameOnRight} - nameWrap={state.showFullNodeName} - filters={pref.resourceFilter} - setTreeFilterGraph={setFilterGraph} - updateUsrHelpTipMsgs={updateHelpTipState} - setShowCompactNodes={setShowCompactNodes} - setNodeExpansion={(node, isExpanded) => setNodeExpansion(node, isExpanded)} - getNodeExpansion={node => getNodeExpansion(node)} - /> + )) || - (pref.view === 'pods' && ( + (isApplication && pref.view === 'pods' && ( selectNode(fullName)} nodeMenu={node => - AppUtils.renderResourceMenu(node, application, tree, appContext, appChanged.current, () => - getApplicationActionMenu(application, false) + AppUtils.renderResourceMenu(node, application as appModels.Application, tree, appContext, appChanged.current, () => + getApplicationActionMenu(application as appModels.Application, false) ) } - quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, appContext, appChanged.current)} + quickStarts={node => + AppUtils.renderResourceButtons(node, application as appModels.Application, tree, appContext, appChanged.current) + } /> )) || - (state.extensionsMap[pref.view] != null && ( - + (isApplication && state.extensionsMap[pref.view] != null && ( + )) || (
services.viewPreferences.getPreferences()}> @@ -1021,10 +1083,18 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat pref={pref} onNodeClick={fullName => selectNode(fullName)} resources={data} - nodeMenu={node => - AppUtils.renderResourceMenu(node, application, tree, appContext, appChanged.current, () => - getApplicationActionMenu(application, false) - ) + nodeMenu={ + isApplication + ? node => + AppUtils.renderResourceMenu( + node, + application as appModels.Application, + tree, + appContext, + appChanged.current, + () => getApplicationActionMenu(application as appModels.Application, false) + ) + : undefined } tree={tree} /> @@ -1040,92 +1110,128 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat )}
- 0} onClose={() => closeGroupedNodesPanel()}> -
- setState(prevState => ({...prevState, slidingPanelPage: page}))} - preferencesKey='grouped-nodes-details'> - {data => ( - selectNode(fullName)} - resources={data} - nodeMenu={node => - AppUtils.renderResourceMenu(node, application, tree, appContext, appChanged.current, () => - getApplicationActionMenu(application, false) - ) - } - tree={tree} - /> - )} - -
-
- selectNode('')}> - updateApp(app, query)} - selectedNode={selectedNode} - appCxt={{...appContext, apis: appContext} as unknown as AppContext} - tab={tab} - /> - - AppUtils.showDeploy(null, null, appContext)} - selectedResource={syncResourceKey} - /> - -1} onClose={() => setRollbackPanelVisible(-1)}> - {selectedRollbackDeploymentIndex > -1 && ( - rollbackApplication(info, application)} - selectDeployment={i => setRollbackPanelVisible(i)} + {isApplication && ( + 0} onClose={() => closeGroupedNodesPanel()}> +
+ setState(prevState => ({...prevState, slidingPanelPage: page}))} + preferencesKey='grouped-nodes-details'> + {data => ( + selectNode(fullName)} + resources={data} + nodeMenu={node => + AppUtils.renderResourceMenu( + node, + application as appModels.Application, + tree, + appContext, + appChanged.current, + () => getApplicationActionMenu(application as appModels.Application, false) + ) + } + tree={tree} + /> + )} + +
+
+ )} + {isApplication && ( + selectNode('')}> + updateApp(app, query)} + selectedNode={selectedNode} + appCxt={{...appContext, apis: appContext} as unknown as AppContext} + tab={tab} /> - )} - - setOperationStatusVisible(false)}> - {operationState && } - - setHydrateOperationStatusVisible(false)}> - {hydrateOperationState && } - - setConditionsStatusVisible(false)}> - {conditions && } - - setState(prevState => ({...prevState, revision: null}))}> - {state.revision === 'SYNC_STATUS_REVISION' && - (application.status.sync.revisions || application.status.sync.revision) && - getContent(application, source, application.status.sync.revisions, application.status.sync.revision)} - {state.revision === 'OPERATION_STATE_REVISION' && - (application.status.operationState.syncResult.revisions || application.status.operationState.syncResult.revision) && - getContent( - application, - source, - application.status.operationState.syncResult.revisions, - application.status.operationState.syncResult.revision + + )} + {isApplication && ( + AppUtils.showDeploy(null, null, appContext)} + selectedResource={syncResourceKey} + /> + )} + {isApplication && ( + -1} onClose={() => setRollbackPanelVisible(-1)}> + {selectedRollbackDeploymentIndex > -1 && ( + rollbackApplication(info, application as appModels.Application)} + selectDeployment={i => setRollbackPanelVisible(i)} + /> )} + + )} + {isApplication && ( + setOperationStatusVisible(false)}> + {operationState && } + + )} + {isApplication && ( + setHydrateOperationStatusVisible(false)}> + {hydrateOperationState && } + + )} + setConditionsStatusVisible(false)}> + {conditions && + (isApplication ? ( + + ) : ( + + ))} - setExtensionPanelVisible('')}> - {selectedExtension !== '' && activeStatusExt?.flyout && } - - setExtensionPanelVisible('')}> - {selectedExtension !== '' && activeTopBarActionMenuExt?.flyout && ( - - )} - + {isApplication && ( + setState(prevState => ({...prevState, revision: null}))}> + {state.revision === 'SYNC_STATUS_REVISION' && + ((application as appModels.Application).status.sync.revisions || (application as appModels.Application).status.sync.revision) && + getContent( + application as appModels.Application, + source, + (application as appModels.Application).status.sync.revisions, + (application as appModels.Application).status.sync.revision + )} + {state.revision === 'OPERATION_STATE_REVISION' && + ((application as appModels.Application).status.operationState.syncResult.revisions || + (application as appModels.Application).status.operationState.syncResult.revision) && + getContent( + application as appModels.Application, + source, + (application as appModels.Application).status.operationState.syncResult.revisions, + (application as appModels.Application).status.operationState.syncResult.revision + )} + + )} + {isApplication && ( + setExtensionPanelVisible('')}> + {selectedExtension !== '' && activeStatusExt?.flyout && ( + + )} + + )} + {isApplication && ( + setExtensionPanelVisible('')}> + {selectedExtension !== '' && activeTopBarActionMenuExt?.flyout && ( + + )} + + )}
); diff --git a/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx b/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx index 0d222c8b98..00fdd7a7ca 100644 --- a/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx +++ b/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx @@ -16,7 +16,9 @@ import { BASE_COLORS, ComparisonStatusIcon, getAppOverridesCount, + getAppSetHealthStatus, HealthStatusIcon, + isApp, isAppNode, isYoungerThanXMinutes, NodeId, @@ -50,7 +52,7 @@ export interface ResourceTreeNode extends models.ResourceNode { } export interface ApplicationResourceTreeProps { - app: models.Application; + app: models.AbstractApplication; tree: models.ApplicationTree; useNetworkingHierarchy: boolean; nodeFilter: (node: ResourceTreeNode) => boolean; @@ -244,7 +246,7 @@ export function compareNodes(first: ResourceTreeNode, second: ResourceTreeNode) ); } -function appNodeKey(app: models.Application) { +function appNodeKey(app: models.AbstractApplication) { return nodeKey({group: 'argoproj.io', kind: app.kind, name: app.metadata.name, namespace: app.metadata.namespace}); } @@ -407,7 +409,7 @@ function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: R } const appNode = isAppNode(node); const rootNode = !node.root; - const extLinks: string[] = props.app.status.summary.externalURLs; + const extLinks: string[] = isApp(props.app) ? (props.app as models.Application).status.summary.externalURLs : []; const podGroupChildren = childMap.get(treeNodeKey(node)); const nonPodChildren = podGroupChildren?.reduce((acc, child) => { if (child.kind !== 'Pod') { @@ -755,7 +757,7 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod } const appNode = isAppNode(node); const rootNode = !node.root; - const extLinks: string[] = props.app.status.summary.externalURLs; + const extLinks: string[] = isApp(props.app) ? (props.app as models.Application).status.summary.externalURLs : []; const childCount = nodesHavingChildren.get(node.uid); return (
version: '', // @ts-expect-error its not any children: [], - status: props.app.status.sync.status, - health: props.app.status.health, + status: isApp(props.app) ? (props.app as models.Application).status.sync.status : null, + health: isApp(props.app) ? (props.app as models.Application).status.health : {status: getAppSetHealthStatus(props.app as models.ApplicationSet), message: ''}, uid: props.app.kind + '-' + props.app.metadata.namespace + '-' + props.app.metadata.name, info: overridesCount > 0 @@ -934,19 +936,34 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => }; const statusByKey = new Map(); - props.app.status.resources.forEach(res => statusByKey.set(nodeKey(res), res)); + const appSetStatusByKey = new Map(); + if (isApp(props.app)) { + (props.app as models.Application).status.resources.forEach(res => statusByKey.set(nodeKey(res), res)); + } else if ((props.app as models.ApplicationSet).status?.resources) { + (props.app as models.ApplicationSet).status.resources.forEach(res => appSetStatusByKey.set(nodeKey(res), res)); + } const nodeByKey = new Map(); props.tree.nodes .map(node => ({...node, orphaned: false})) .concat(((props.showOrphanedResources && props.tree.orphanedNodes) || []).map(node => ({...node, orphaned: true}))) .forEach(node => { - const status = statusByKey.get(nodeKey(node)); const resourceNode: ResourceTreeNode = {...node}; - if (status) { - resourceNode.health = status.health; - resourceNode.status = status.status; - resourceNode.hook = status.hook; - resourceNode.requiresPruning = status.requiresPruning; + if (isApp(props.app)) { + const status = statusByKey.get(nodeKey(node)); + if (status) { + resourceNode.health = status.health; + resourceNode.status = status.status; + resourceNode.hook = status.hook; + resourceNode.requiresPruning = status.requiresPruning; + } + } else { + const status = appSetStatusByKey.get(nodeKey(node)); + if (status && status.health) { + resourceNode.health = { + status: status.health.status as models.HealthStatusCode, + message: '' + }; + } } nodeByKey.set(treeNodeKey(node), resourceNode); }); @@ -979,7 +996,7 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => } }, [podCount]); - function filterGraph(app: models.Application, filteredIndicatorParent: string, graphNodesFilter: dagre.graphlib.Graph, predicate: (node: ResourceTreeNode) => boolean) { + function filterGraph(app: models.AbstractApplication, filteredIndicatorParent: string, graphNodesFilter: dagre.graphlib.Graph, predicate: (node: ResourceTreeNode) => boolean) { const appKey = appNodeKey(app); let filtered = 0; graphNodesFilter.nodes().forEach(nodeId => { @@ -1118,8 +1135,12 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => } } else { // Tree view - const managedKeys = new Set(props.app.status.resources.map(nodeKey)); - const orphanedKeys = new Set(props.tree.orphanedNodes?.map(nodeKey)); + const managedKeys = isApp(props.app) + ? new Set((props.app as models.Application).status.resources.map(nodeKey)) + : (props.app as models.ApplicationSet).status?.resources + ? new Set((props.app as models.ApplicationSet).status.resources.map(nodeKey)) + : new Set(); + const orphanedKeys = isApp(props.app) ? new Set(props.tree.orphanedNodes?.map(nodeKey)) : new Set(); const orphans: ResourceTreeNode[] = []; let allChildNodes: ResourceTreeNode[] = []; nodesHavingChildren.set(appNode.uid, 1); diff --git a/ui/src/app/applications/components/application-status-panel/appset-status-panel.tsx b/ui/src/app/applications/components/application-status-panel/appset-status-panel.tsx new file mode 100644 index 0000000000..20e50b1bd2 --- /dev/null +++ b/ui/src/app/applications/components/application-status-panel/appset-status-panel.tsx @@ -0,0 +1,89 @@ +import {HelpIcon} from 'argo-ui'; +import * as React from 'react'; +import {ARGO_GRAY6_COLOR} from '../../../shared/components'; +import {Timestamp} from '../../../shared/components/timestamp'; +import * as models from '../../../shared/models'; +import {getAppSetConditionCategory, getAppSetHealthStatus, HealthStatusIcon} from '../utils'; + +import './application-status-panel.scss'; + +interface Props { + appSet: models.ApplicationSet; + showConditions?: () => any; +} + +interface SectionInfo { + title: string; + helpContent?: string; +} + +const sectionLabel = (info: SectionInfo) => ( + +); + +const getConditionCounts = (conditions: models.ApplicationSetCondition[]) => { + const counts = {info: 0, warning: 0, error: 0}; + if (!conditions) return counts; + + conditions.forEach(c => { + const category = getAppSetConditionCategory(c); + counts[category]++; + }); + return counts; +}; + +export const ApplicationSetStatusPanel = ({appSet, showConditions}: Props) => { + const healthStatus = getAppSetHealthStatus(appSet); + const conditions = appSet.status?.conditions || []; + const conditionCounts = getConditionCounts(conditions); + const latestCondition = conditions.length > 0 ? conditions[conditions.length - 1] : null; + + return ( +
+
+ {sectionLabel({title: 'APPSET HEALTH', helpContent: 'The health status of your ApplicationSet derived from its conditions'})} +
+ +   + {healthStatus} +
+ {latestCondition?.message &&
{latestCondition.message}
} +
+ + {conditions.length > 0 && ( +
+ {sectionLabel({title: 'CONDITIONS'})} +
showConditions && showConditions()}> + {conditionCounts.info > 0 && ( + + {conditionCounts.info} Info + + )} + {conditionCounts.error > 0 && ( + + {conditionCounts.error} Error + {conditionCounts.error !== 1 && 's'} + + )} +
+
+ )} + + {latestCondition?.lastTransitionTime && ( +
+ {sectionLabel({title: 'LAST UPDATED'})} +
+ +
+
+ )} +
+ ); +}; diff --git a/ui/src/app/applications/components/applications-list/appset-tile.tsx b/ui/src/app/applications/components/applications-list/appset-tile.tsx index d8e226b5e9..224d8e0b3e 100644 --- a/ui/src/app/applications/components/applications-list/appset-tile.tsx +++ b/ui/src/app/applications/components/applications-list/appset-tile.tsx @@ -6,6 +6,7 @@ import * as AppUtils from '../utils'; import {getApplicationLinkURL, getManagedByURL, getAppSetHealthStatus} from '../utils'; import {services} from '../../../shared/services'; import {ViewPreferences} from '../../../shared/services'; +import {ResourceIcon} from '../resource-icon'; export interface AppSetTileProps { appSet: models.ApplicationSet; @@ -50,7 +51,7 @@ export const AppSetTile = ({appSet, selected, pref, ctx, tileRef}: AppSetTilePro {/* Header row with icon, title, and action buttons */}
- + {AppUtils.appQualifiedName(appSet, useAuthSettingsCtx?.appsInAnyNamespaceEnabled)} diff --git a/ui/src/app/applications/components/resource-icon.tsx b/ui/src/app/applications/components/resource-icon.tsx index c3aba22691..8f1b1b64b0 100644 --- a/ui/src/app/applications/components/resource-icon.tsx +++ b/ui/src/app/applications/components/resource-icon.tsx @@ -10,6 +10,15 @@ export const ResourceIcon = ({group, kind, customStyle}: {group: string; kind: s if (kind === 'Application') { return ; } + if (kind === 'ApplicationSet') { + return ( + + {'{'} + + {'}'} + + ); + } // First, check for group-based custom icons if (group) { const matchedGroup = matchGroupToResource(group); diff --git a/ui/src/app/applications/components/utils.tsx b/ui/src/app/applications/components/utils.tsx index fe5d08fdc1..1ea0597737 100644 --- a/ui/src/app/applications/components/utils.tsx +++ b/ui/src/app/applications/components/utils.tsx @@ -1425,6 +1425,22 @@ export function getConditionCategory(condition: appModels.ApplicationCondition): } } +export function getAppSetConditionCategory(condition: appModels.ApplicationSetCondition): 'error' | 'warning' | 'info' { + const status = condition.status?.toLowerCase(); + const type = condition.type; + + // ErrorOccurred with status True = error + if (type === 'ErrorOccurred' && status === 'true') { + return 'error'; + } + // ParametersGenerated or ResourcesUpToDate with status False = error (indicates failure) + if ((type === 'ParametersGenerated' || type === 'ResourcesUpToDate') && status === 'false') { + return 'error'; + } + // Otherwise it's informational + return 'info'; +} + export function isAppNode(node: appModels.ResourceNode) { return node.kind === 'Application' && node.group === 'argoproj.io'; }