diff --git a/src/gitops/components/imageupdater/ImageUpdaterList.tsx b/src/gitops/components/imageupdater/ImageUpdaterList.tsx index 33ae2b797..5d05f5874 100644 --- a/src/gitops/components/imageupdater/ImageUpdaterList.tsx +++ b/src/gitops/components/imageupdater/ImageUpdaterList.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom-v5-compat'; -import * as YamlFormatter from 'yaml'; import ActionsDropdown from '@gitops/utils/components/ActionDropDown/ActionDropDown'; import { modelToGroupVersionKind } from '@gitops/utils/utils'; @@ -43,7 +42,7 @@ import { useGitOpsDataViewSort, useGitOpsListPagePagination, } from '../shared/DataView'; -import { filterByConsoleNameAndLabels, parseLabelFilterParam } from '../shared/listPageTextFilters'; +import { filterByConsoleNameAndLabels, getLabelsSortKey, parseLabelFilterParam } from '../shared/listPageTextFilters'; import MetadataLabels from '../shared/MetadataLabels'; import { useImageUpdaterActionsProvider } from './hooks/useImageUpdaterActionsProvider'; @@ -269,8 +268,8 @@ export const sortData = ( bValue = b.status?.conditions?.find((c) => c.type === 'Ready')?.status || ''; break; case 'labels': - aValue = YamlFormatter.stringify(a.metadata?.labels || {}); - bValue = YamlFormatter.stringify(b.metadata?.labels || {}); + aValue = getLabelsSortKey(a.metadata?.labels); + bValue = getLabelsSortKey(b.metadata?.labels); break; default: return 0; diff --git a/src/gitops/components/project/ProjectList.tsx b/src/gitops/components/project/ProjectList.tsx index fa3818c99..1065652bf 100644 --- a/src/gitops/components/project/ProjectList.tsx +++ b/src/gitops/components/project/ProjectList.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom-v5-compat'; -import * as YamlFormatter from 'yaml'; import ActionsDropdown from '@gitops/utils/components/ActionDropDown/ActionDropDown'; import { modelToGroupVersionKind, modelToRef } from '@gitops/utils/utils'; @@ -41,7 +40,7 @@ import { useGitOpsDataViewSort, useGitOpsListPagePagination, } from '../shared/DataView'; -import { filterByConsoleNameAndLabels, parseLabelFilterParam } from '../shared/listPageTextFilters'; +import { filterByConsoleNameAndLabels, getLabelsSortKey, parseLabelFilterParam } from '../shared/listPageTextFilters'; import { MetadataLabels } from '../shared/MetadataLabels/MetadataLabels'; import { useProjectActionsProvider } from './hooks/useProjectActionsProvider'; @@ -375,8 +374,8 @@ export const sortData = ( bValue = getApplicationsCount(b, applications, appsLoaded); break; case 'labels': - aValue = YamlFormatter.stringify(a.metadata?.labels || {}); - bValue = YamlFormatter.stringify(b.metadata?.labels || {}); + aValue = getLabelsSortKey(a.metadata?.labels); + bValue = getLabelsSortKey(b.metadata?.labels); break; case 'last-updated': aValue = getLastUpdateTimestamp(a) || ''; diff --git a/src/gitops/components/rollout/RolloutList.tsx b/src/gitops/components/rollout/RolloutList.tsx index 82f7aa5f3..79409cab8 100644 --- a/src/gitops/components/rollout/RolloutList.tsx +++ b/src/gitops/components/rollout/RolloutList.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { Link } from 'react-router-dom-v5-compat'; -import * as YamlFormatter from 'yaml'; import { AppProjectKind } from '@gitops/models/AppProjectModel'; import ActionsDropdown from '@gitops/utils/components/ActionDropDown/ActionDropDown'; @@ -44,6 +43,7 @@ import { import { filterByConsoleNameAndLabels, filterResourcesByLabelQuery, + getLabelsSortKey, parseLabelFilterParam, } from '../shared/listPageTextFilters'; import { MetadataLabels } from '../shared/MetadataLabels/MetadataLabels'; @@ -265,8 +265,8 @@ export const sortData = ( bValue = b.status?.readyReplicas || ''; break; case 'labels': - aValue = YamlFormatter.stringify(a.metadata?.labels || {}); - bValue = YamlFormatter.stringify(b.metadata?.labels || {}); + aValue = getLabelsSortKey(a.metadata?.labels); + bValue = getLabelsSortKey(b.metadata?.labels); break; case 'selector': aValue = a.status?.selector || ''; diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index 9d864b68a..ee8e78cce 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import * as YamlFormatter from 'yaml'; import { ApplicationSetKind } from '@gitops/models/ApplicationSetModel'; import { @@ -66,6 +65,7 @@ import { import { filterByConsoleNameAndLabels, filterResourcesByLabelQuery, + getLabelsSortKey, parseLabelFilterParam, } from './listPageTextFilters'; import MetadataLabels from './MetadataLabels'; @@ -362,8 +362,8 @@ export const sortData = ( bValue = b.status?.sync?.revision || ''; break; case 'labels': - aValue = YamlFormatter.stringify(a.metadata?.labels || {}); - bValue = YamlFormatter.stringify(b.metadata?.labels || {}); + aValue = getLabelsSortKey(a.metadata?.labels); + bValue = getLabelsSortKey(b.metadata?.labels); break; case 'project': aValue = a.spec?.project || ''; diff --git a/src/gitops/components/shared/ApplicationSetList.tsx b/src/gitops/components/shared/ApplicationSetList.tsx index 2ae7f0dba..49edec67d 100644 --- a/src/gitops/components/shared/ApplicationSetList.tsx +++ b/src/gitops/components/shared/ApplicationSetList.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import * as YamlFormatter from 'yaml'; import { K8sResourceCommon, @@ -50,6 +49,7 @@ import { import { filterByConsoleNameAndLabels, filterResourcesByLabelQuery, + getLabelsSortKey, parseLabelFilterParam, } from './listPageTextFilters'; import MetadataLabels from './MetadataLabels'; @@ -536,8 +536,8 @@ export const sortData = ( bValue = getAppSetGeneratorCount(b); break; case 'labels': - aValue = YamlFormatter.stringify(a.metadata?.labels || {}); - bValue = YamlFormatter.stringify(b.metadata?.labels || {}); + aValue = getLabelsSortKey(a.metadata?.labels); + bValue = getLabelsSortKey(b.metadata?.labels); break; case 'created-at': aValue = new Date(a.metadata?.creationTimestamp || 0).getTime(); diff --git a/src/gitops/components/shared/listPageTextFilters.test.ts b/src/gitops/components/shared/listPageTextFilters.test.ts index 2698f8107..0d1c4b609 100644 --- a/src/gitops/components/shared/listPageTextFilters.test.ts +++ b/src/gitops/components/shared/listPageTextFilters.test.ts @@ -3,6 +3,7 @@ import { filterByConsoleNameAndLabels, filterResourcesByLabelQuery, fuzzySearch, + getLabelsSortKey, matchesConsoleLabelFilter, matchesConsoleNameFilter, matchesLabelSearchQuery, @@ -83,6 +84,25 @@ describe('matchesLabelSearchQuery (GitOps q param)', () => { }); }); +describe('getLabelsSortKey', () => { + it('returns the same key regardless of label key insertion order', () => { + const first = { app: 'foo', env: 'prod' }; + const second = { env: 'prod', app: 'foo' }; + expect(getLabelsSortKey(first)).toBe('app=foo,env=prod'); + expect(getLabelsSortKey(second)).toBe('app=foo,env=prod'); + expect(getLabelsSortKey(first)).toBe(getLabelsSortKey(second)); + }); + + it('sorts keys alphabetically', () => { + expect(getLabelsSortKey({ z: '1', a: '2', m: '3' })).toBe('a=2,m=3,z=1'); + }); + + it('returns an empty string when labels are missing or empty', () => { + expect(getLabelsSortKey(undefined)).toBe(''); + expect(getLabelsSortKey({})).toBe(''); + }); +}); + describe('name and label filters with pagination', () => { const items = [ resource('app-matrix-2-staging-us-east', { app: 'matrix' }), diff --git a/src/gitops/components/shared/listPageTextFilters.ts b/src/gitops/components/shared/listPageTextFilters.ts index 50ee5760c..dd9c0a514 100644 --- a/src/gitops/components/shared/listPageTextFilters.ts +++ b/src/gitops/components/shared/listPageTextFilters.ts @@ -101,3 +101,9 @@ export const filterResourcesByLabelQuery = ( } return (items ?? []).filter((item) => matchesLabelSearchQuery(query, item.metadata?.labels)); }; + +export const getLabelsSortKey = (labels: Record | undefined): string => + Object.keys(labels ?? {}) + .sort() + .map((key) => `${key}=${labels?.[key] ?? ''}`) + .join(',');