diff --git a/ui/.prettierrc b/ui/.prettierrc index f1d6305659..0abdae70b5 100644 --- a/ui/.prettierrc +++ b/ui/.prettierrc @@ -4,5 +4,6 @@ "printWidth": 180, "singleQuote": true, "tabWidth": 4, - "jsxBracketSameLine": true + "jsxBracketSameLine": true, + "quoteProps": "consistent" } 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 9f5b8633cb..79023a1607 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 @@ -193,7 +193,7 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod
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}}> diff --git a/ui/src/app/applications/components/applications-list/applications-list.tsx b/ui/src/app/applications/components/applications-list/applications-list.tsx index ddfaf43aee..defcf8186c 100644 --- a/ui/src/app/applications/components/applications-list/applications-list.tsx +++ b/ui/src/app/applications/components/applications-list/applications-list.tsx @@ -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 { - return Observable.fromPromise(services.applications.list([], {fields: APP_LIST_FIELDS, selector})).flatMap(applications => +function loadApplications(): Observable { + 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<{}>) => { {pref => ( loadApplications(selector)} + load={() => loadApplications()} loadingRenderer={() => (
@@ -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(',') }); }} /> diff --git a/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx b/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx index af8b184e9f..97a13d14ec 100644 --- a/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx +++ b/ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx @@ -20,7 +20,7 @@ export const ApplicationsSyncPanel = ({show, apps, hide}: {show: boolean; apps:
+ {' '} diff --git a/ui/src/app/applications/components/label-selector.test.ts b/ui/src/app/applications/components/label-selector.test.ts new file mode 100644 index 0000000000..3b436ed258 --- /dev/null +++ b/ui/src/app/applications/components/label-selector.test.ts @@ -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(); +}); diff --git a/ui/src/app/applications/components/label-selector.ts b/ui/src/app/applications/components/label-selector.ts new file mode 100644 index 0000000000..751ae6e418 --- /dev/null +++ b/ui/src/app/applications/components/label-selector.ts @@ -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 || {}); +}