mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
Signed-off-by: Alex Collins <alex_collins@intuit.com>
This commit is contained in:
parent
1b46143d07
commit
2b3601cfa3
4 changed files with 220 additions and 55 deletions
4
Makefile
4
Makefile
|
|
@ -430,7 +430,7 @@ start-e2e-local:
|
|||
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
|
||||
ARGOCD_GPG_DATA_PATH=/tmp/argo-e2e/app/config/gpg/source \
|
||||
ARGOCD_GNUPGHOME=/tmp/argo-e2e/app/config/gpg/keys \
|
||||
ARGOCD_GPG_ENABLED=true \
|
||||
ARGOCD_GPG_ENABLED=$(ARGOCD_GPG_ENABLED) \
|
||||
ARGOCD_E2E_DISABLE_AUTH=false \
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
|
||||
|
|
@ -463,7 +463,7 @@ start-local: mod-vendor-local dep-ui-local
|
|||
mkdir -p /tmp/argocd-local/gpg/source
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
ARGOCD_IN_CI=false \
|
||||
ARGOCD_GPG_ENABLED=true \
|
||||
ARGOCD_GPG_ENABLED=$(ARGOCD_GPG_ENABLED) \
|
||||
ARGOCD_E2E_TEST=false \
|
||||
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {Checkbox as ArgoCheckbox, DropDownMenu, NotificationType, SlidingPanel, TopBarFilter} from 'argo-ui';
|
||||
import {Checkbox as ArgoCheckbox, DropDownMenu, NotificationType, SlidingPanel} from 'argo-ui';
|
||||
import * as classNames from 'classnames';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
|
|
@ -9,6 +9,7 @@ import {delay, filter, map, mergeMap, repeat, retryWhen} from 'rxjs/operators';
|
|||
import {DataLoader, EmptyState, ErrorNotification, ObservableQuery, Page, Paginate, Revision, Timestamp} from '../../../shared/components';
|
||||
import {AppContext, ContextApis} from '../../../shared/context';
|
||||
import * as appModels from '../../../shared/models';
|
||||
import {ApplicationTree} from '../../../shared/models';
|
||||
import {AppDetailsPreferences, AppsDetailsViewType, services} from '../../../shared/services';
|
||||
|
||||
import {ApplicationConditions} from '../application-conditions/application-conditions';
|
||||
|
|
@ -21,6 +22,7 @@ import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-p
|
|||
import {ResourceDetails} from '../resource-details/resource-details';
|
||||
import * as AppUtils from '../utils';
|
||||
import {ApplicationResourceList} from './application-resource-list';
|
||||
import {Filters} from './filters';
|
||||
|
||||
require('./application-details.scss');
|
||||
|
||||
|
|
@ -29,6 +31,15 @@ interface ApplicationDetailsState {
|
|||
revision?: string;
|
||||
}
|
||||
|
||||
interface FilterInput {
|
||||
kind: string[];
|
||||
health: string[];
|
||||
sync: string[];
|
||||
namespace: string[];
|
||||
createdWithin: number[]; // number of minutes the resource must be created within
|
||||
ownership: string[];
|
||||
}
|
||||
|
||||
export const NodeInfo = (node?: string): {key: string; container: number} => {
|
||||
const nodeContainer = {key: '', container: 0};
|
||||
if (node) {
|
||||
|
|
@ -108,44 +119,13 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{nam
|
|||
}>
|
||||
{({application, tree, pref}: {application: appModels.Application; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => {
|
||||
tree.nodes = tree.nodes || [];
|
||||
const kindsSet = new Set<string>(tree.nodes.map(item => item.kind));
|
||||
const treeFilter = this.getTreeFilter(pref.resourceFilter);
|
||||
treeFilter.kind.forEach(kind => {
|
||||
kindsSet.add(kind);
|
||||
});
|
||||
const kinds = Array.from(kindsSet);
|
||||
const noKindsFilter = pref.resourceFilter.filter(item => item.indexOf('kind:') !== 0);
|
||||
const refreshing = application.metadata.annotations && application.metadata.annotations[appModels.AnnotationRefreshKey];
|
||||
|
||||
const filterTopBar: TopBarFilter<string> = {
|
||||
items: [
|
||||
{content: () => <span>Sync</span>},
|
||||
{value: 'sync:Synced', label: 'Synced'},
|
||||
// Unhealthy includes 'Unknown' and 'OutOfSync'
|
||||
{value: 'sync:OutOfSync', label: 'OutOfSync'},
|
||||
{content: () => <span>Health</span>},
|
||||
{value: 'health:Healthy', label: 'Healthy'},
|
||||
{value: 'health:Progressing', label: 'Progressing'},
|
||||
{value: 'health:Degraded', label: 'Degraded'},
|
||||
{value: 'health:Missing', label: 'Missing'},
|
||||
{value: 'health:Unknown', label: 'Unknown'},
|
||||
{
|
||||
content: setSelection => (
|
||||
<div>
|
||||
Kinds <a onClick={() => setSelection(noKindsFilter.concat(kinds.map(kind => `kind:${kind}`)))}>all</a> /{' '}
|
||||
<a onClick={() => setSelection(noKindsFilter)}>none</a>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
...kinds.sort().map(kind => ({value: `kind:${kind}`, label: kind}))
|
||||
],
|
||||
selectedValues: pref.resourceFilter,
|
||||
selectionChanged: items => {
|
||||
this.appContext.apis.navigation.goto('.', {resource: `${items.join(',')}`});
|
||||
services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: items}});
|
||||
}
|
||||
const setFilter = (items: string[]) => {
|
||||
this.appContext.apis.navigation.goto('.', {resource: items.join(',')});
|
||||
services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: items}});
|
||||
};
|
||||
|
||||
const clearFilter = () => setFilter([]);
|
||||
const refreshing = application.metadata.annotations && application.metadata.annotations[appModels.AnnotationRefreshKey];
|
||||
const appNodesByName = this.groupAppNodesByKey(application, tree);
|
||||
const selectedItem = (this.selectedNodeKey && appNodesByName.get(this.selectedNodeKey)) || null;
|
||||
const isAppSelected = selectedItem === application;
|
||||
|
|
@ -166,7 +146,7 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{nam
|
|||
.filter(res => {
|
||||
const resNode: ResourceTreeNode = {...res, root: null, info: null, parentRefs: [], resourceVersion: '', uid: ''};
|
||||
resNode.root = resNode;
|
||||
return this.filterTreeNode(resNode, treeFilter);
|
||||
return this.filterTreeNode(tree, resNode, treeFilter);
|
||||
})
|
||||
.concat(orphans);
|
||||
|
||||
|
|
@ -175,7 +155,6 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{nam
|
|||
<Page
|
||||
title='Application Details'
|
||||
toolbar={{
|
||||
filter: filterTopBar,
|
||||
breadcrumbs: [{title: 'Applications', path: '/applications'}, {title: this.props.match.params.name}],
|
||||
actionMenu: {items: this.getApplicationActionMenu(application)},
|
||||
tools: (
|
||||
|
|
@ -227,6 +206,7 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{nam
|
|||
</div>
|
||||
<div className='application-details__tree'>
|
||||
{refreshing && <p className='application-details__refreshing-label'>Refreshing</p>}
|
||||
<Filters pref={pref} tree={tree} onSetFilter={setFilter} onClearFilter={clearFilter} />
|
||||
{(tree.orphanedNodes || []).length > 0 && (
|
||||
<div className='application-details__orphaned-filter'>
|
||||
<ArgoCheckbox
|
||||
|
|
@ -242,7 +222,7 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{nam
|
|||
)}
|
||||
{((pref.view === 'tree' || pref.view === 'network') && (
|
||||
<ApplicationResourceTree
|
||||
nodeFilter={node => this.filterTreeNode(node, treeFilter)}
|
||||
nodeFilter={node => this.filterTreeNode(tree, node, treeFilter)}
|
||||
selectedNodeFullName={this.selectedNodeKey}
|
||||
onNodeClick={fullName => this.selectNode(fullName)}
|
||||
nodeMenu={node =>
|
||||
|
|
@ -254,10 +234,7 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{nam
|
|||
app={application}
|
||||
showOrphanedResources={pref.orphanedResources}
|
||||
useNetworkingHierarchy={pref.view === 'network'}
|
||||
onClearFilter={() => {
|
||||
this.appContext.apis.navigation.goto('.', {resource: ''});
|
||||
services.viewPreferences.updatePreferences({appDetails: {...pref, resourceFilter: []}});
|
||||
}}
|
||||
onClearFilter={clearFilter}
|
||||
/>
|
||||
)) ||
|
||||
(pref.view === 'pods' && (
|
||||
|
|
@ -459,14 +436,43 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{nam
|
|||
];
|
||||
}
|
||||
|
||||
private filterTreeNode(node: ResourceTreeNode, filterInput: {kind: string[]; health: string[]; sync: string[]}): boolean {
|
||||
private filterTreeNode(tree: ApplicationTree, node: ResourceTreeNode, filterInput: FilterInput, ownership?: string): boolean {
|
||||
const syncStatuses = filterInput.sync.map(item => (item === 'OutOfSync' ? ['OutOfSync', 'Unknown'] : [item])).reduce((first, second) => first.concat(second), []);
|
||||
|
||||
return (
|
||||
const minutesAgo = (m: number) => {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() - m * 60000);
|
||||
return d;
|
||||
};
|
||||
const createdAt = new Date(node.createdAt); // will be falsely if the node has not been created, and so will not appear
|
||||
const createdWithin = (n: number) => createdAt.getTime() > minutesAgo(n).getTime();
|
||||
|
||||
const root = node.root || ({} as ResourceTreeNode);
|
||||
const hook = root && root.hook;
|
||||
if (
|
||||
(filterInput.kind.length === 0 || filterInput.kind.indexOf(node.kind) > -1) &&
|
||||
(syncStatuses.length === 0 || node.root.hook || (node.root.status && syncStatuses.indexOf(node.root.status) > -1)) &&
|
||||
(filterInput.health.length === 0 || node.root.hook || (node.root.health && filterInput.health.indexOf(node.root.health.status) > -1))
|
||||
);
|
||||
(syncStatuses.length === 0 || hook || (root.status && syncStatuses.indexOf(root.status) > -1)) &&
|
||||
(filterInput.health.length === 0 || hook || (root.health && filterInput.health.indexOf(root.health.status) > -1)) &&
|
||||
(filterInput.namespace.length === 0 || filterInput.namespace.includes(node.namespace)) &&
|
||||
(filterInput.createdWithin.length === 0 || !!filterInput.createdWithin.find(v => createdWithin(v)))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (filterInput.ownership.includes('Owned') && ownership !== 'Owners') {
|
||||
const owned = tree.nodes.filter(n => (node.parentRefs || []).find(r => r.uid === n.uid));
|
||||
if (owned.find(n => this.filterTreeNode(tree, n, filterInput, 'Owned'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (filterInput.ownership.includes('Owners') && ownership !== 'Owned') {
|
||||
const owners = tree.nodes.filter(n => (n.parentRefs || []).find(r => r.uid === node.uid));
|
||||
if (owners.find(n => this.filterTreeNode(tree, n, filterInput, 'Owners'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private loadAppInfo(name: string): Observable<{application: appModels.Application; tree: appModels.ApplicationTree}> {
|
||||
|
|
@ -535,11 +541,14 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{nam
|
|||
return nodeByKey;
|
||||
}
|
||||
|
||||
private getTreeFilter(filterInput: string[]): {kind: string[]; health: string[]; sync: string[]} {
|
||||
private getTreeFilter(filterInput: string[]): FilterInput {
|
||||
const kind = new Array<string>();
|
||||
const health = new Array<string>();
|
||||
const sync = new Array<string>();
|
||||
for (const item of filterInput) {
|
||||
const namespace = new Array<string>();
|
||||
const createdWithin = new Array<number>();
|
||||
const ownership = new Array<string>();
|
||||
for (const item of filterInput || []) {
|
||||
const [type, val] = item.split(':');
|
||||
switch (type) {
|
||||
case 'kind':
|
||||
|
|
@ -551,9 +560,18 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{nam
|
|||
case 'sync':
|
||||
sync.push(val);
|
||||
break;
|
||||
case 'namespace':
|
||||
namespace.push(val);
|
||||
break;
|
||||
case 'createdWithin':
|
||||
createdWithin.push(parseInt(val, 10));
|
||||
break;
|
||||
case 'ownership':
|
||||
ownership.push(val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {kind, health, sync};
|
||||
return {kind, health, sync, namespace, createdWithin, ownership};
|
||||
}
|
||||
|
||||
private setOperationStatusVisible(isVisible: boolean) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
import {HelpIcon} from 'argo-ui';
|
||||
import * as React from 'react';
|
||||
import {useContext} from 'react';
|
||||
import {Context} from '../../../shared/context';
|
||||
import {ApplicationTree} from '../../../shared/models';
|
||||
import {AppDetailsPreferences} from '../../../shared/services';
|
||||
|
||||
const uniq = (value: string, index: number, self: string[]) => self.indexOf(value) === index;
|
||||
|
||||
export const Filters = ({
|
||||
pref,
|
||||
tree,
|
||||
onSetFilter,
|
||||
onClearFilter
|
||||
}: {
|
||||
pref: AppDetailsPreferences;
|
||||
tree: ApplicationTree;
|
||||
onSetFilter: (items: string[]) => void;
|
||||
onClearFilter: () => void;
|
||||
}) => {
|
||||
const {history, navigation} = useContext(Context);
|
||||
|
||||
const shown = new URLSearchParams(history.location.search).get('showFilters') === 'true';
|
||||
const setShown = (showFilters: boolean) => navigation.goto('.', {showFilters});
|
||||
|
||||
const resourceFilter = pref.resourceFilter || [];
|
||||
const hasPrefix = (prefix: string) => (v: string) => v.startsWith(prefix + ':');
|
||||
const removePrefix = (prefix: string) => (v: string) => v.replace(prefix + ':', '');
|
||||
|
||||
const anyFiltered = pref.resourceFilter.length > 0;
|
||||
const isFiltered = (prefix: string, suffix: string) => resourceFilter.includes(`${prefix}:${suffix}`);
|
||||
const anyPrefixFiltered = (prefix: string) => resourceFilter.find(hasPrefix(prefix));
|
||||
const setFilters = (prefix: string, suffixes: string[], v: boolean) => {
|
||||
const filters = suffixes.map(suffix => `${prefix}:${suffix}`);
|
||||
const items = resourceFilter.filter(y => !filters.includes(y));
|
||||
if (v) {
|
||||
items.push(...filters);
|
||||
}
|
||||
onSetFilter(items);
|
||||
};
|
||||
const enableFilter = (prefix: string, suffix: string) => {
|
||||
const items = resourceFilter.filter(v => !hasPrefix(prefix)(v)).concat([`${prefix}:${suffix}`]);
|
||||
onSetFilter(items);
|
||||
};
|
||||
// this is smarter than it looks at first glance, rather than just un-checked known items,
|
||||
// it instead finds out what is enabled, and then removes them, which will be tolerant to weird or unknown items
|
||||
const clearFilters = (prefix: string) => {
|
||||
return setFilters(prefix, resourceFilter.filter(hasPrefix(prefix)).map(removePrefix(prefix)), false);
|
||||
};
|
||||
|
||||
// we need to include ones that might have been filter in other apps that do not apply to the current app,
|
||||
// otherwise the user will not be able to clear them from this panel
|
||||
const alreadyFilteredOn = (prefix: string) => resourceFilter.filter(hasPrefix(prefix)).map(removePrefix(prefix));
|
||||
|
||||
const kinds = tree.nodes
|
||||
.map(x => x.kind)
|
||||
.concat(alreadyFilteredOn('kind'))
|
||||
.filter(uniq)
|
||||
.sort();
|
||||
const namespaces = tree.nodes
|
||||
.map(x => x.namespace)
|
||||
.concat(alreadyFilteredOn('namespace'))
|
||||
.filter(uniq)
|
||||
.sort();
|
||||
|
||||
const checkbox = (prefix: string, suffix: string, label: string = null) => (
|
||||
<label key={suffix} style={{display: 'inline-block', paddingRight: 5}}>
|
||||
<input type='checkbox' checked={isFiltered(prefix, suffix)} onChange={() => setFilters(prefix, [suffix], !isFiltered(prefix, suffix))} /> {label || suffix}
|
||||
</label>
|
||||
);
|
||||
const checkboxes = (prefix: string, suffixes: string[]) => suffixes.map(suffix => checkbox(prefix, suffix));
|
||||
|
||||
const radiobox = (prefix: string, suffix: string, label: string) => (
|
||||
<label key={suffix}>
|
||||
<input type='radio' onChange={() => enableFilter(prefix, suffix)} checked={isFiltered(prefix, suffix)} /> {label}{' '}
|
||||
</label>
|
||||
);
|
||||
|
||||
const clearFilterLink = (prefix: string) =>
|
||||
anyPrefixFiltered(prefix) && (
|
||||
<small style={{marginLeft: 'auto'}}>
|
||||
<a onClick={() => clearFilters(prefix)}>clear</a>
|
||||
</small>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='applications-list__filters__title'>
|
||||
FILTERS <i className='fa fa-filter' />
|
||||
{anyFiltered && (
|
||||
<small>
|
||||
<a onClick={() => onClearFilter()}>clear all</a>
|
||||
</small>
|
||||
)}
|
||||
<a onClick={() => setShown(!shown)} style={{marginLeft: 'auto', fontSize: '12px', lineHeight: '5px', display: shown && 'block'}}>
|
||||
{!shown ? 'SHOW' : 'HIDE'}
|
||||
</a>
|
||||
</div>
|
||||
{shown && (
|
||||
<div className='applications-list__filters'>
|
||||
<div className='filter'>
|
||||
<div className='filter__header'>
|
||||
OWNERSHIP
|
||||
<HelpIcon
|
||||
title='Always show resources that own/owned by resources that will be shown.
|
||||
For example, if you you want to find what owns pod, so you select "pods" ond choose "Owners"'
|
||||
/>
|
||||
{clearFilterLink('ownership')}
|
||||
</div>
|
||||
<div>{checkboxes('ownership', ['Owners', 'Owned'])}</div>
|
||||
</div>
|
||||
<div className='filter'>
|
||||
<div className='filter__header'>SYNC STATUS {clearFilterLink('sync')}</div>
|
||||
<div>{checkboxes('sync', ['Synced', 'OutOfSync'])}</div>
|
||||
</div>
|
||||
<div className='filter'>
|
||||
<div className='filter__header'>HEALTH STATUS {clearFilterLink('health')}</div>
|
||||
<div>{checkboxes('health', ['Healthy', 'Progressing', 'Degraded', 'Suspended', 'Missing', 'Unknown'])}</div>
|
||||
</div>
|
||||
<div className='filter'>
|
||||
<div className='filter__header'>
|
||||
KINDS
|
||||
<small style={{marginLeft: 'auto'}}>
|
||||
<a onClick={() => setFilters('kind', kinds, true)}>all</a> {anyPrefixFiltered('kind') && <a onClick={() => clearFilters('kind')}>clear</a>}
|
||||
</small>
|
||||
</div>
|
||||
<div>{checkboxes('kind', kinds)}</div>
|
||||
</div>
|
||||
{namespaces.length > 1 && (
|
||||
<div className='filter'>
|
||||
<div className='filter__header'>NAMESPACE {clearFilterLink('namespace')}</div>
|
||||
<div>{checkboxes('namespace', namespaces)}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='filter'>
|
||||
<div className='filter__header'>
|
||||
CREATED WITHIN
|
||||
<HelpIcon title='Use this to find recently created resources, if you want recently synced resources, please raise an issue' />
|
||||
{clearFilterLink('createdWithin')}
|
||||
</div>
|
||||
<div>{[1, 3, 5, 15, 60].map(m => radiobox('createdWithin', String(m), m + 'm'))}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -71,7 +71,7 @@ const DEFAULT_PREFERENCES: ViewPreferences = {
|
|||
version: 1,
|
||||
appDetails: {
|
||||
view: 'tree',
|
||||
resourceFilter: ['kind:Deployment', 'kind:Service', 'kind:Pod', 'kind:StatefulSet', 'kind:Ingress', 'kind:ConfigMap', 'kind:Job', 'kind:DaemonSet', 'kind:Workflow'],
|
||||
resourceFilter: [],
|
||||
inlineDiff: false,
|
||||
compactDiff: false,
|
||||
resourceView: 'manifest',
|
||||
|
|
|
|||
Loading…
Reference in a new issue