diff --git a/Makefile b/Makefile index 13f39574c5..ed4b22eda3 100644 --- a/Makefile +++ b/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} 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 3ef166993b..41624b5db8 100644 --- a/ui/src/app/applications/components/application-details/application-details.tsx +++ b/ui/src/app/applications/components/application-details/application-details.tsx @@ -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 {({application, tree, pref}: {application: appModels.Application; tree: appModels.ApplicationTree; pref: AppDetailsPreferences}) => { tree.nodes = tree.nodes || []; - const kindsSet = new Set(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 = { - items: [ - {content: () => Sync}, - {value: 'sync:Synced', label: 'Synced'}, - // Unhealthy includes 'Unknown' and 'OutOfSync' - {value: 'sync:OutOfSync', label: 'OutOfSync'}, - {content: () => Health}, - {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 => ( -
- Kinds setSelection(noKindsFilter.concat(kinds.map(kind => `kind:${kind}`)))}>all /{' '} - setSelection(noKindsFilter)}>none -
- ) - }, - ...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 { 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
{refreshing &&

Refreshing

} + {(tree.orphanedNodes || []).length > 0 && (
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 { - 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 (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(); const health = new Array(); const sync = new Array(); - for (const item of filterInput) { + const namespace = new Array(); + const createdWithin = new Array(); + const ownership = new Array(); + for (const item of filterInput || []) { const [type, val] = item.split(':'); switch (type) { case 'kind': @@ -551,9 +560,18 @@ export class ApplicationDetails extends React.Component 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) => ( + + ); + const checkboxes = (prefix: string, suffixes: string[]) => suffixes.map(suffix => checkbox(prefix, suffix)); + + const radiobox = (prefix: string, suffix: string, label: string) => ( + + ); + + const clearFilterLink = (prefix: string) => + anyPrefixFiltered(prefix) && ( + + clearFilters(prefix)}>clear + + ); + + return ( + <> + + {shown && ( +
+
+
+ OWNERSHIP + + {clearFilterLink('ownership')} +
+
{checkboxes('ownership', ['Owners', 'Owned'])}
+
+
+
SYNC STATUS {clearFilterLink('sync')}
+
{checkboxes('sync', ['Synced', 'OutOfSync'])}
+
+
+
HEALTH STATUS {clearFilterLink('health')}
+
{checkboxes('health', ['Healthy', 'Progressing', 'Degraded', 'Suspended', 'Missing', 'Unknown'])}
+
+
+
+ KINDS + + setFilters('kind', kinds, true)}>all {anyPrefixFiltered('kind') && clearFilters('kind')}>clear} + +
+
{checkboxes('kind', kinds)}
+
+ {namespaces.length > 1 && ( +
+
NAMESPACE {clearFilterLink('namespace')}
+
{checkboxes('namespace', namespaces)}
+
+ )} +
+
+ CREATED WITHIN + + {clearFilterLink('createdWithin')} +
+
{[1, 3, 5, 15, 60].map(m => radiobox('createdWithin', String(m), m + 'm'))}
+
+
+ )} + + ); +}; diff --git a/ui/src/app/shared/services/view-preferences-service.ts b/ui/src/app/shared/services/view-preferences-service.ts index 38081ad9eb..2c2f837af4 100644 --- a/ui/src/app/shared/services/view-preferences-service.ts +++ b/ui/src/app/shared/services/view-preferences-service.ts @@ -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',