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'})}
+
+
+ )}
+
+ {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';
}