mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
feat(ui): add appset tree-view details page (#26262)
Signed-off-by: Peter Jiang <peterjiang823@gmail.com> Signed-off-by: Peter Jiang <35584807+pjiang-dev@users.noreply.github.com> Co-authored-by: reggie-k <regina.voloshin@codefresh.io> Co-authored-by: Keith Chong <kykchong@redhat.com>
This commit is contained in:
@@ -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 (
|
||||
<div className='application-conditions'>
|
||||
<h4>Application conditions</h4>
|
||||
<h4>{title}</h4>
|
||||
{(conditions.length === 0 && <p>Application is healthy</p>) || (
|
||||
<div className='argo-table-list'>
|
||||
{conditions.map((condition, index) => (
|
||||
@@ -30,3 +40,33 @@ export const ApplicationConditions = ({conditions}: {conditions: models.Applicat
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ApplicationSetConditions = ({conditions, title = 'ApplicationSet conditions'}: ApplicationSetConditionsProps) => {
|
||||
return (
|
||||
<div className='application-conditions'>
|
||||
<h4>{title}</h4>
|
||||
{(conditions.length === 0 && <p>ApplicationSet is healthy</p>) || (
|
||||
<div className='argo-table-list'>
|
||||
{conditions.map((condition, index) => (
|
||||
<div
|
||||
className={`argo-table-list__row application-conditions__condition application-conditions__condition--${getAppSetConditionCategory(condition)}`}
|
||||
key={index}>
|
||||
<div className='row'>
|
||||
<div className='columns small-2'>
|
||||
{condition.type}
|
||||
{condition.status && <span className='application-conditions__status'> ({condition.status})</span>}
|
||||
</div>
|
||||
<div className='columns small-7' style={{whiteSpace: 'normal', lineHeight: 'normal'}}>
|
||||
{condition.message}
|
||||
</div>
|
||||
<div className='columns small-3'>
|
||||
<Timestamp date={condition.lastTransitionTime} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<string, models.ResourceStatus>();
|
||||
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<string, any>();
|
||||
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<string>();
|
||||
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: <ApplicationsDetailsAppDropdown appName={props.match.params.name} objectListKind={objectListKind} />}
|
||||
],
|
||||
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: (
|
||||
<React.Fragment key='app-list-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}});
|
||||
}}
|
||||
/>
|
||||
<i
|
||||
className={classNames('fa fa-th', {selected: pref.view === Pods})}
|
||||
title='Pods'
|
||||
onClick={() => {
|
||||
appContext.navigation.goto('.', {view: Pods});
|
||||
services.viewPreferences.updatePreferences({appDetails: {...pref, view: Pods}});
|
||||
}}
|
||||
/>
|
||||
<i
|
||||
className={classNames('fa fa-network-wired', {selected: pref.view === Network})}
|
||||
title='Network'
|
||||
onClick={() => {
|
||||
appContext.navigation.goto('.', {view: Network});
|
||||
services.viewPreferences.updatePreferences({appDetails: {...pref, view: Network}});
|
||||
}}
|
||||
/>
|
||||
{isApplication && (
|
||||
<>
|
||||
<i
|
||||
className={classNames('fa fa-th', {selected: pref.view === Pods})}
|
||||
title='Pods'
|
||||
onClick={() => {
|
||||
appContext.navigation.goto('.', {view: Pods});
|
||||
services.viewPreferences.updatePreferences({appDetails: {...pref, view: Pods}});
|
||||
}}
|
||||
/>
|
||||
<i
|
||||
className={classNames('fa fa-network-wired', {selected: pref.view === Network})}
|
||||
title='Network'
|
||||
onClick={() => {
|
||||
appContext.navigation.goto('.', {view: Network});
|
||||
services.viewPreferences.updatePreferences({appDetails: {...pref, view: Network}});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<i
|
||||
className={classNames('fa fa-th-list', {selected: pref.view === List})}
|
||||
title='List'
|
||||
@@ -845,9 +925,10 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat
|
||||
services.viewPreferences.updatePreferences({appDetails: {...pref, view: List}});
|
||||
}}
|
||||
/>
|
||||
{state.extensions &&
|
||||
{isApplication &&
|
||||
state.extensions &&
|
||||
(state.extensions || [])
|
||||
.filter(ext => ext.shouldDisplay(application))
|
||||
.filter(ext => ext.shouldDisplay(application as appModels.Application))
|
||||
.map(ext => (
|
||||
<i
|
||||
key={ext.title}
|
||||
@@ -865,15 +946,22 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat
|
||||
}}>
|
||||
<div className='application-details__wrapper'>
|
||||
<div className='application-details__status-panel'>
|
||||
<ApplicationStatusPanel
|
||||
application={application}
|
||||
showDiff={() => selectNode(appFullName, 0, 'diff')}
|
||||
showOperation={() => setOperationStatusVisible(true)}
|
||||
showHydrateOperation={() => setHydrateOperationStatusVisible(true)}
|
||||
showConditions={() => setConditionsStatusVisible(true)}
|
||||
showExtension={id => setExtensionPanelVisible(id)}
|
||||
showMetadataInfo={revision => setState(prevState => ({...prevState, revision}))}
|
||||
/>
|
||||
{isApplication ? (
|
||||
<ApplicationStatusPanel
|
||||
application={application as appModels.Application}
|
||||
showDiff={() => selectNode(appFullName, 0, 'diff')}
|
||||
showOperation={() => setOperationStatusVisible(true)}
|
||||
showHydrateOperation={() => setHydrateOperationStatusVisible(true)}
|
||||
showConditions={() => setConditionsStatusVisible(true)}
|
||||
showExtension={id => setExtensionPanelVisible(id)}
|
||||
showMetadataInfo={revision => setState(prevState => ({...prevState, revision}))}
|
||||
/>
|
||||
) : (
|
||||
<ApplicationSetStatusPanel
|
||||
appSet={application as appModels.ApplicationSet}
|
||||
showConditions={() => setConditionsStatusVisible(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='application-details__tree'>
|
||||
{refreshing && <p className='application-details__refreshing-label'>Refreshing</p>}
|
||||
@@ -950,52 +1038,26 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat
|
||||
<div className={`zoom-value`}>{zoomNum}%</div>
|
||||
</span>
|
||||
</div>
|
||||
<ApplicationResourceTree
|
||||
nodeFilter={node => 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)}
|
||||
/>
|
||||
<ApplicationResourceTree {...getResourceTreeProps()} />
|
||||
</>
|
||||
)) ||
|
||||
(pref.view === 'pods' && (
|
||||
(isApplication && pref.view === 'pods' && (
|
||||
<PodView
|
||||
tree={tree}
|
||||
app={application}
|
||||
app={application as appModels.Application}
|
||||
onItemClick={fullName => 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 && (
|
||||
<ExtensionView extension={state.extensionsMap[pref.view]} application={application} tree={tree} />
|
||||
(isApplication && state.extensionsMap[pref.view] != null && (
|
||||
<ExtensionView extension={state.extensionsMap[pref.view]} application={application as appModels.Application} tree={tree} />
|
||||
)) || (
|
||||
<div>
|
||||
<DataLoader load={() => 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
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SlidingPanel isShown={state.groupedResources.length > 0} onClose={() => closeGroupedNodesPanel()}>
|
||||
<div className='application-details__sliding-panel-pagination-wrap'>
|
||||
<Paginate
|
||||
page={state.slidingPanelPage}
|
||||
data={state.groupedResources}
|
||||
onPageChange={page => setState(prevState => ({...prevState, slidingPanelPage: page}))}
|
||||
preferencesKey='grouped-nodes-details'>
|
||||
{data => (
|
||||
<ApplicationResourceList
|
||||
pref={pref}
|
||||
onNodeClick={fullName => selectNode(fullName)}
|
||||
resources={data}
|
||||
nodeMenu={node =>
|
||||
AppUtils.renderResourceMenu(node, application, tree, appContext, appChanged.current, () =>
|
||||
getApplicationActionMenu(application, false)
|
||||
)
|
||||
}
|
||||
tree={tree}
|
||||
/>
|
||||
)}
|
||||
</Paginate>
|
||||
</div>
|
||||
</SlidingPanel>
|
||||
<SlidingPanel isShown={selectedNode != null || isAppSelected} onClose={() => selectNode('')}>
|
||||
<ResourceDetails
|
||||
tree={tree}
|
||||
application={application}
|
||||
isAppSelected={isAppSelected}
|
||||
updateApp={(app: models.Application, query: {validate?: boolean}) => updateApp(app, query)}
|
||||
selectedNode={selectedNode}
|
||||
appCxt={{...appContext, apis: appContext} as unknown as AppContext}
|
||||
tab={tab}
|
||||
/>
|
||||
</SlidingPanel>
|
||||
<ApplicationSyncPanel
|
||||
application={application}
|
||||
hide={() => AppUtils.showDeploy(null, null, appContext)}
|
||||
selectedResource={syncResourceKey}
|
||||
/>
|
||||
<SlidingPanel isShown={selectedRollbackDeploymentIndex > -1} onClose={() => setRollbackPanelVisible(-1)}>
|
||||
{selectedRollbackDeploymentIndex > -1 && (
|
||||
<ApplicationDeploymentHistory
|
||||
app={application}
|
||||
rollbackApp={info => rollbackApplication(info, application)}
|
||||
selectDeployment={i => setRollbackPanelVisible(i)}
|
||||
{isApplication && (
|
||||
<SlidingPanel isShown={state.groupedResources.length > 0} onClose={() => closeGroupedNodesPanel()}>
|
||||
<div className='application-details__sliding-panel-pagination-wrap'>
|
||||
<Paginate
|
||||
page={state.slidingPanelPage}
|
||||
data={state.groupedResources}
|
||||
onPageChange={page => setState(prevState => ({...prevState, slidingPanelPage: page}))}
|
||||
preferencesKey='grouped-nodes-details'>
|
||||
{data => (
|
||||
<ApplicationResourceList
|
||||
pref={pref}
|
||||
onNodeClick={fullName => 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}
|
||||
/>
|
||||
)}
|
||||
</Paginate>
|
||||
</div>
|
||||
</SlidingPanel>
|
||||
)}
|
||||
{isApplication && (
|
||||
<SlidingPanel isShown={selectedNode != null || isAppSelected} onClose={() => selectNode('')}>
|
||||
<ResourceDetails
|
||||
tree={tree}
|
||||
application={application as appModels.Application}
|
||||
isAppSelected={isAppSelected}
|
||||
updateApp={(app: models.Application, query: {validate?: boolean}) => updateApp(app, query)}
|
||||
selectedNode={selectedNode}
|
||||
appCxt={{...appContext, apis: appContext} as unknown as AppContext}
|
||||
tab={tab}
|
||||
/>
|
||||
)}
|
||||
</SlidingPanel>
|
||||
<SlidingPanel isShown={showOperationState && !!operationState} onClose={() => setOperationStatusVisible(false)}>
|
||||
{operationState && <ApplicationOperationState application={application} operationState={operationState} />}
|
||||
</SlidingPanel>
|
||||
<SlidingPanel isShown={showHydrateOperationState && !!hydrateOperationState} onClose={() => setHydrateOperationStatusVisible(false)}>
|
||||
{hydrateOperationState && <ApplicationHydrateOperationState hydrateOperationState={hydrateOperationState} />}
|
||||
</SlidingPanel>
|
||||
<SlidingPanel isShown={showConditions && !!conditions} onClose={() => setConditionsStatusVisible(false)}>
|
||||
{conditions && <ApplicationConditions conditions={conditions} />}
|
||||
</SlidingPanel>
|
||||
<SlidingPanel
|
||||
isShown={state.revision === 'SYNC_STATUS_REVISION' || state.revision === 'OPERATION_STATE_REVISION'}
|
||||
isMiddle={true}
|
||||
onClose={() => 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
|
||||
</SlidingPanel>
|
||||
)}
|
||||
{isApplication && (
|
||||
<ApplicationSyncPanel
|
||||
application={application as appModels.Application}
|
||||
hide={() => AppUtils.showDeploy(null, null, appContext)}
|
||||
selectedResource={syncResourceKey}
|
||||
/>
|
||||
)}
|
||||
{isApplication && (
|
||||
<SlidingPanel isShown={selectedRollbackDeploymentIndex > -1} onClose={() => setRollbackPanelVisible(-1)}>
|
||||
{selectedRollbackDeploymentIndex > -1 && (
|
||||
<ApplicationDeploymentHistory
|
||||
app={application as appModels.Application}
|
||||
rollbackApp={info => rollbackApplication(info, application as appModels.Application)}
|
||||
selectDeployment={i => setRollbackPanelVisible(i)}
|
||||
/>
|
||||
)}
|
||||
</SlidingPanel>
|
||||
)}
|
||||
{isApplication && (
|
||||
<SlidingPanel isShown={showOperationState && !!operationState} onClose={() => setOperationStatusVisible(false)}>
|
||||
{operationState && <ApplicationOperationState application={application as appModels.Application} operationState={operationState} />}
|
||||
</SlidingPanel>
|
||||
)}
|
||||
{isApplication && (
|
||||
<SlidingPanel isShown={showHydrateOperationState && !!hydrateOperationState} onClose={() => setHydrateOperationStatusVisible(false)}>
|
||||
{hydrateOperationState && <ApplicationHydrateOperationState hydrateOperationState={hydrateOperationState} />}
|
||||
</SlidingPanel>
|
||||
)}
|
||||
<SlidingPanel isShown={showConditions && !!conditions} onClose={() => setConditionsStatusVisible(false)}>
|
||||
{conditions &&
|
||||
(isApplication ? (
|
||||
<ApplicationConditions conditions={conditions as appModels.ApplicationCondition[]} />
|
||||
) : (
|
||||
<ApplicationSetConditions conditions={conditions as appModels.ApplicationSetCondition[]} />
|
||||
))}
|
||||
</SlidingPanel>
|
||||
<SlidingPanel
|
||||
isShown={selectedExtension !== '' && activeStatusExt != null && activeStatusExt.flyout != null}
|
||||
onClose={() => setExtensionPanelVisible('')}>
|
||||
{selectedExtension !== '' && activeStatusExt?.flyout && <activeStatusExt.flyout application={application} tree={tree} />}
|
||||
</SlidingPanel>
|
||||
<SlidingPanel
|
||||
isMiddle={activeTopBarActionMenuExt?.isMiddle ?? true}
|
||||
isShown={selectedExtension !== '' && activeTopBarActionMenuExt != null && activeTopBarActionMenuExt.flyout != null}
|
||||
onClose={() => setExtensionPanelVisible('')}>
|
||||
{selectedExtension !== '' && activeTopBarActionMenuExt?.flyout && (
|
||||
<activeTopBarActionMenuExt.flyout application={application} tree={tree} />
|
||||
)}
|
||||
</SlidingPanel>
|
||||
{isApplication && (
|
||||
<SlidingPanel
|
||||
isShown={state.revision === 'SYNC_STATUS_REVISION' || state.revision === 'OPERATION_STATE_REVISION'}
|
||||
isMiddle={true}
|
||||
onClose={() => 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
|
||||
)}
|
||||
</SlidingPanel>
|
||||
)}
|
||||
{isApplication && (
|
||||
<SlidingPanel
|
||||
isShown={selectedExtension !== '' && activeStatusExt != null && activeStatusExt.flyout != null}
|
||||
onClose={() => setExtensionPanelVisible('')}>
|
||||
{selectedExtension !== '' && activeStatusExt?.flyout && (
|
||||
<activeStatusExt.flyout application={application as appModels.Application} tree={tree} />
|
||||
)}
|
||||
</SlidingPanel>
|
||||
)}
|
||||
{isApplication && (
|
||||
<SlidingPanel
|
||||
isMiddle={activeTopBarActionMenuExt?.isMiddle ?? true}
|
||||
isShown={selectedExtension !== '' && activeTopBarActionMenuExt != null && activeTopBarActionMenuExt.flyout != null}
|
||||
onClose={() => setExtensionPanelVisible('')}>
|
||||
{selectedExtension !== '' && activeTopBarActionMenuExt?.flyout && (
|
||||
<activeTopBarActionMenuExt.flyout application={application as appModels.Application} tree={tree} />
|
||||
)}
|
||||
</SlidingPanel>
|
||||
)}
|
||||
</Page>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
@@ -919,8 +921,8 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) =>
|
||||
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<string, models.ResourceStatus>();
|
||||
props.app.status.resources.forEach(res => statusByKey.set(nodeKey(res), res));
|
||||
const appSetStatusByKey = new Map<string, models.ApplicationSetResource>();
|
||||
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<string, ResourceTreeNode>();
|
||||
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<string>();
|
||||
const orphanedKeys = isApp(props.app) ? new Set(props.tree.orphanedNodes?.map(nodeKey)) : new Set<string>();
|
||||
const orphans: ResourceTreeNode[] = [];
|
||||
let allChildNodes: ResourceTreeNode[] = [];
|
||||
nodesHavingChildren.set(appNode.uid, 1);
|
||||
|
||||
@@ -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) => (
|
||||
<label style={{display: 'flex', alignItems: 'flex-start', fontSize: '12px', fontWeight: 600, color: ARGO_GRAY6_COLOR, minHeight: '18px'}}>
|
||||
{info.title}
|
||||
{info.helpContent && (
|
||||
<span style={{marginLeft: '5px'}}>
|
||||
<HelpIcon title={info.helpContent} />
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className='application-status-panel row'>
|
||||
<div className='application-status-panel__item'>
|
||||
{sectionLabel({title: 'APPSET HEALTH', helpContent: 'The health status of your ApplicationSet derived from its conditions'})}
|
||||
<div className='application-status-panel__item-value'>
|
||||
<HealthStatusIcon state={{status: healthStatus, message: ''}} />
|
||||
|
||||
{healthStatus}
|
||||
</div>
|
||||
{latestCondition?.message && <div className='application-status-panel__item-name'>{latestCondition.message}</div>}
|
||||
</div>
|
||||
|
||||
{conditions.length > 0 && (
|
||||
<div className='application-status-panel__item'>
|
||||
{sectionLabel({title: 'CONDITIONS'})}
|
||||
<div className='application-status-panel__item-value application-status-panel__conditions' onClick={() => showConditions && showConditions()}>
|
||||
{conditionCounts.info > 0 && (
|
||||
<a className='info'>
|
||||
<i className='fa fa-info-circle application-status-panel__item-value__status-button' /> {conditionCounts.info} Info
|
||||
</a>
|
||||
)}
|
||||
{conditionCounts.error > 0 && (
|
||||
<a className='error'>
|
||||
<i className='fa fa-exclamation-circle application-status-panel__item-value__status-button' /> {conditionCounts.error} Error
|
||||
{conditionCounts.error !== 1 && 's'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{latestCondition?.lastTransitionTime && (
|
||||
<div className='application-status-panel__item'>
|
||||
{sectionLabel({title: 'LAST UPDATED'})}
|
||||
<div className='application-status-panel__item-value'>
|
||||
<Timestamp date={latestCondition.lastTransitionTime} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 */}
|
||||
<div className='row'>
|
||||
<div className='columns small-11'>
|
||||
<i className='icon argo-icon-git' />
|
||||
<ResourceIcon group='argoproj.io' kind='ApplicationSet' customStyle={{marginRight: '5px'}} />
|
||||
<Tooltip content={AppUtils.appInstanceName(appSet)}>
|
||||
<span className='applications-list__title'>{AppUtils.appQualifiedName(appSet, useAuthSettingsCtx?.appsInAnyNamespaceEnabled)}</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -10,6 +10,15 @@ export const ResourceIcon = ({group, kind, customStyle}: {group: string; kind: s
|
||||
if (kind === 'Application') {
|
||||
return <i title={kind} className={`icon argo-icon-application`} style={customStyle} />;
|
||||
}
|
||||
if (kind === 'ApplicationSet') {
|
||||
return (
|
||||
<span title={kind} style={{display: 'inline-flex', alignItems: 'center', ...customStyle}}>
|
||||
{'{'}
|
||||
<i className='icon argo-icon-application' style={{margin: '0 1px'}} />
|
||||
{'}'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
// First, check for group-based custom icons
|
||||
if (group) {
|
||||
const matchedGroup = matchGroupToResource(group);
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user