mirror of
https://github.com/argoproj/argo-cd
synced 2026-05-23 09:18:26 +00:00
Execute application label filtering on client side (#2605)
This commit is contained in:
parent
6930ecc947
commit
89b33a1442
6 changed files with 96 additions and 10 deletions
|
|
@ -4,5 +4,6 @@
|
|||
"printWidth": 180,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"jsxBracketSameLine": true
|
||||
"jsxBracketSameLine": true,
|
||||
"quoteProps": "consistent"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod
|
|||
<div
|
||||
onClick={() => props.onNodeClick && props.onNodeClick(fullName)}
|
||||
className={classNames('application-resource-tree__node', {
|
||||
active: fullName === props.selectedNodeFullName,
|
||||
'active': fullName === props.selectedNodeFullName,
|
||||
'application-resource-tree__node--orphaned': node.orphaned
|
||||
})}
|
||||
style={{left: node.x, top: node.y, width: node.width, height: node.height}}>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {AppsListPreferences, AppsListViewType, services} from '../../../shared/s
|
|||
import {ApplicationCreatePanel} from '../application-create-panel/application-create-panel';
|
||||
import {ApplicationSyncPanel} from '../application-sync-panel/application-sync-panel';
|
||||
import {ApplicationsSyncPanel} from '../applications-sync-panel/applications-sync-panel';
|
||||
import * as LabelSelector from '../label-selector';
|
||||
import * as AppUtils from '../utils';
|
||||
import {ApplicationsFilter} from './applications-filter';
|
||||
import {ApplicationsSummary} from './applications-summary';
|
||||
|
|
@ -38,12 +39,12 @@ const APP_FIELDS = [
|
|||
const APP_LIST_FIELDS = APP_FIELDS.map(field => `items.${field}`);
|
||||
const APP_WATCH_FIELDS = ['result.type', ...APP_FIELDS.map(field => `result.application.${field}`)];
|
||||
|
||||
function loadApplications(selector: string): Observable<models.Application[]> {
|
||||
return Observable.fromPromise(services.applications.list([], {fields: APP_LIST_FIELDS, selector})).flatMap(applications =>
|
||||
function loadApplications(): Observable<models.Application[]> {
|
||||
return Observable.fromPromise(services.applications.list([], {fields: APP_LIST_FIELDS})).flatMap(applications =>
|
||||
Observable.merge(
|
||||
Observable.from([applications]),
|
||||
services.applications
|
||||
.watch(null, {fields: APP_WATCH_FIELDS, selector})
|
||||
.watch(null, {fields: APP_WATCH_FIELDS})
|
||||
.map(appChange => {
|
||||
const index = applications.findIndex(item => item.metadata.name === appChange.application.metadata.name);
|
||||
if (index > -1 && appChange.application.metadata.resourceVersion === applications[index].metadata.resourceVersion) {
|
||||
|
|
@ -116,6 +117,7 @@ const ViewPref = ({children}: {children: (pref: AppsListPreferences & {page: num
|
|||
viewPref.labelsFilter = params
|
||||
.get('labels')
|
||||
.split(',')
|
||||
.map(decodeURIComponent)
|
||||
.filter(item => !!item);
|
||||
}
|
||||
return {...viewPref, page: parseInt(params.get('page') || '0', 10), search: params.get('search') || ''};
|
||||
|
|
@ -136,7 +138,8 @@ function filterApps(applications: models.Application[], pref: AppsListPreference
|
|||
(pref.syncFilter.length === 0 || pref.syncFilter.includes(app.status.sync.status)) &&
|
||||
(pref.healthFilter.length === 0 || pref.healthFilter.includes(app.status.health.status)) &&
|
||||
(pref.namespacesFilter.length === 0 || pref.namespacesFilter.some(ns => minimatch(app.spec.destination.namespace, ns))) &&
|
||||
(pref.clustersFilter.length === 0 || pref.clustersFilter.some(server => minimatch(app.spec.destination.server, server)))
|
||||
(pref.clustersFilter.length === 0 || pref.clustersFilter.some(server => minimatch(app.spec.destination.server, server))) &&
|
||||
(pref.labelsFilter.length === 0 || pref.labelsFilter.every(selector => LabelSelector.match(selector, app.metadata.labels)))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -209,8 +212,7 @@ export const ApplicationsList = (props: RouteComponentProps<{}>) => {
|
|||
<ViewPref>
|
||||
{pref => (
|
||||
<DataLoader
|
||||
input={(pref.labelsFilter || []).join(',')}
|
||||
load={selector => loadApplications(selector)}
|
||||
load={() => loadApplications()}
|
||||
loadingRenderer={() => (
|
||||
<div className='argo-container'>
|
||||
<MockupList height={100} marginTop={30} />
|
||||
|
|
@ -275,7 +277,7 @@ export const ApplicationsList = (props: RouteComponentProps<{}>) => {
|
|||
health: newPref.healthFilter.join(','),
|
||||
namespace: newPref.namespacesFilter.join(','),
|
||||
cluster: newPref.clustersFilter.join(','),
|
||||
labels: newPref.labelsFilter.join(',')
|
||||
labels: newPref.labelsFilter.map(encodeURIComponent).join(',')
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const ApplicationsSyncPanel = ({show, apps, hide}: {show: boolean; apps:
|
|||
<div>
|
||||
<button className='argo-button argo-button--base' onClick={() => form.submitForm(null)}>
|
||||
Sync
|
||||
</button>
|
||||
</button>{' '}
|
||||
<button onClick={() => hide()} className='argo-button argo-button--base-o'>
|
||||
Cancel
|
||||
</button>
|
||||
|
|
|
|||
44
ui/src/app/applications/components/label-selector.test.ts
Normal file
44
ui/src/app/applications/components/label-selector.test.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as LabelSelector from './label-selector';
|
||||
|
||||
test('exists', () => {
|
||||
expect(LabelSelector.match('test', {test: 'hello'})).toBeTruthy();
|
||||
expect(LabelSelector.match('test1', {test: 'hello'})).toBeFalsy();
|
||||
});
|
||||
|
||||
test('not exists', () => {
|
||||
expect(LabelSelector.match('!test', {test: 'hello'})).toBeFalsy();
|
||||
expect(LabelSelector.match('!test1', {test: 'hello'})).toBeTruthy();
|
||||
});
|
||||
|
||||
test('in', () => {
|
||||
expect(LabelSelector.match('test in 1, 2, 3', {test: '1'})).toBeTruthy();
|
||||
expect(LabelSelector.match('test in 1, 2, 3', {test: '4'})).toBeFalsy();
|
||||
expect(LabelSelector.match('test in 1, 2, 3', {test1: '1'})).toBeFalsy();
|
||||
});
|
||||
|
||||
test('notIn', () => {
|
||||
expect(LabelSelector.match('test notin 1, 2, 3', {test: '1'})).toBeFalsy();
|
||||
expect(LabelSelector.match('test notin 1, 2, 3', {test: '4'})).toBeTruthy();
|
||||
expect(LabelSelector.match('test notin 1, 2, 3', {test1: '1'})).toBeTruthy();
|
||||
});
|
||||
|
||||
test('equal', () => {
|
||||
expect(LabelSelector.match('test=hello', {test: 'hello'})).toBeTruthy();
|
||||
expect(LabelSelector.match('test=world', {test: 'hello'})).toBeFalsy();
|
||||
expect(LabelSelector.match('test==hello', {test: 'hello'})).toBeTruthy();
|
||||
});
|
||||
|
||||
test('notEqual', () => {
|
||||
expect(LabelSelector.match('test!=hello', {test: 'hello'})).toBeFalsy();
|
||||
expect(LabelSelector.match('test!=world', {test: 'hello'})).toBeTruthy();
|
||||
});
|
||||
|
||||
test('greaterThen', () => {
|
||||
expect(LabelSelector.match('test gt 1', {test: '2'})).toBeTruthy();
|
||||
expect(LabelSelector.match('test gt 3', {test: '2'})).toBeFalsy();
|
||||
});
|
||||
|
||||
test('lessThen', () => {
|
||||
expect(LabelSelector.match('test lt 1', {test: '2'})).toBeFalsy();
|
||||
expect(LabelSelector.match('test lt 3', {test: '2'})).toBeTruthy();
|
||||
});
|
||||
39
ui/src/app/applications/components/label-selector.ts
Normal file
39
ui/src/app/applications/components/label-selector.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
type operatorFn = (labels: {[name: string]: string}, key: string, values: string[]) => boolean;
|
||||
|
||||
const operators: {[type: string]: operatorFn} = {
|
||||
'!=': (labels, key, values) => labels[key] !== values[0],
|
||||
'==': (labels, key, values) => labels[key] === values[0],
|
||||
'=': (labels, key, values) => labels[key] === values[0],
|
||||
'notin': (labels, key, values) => !values.includes(labels[key]),
|
||||
'in': (labels, key, values) => values.includes(labels[key]),
|
||||
'gt': (labels, key, values) => parseFloat(labels[key]) > parseFloat(values[0]),
|
||||
'lt': (labels, key, values) => parseFloat(labels[key]) < parseFloat(values[0])
|
||||
};
|
||||
|
||||
function split(input: string, delimiter: string): string[] {
|
||||
return input
|
||||
.split(delimiter)
|
||||
.map(part => part.trim())
|
||||
.filter(part => part !== '');
|
||||
}
|
||||
|
||||
export type LabelSelector = (labels: {[name: string]: string}) => boolean;
|
||||
|
||||
export function parse(selector: string): LabelSelector {
|
||||
for (const type of Object.keys(operators)) {
|
||||
const operator = operators[type];
|
||||
const parts = split(selector, type);
|
||||
if (parts.length > 1) {
|
||||
const values = split(parts[1], ',');
|
||||
return labels => operator(labels, parts[0], values);
|
||||
}
|
||||
}
|
||||
if (selector.startsWith('!')) {
|
||||
return labels => !labels.hasOwnProperty(selector.slice(1));
|
||||
}
|
||||
return labels => labels.hasOwnProperty(selector);
|
||||
}
|
||||
|
||||
export function match(selector: string, labels: {[name: string]: string}): boolean {
|
||||
return parse(selector)(labels || {});
|
||||
}
|
||||
Loading…
Reference in a new issue