diff --git a/locales/en/plugin__gitops-plugin.json b/locales/en/plugin__gitops-plugin.json index a11904d03..855f753dc 100644 --- a/locales/en/plugin__gitops-plugin.json +++ b/locales/en/plugin__gitops-plugin.json @@ -102,6 +102,7 @@ "Argo CD project that this ApplicationSet belongs to.": "Argo CD project that this ApplicationSet belongs to.", "Git repository URL where the ApplicationSet configuration is stored.": "Git repository URL where the ApplicationSet configuration is stored.", "Progressive Sync Step {{x}}": "Progressive Sync Step {{x}}", + "Application": "Application", "Applications": "Applications", "Show all match expressions": "Show all match expressions", "Edit ApplicationSet": "Edit ApplicationSet", @@ -342,6 +343,14 @@ "Created at": "Created at", "Time is a wrapper around time. Time which supports correct marshaling to YAML and JSON.": "Time is a wrapper around time. Time which supports correct marshaling to YAML and JSON.", "Owner references link this resource to its parent object. For example, Applications generated by an ApplicationSet will have that ApplicationSet as their owner. This relationship enables proper resource lifecycle management and garbage collection.": "Owner references link this resource to its parent object. For example, Applications generated by an ApplicationSet will have that ApplicationSet as their owner. This relationship enables proper resource lifecycle management and garbage collection.", + "Manage columns": "Manage columns", + "Selected columns will appear in the table.": "Selected columns will appear in the table.", + "The namespace column is only shown when in \"All projects\"": "The namespace column is only shown when in \"All projects\"", + "Default {{resourceKind}} columns": "Default {{resourceKind}} columns", + "Additional columns": "Additional columns", + "Restore default columns": "Restore default columns", + "Save": "Save", + "Cancel": "Cancel", "Pagination": "Pagination", "Go to first page": "Go to first page", "Go to previous page": "Go to previous page", diff --git a/locales/es/plugin__gitops-plugin.json b/locales/es/plugin__gitops-plugin.json index 8350ce242..26423f9b1 100644 --- a/locales/es/plugin__gitops-plugin.json +++ b/locales/es/plugin__gitops-plugin.json @@ -102,6 +102,7 @@ "Argo CD project that this ApplicationSet belongs to.": "Proyecto Argo CD al que pertenece este ApplicationSet.", "Git repository URL where the ApplicationSet configuration is stored.": "URL del repositorio Git en el que se almacena la configuración de ApplicationSet.", "Progressive Sync Step {{x}}": "Paso de sincronización progresiva {{x}}", + "Application": "Aplicación", "Applications": "Aplicaciones", "Show all match expressions": "Mostrar todas las expresiones de coincidencia", "Edit ApplicationSet": "Editar ApplicationSet", @@ -342,6 +343,14 @@ "Created at": "Creado en", "Time is a wrapper around time. Time which supports correct marshaling to YAML and JSON.": "Time es una envoltura de time. Tiempo que admite la serialización correcta a YAML y JSON.", "Owner references link this resource to its parent object. For example, Applications generated by an ApplicationSet will have that ApplicationSet as their owner. This relationship enables proper resource lifecycle management and garbage collection.": "Las referencias del propietario vinculan este recurso con su objeto principal. Por ejemplo, las aplicaciones generadas por un ApplicationSet tendrán a ese ApplicationSet como propietario. Esta relación permite una gestión adecuada del ciclo de vida de los recursos y la recolección de basura.", + "Manage columns": "Administrar columnas", + "Selected columns will appear in the table.": "Las columnas seleccionadas aparecerán en la tabla.", + "The namespace column is only shown when in \"All projects\"": "La columna de espacio de nombres solo se muestra en \"Todos los proyectos\"", + "Default {{resourceKind}} columns": "Columnas {{resourceKind}} predeterminadas", + "Additional columns": "Columnas adicionales", + "Restore default columns": "Restaurar columnas predeterminadas", + "Save": "Guardar", + "Cancel": "Cancelar", "Pagination": "Pagination", "Go to first page": "Go to first page", "Go to previous page": "Go to previous page", diff --git a/locales/fr/plugin__gitops-plugin.json b/locales/fr/plugin__gitops-plugin.json index 7684479e0..0a17c84d4 100644 --- a/locales/fr/plugin__gitops-plugin.json +++ b/locales/fr/plugin__gitops-plugin.json @@ -102,6 +102,7 @@ "Argo CD project that this ApplicationSet belongs to.": "Projet Argo CD auquel appartient cet ApplicationSet.", "Git repository URL where the ApplicationSet configuration is stored.": "URL du dépôt Git où est stockée la configuration ApplicationSet.", "Progressive Sync Step {{x}}": "Étape de synchronisation progressive {{x}}", + "Application": "Application", "Applications": "Applications", "Show all match expressions": "Afficher toutes les expressions de correspondance", "Edit ApplicationSet": "Modifier l'ensemble d'applications", @@ -342,6 +343,14 @@ "Created at": "Heure de création", "Time is a wrapper around time. Time which supports correct marshaling to YAML and JSON.": "Le temps est un emballage autour du temps. Temps permettant une sérialisation correcte en YAML et JSON.", "Owner references link this resource to its parent object. For example, Applications generated by an ApplicationSet will have that ApplicationSet as their owner. This relationship enables proper resource lifecycle management and garbage collection.": "Les références de propriétaire relient cette ressource à son objet parent. Par exemple, les applications générées par un ApplicationSet auront cet ApplicationSet comme propriétaire. Cette relation permet une gestion adéquate du cycle de vie des ressources et une collecte efficace des déchets.", + "Manage columns": "Gérer les colonnes", + "Selected columns will appear in the table.": "Les colonnes sélectionnées apparaîtront dans le tableau.", + "The namespace column is only shown when in \"All projects\"": "La colonne d’espace de noms n’est affichée que dans « Tous les projets ».", + "Default {{resourceKind}} columns": "Colonnes {{resourceKind}} par défaut", + "Additional columns": "Colonnes supplémentaires", + "Restore default columns": "Restaurer les colonnes par défaut", + "Save": "Enregistrer", + "Cancel": "Annuler", "Pagination": "Pagination", "Go to first page": "Go to first page", "Go to previous page": "Go to previous page", diff --git a/locales/ja/plugin__gitops-plugin.json b/locales/ja/plugin__gitops-plugin.json index 9e39988ee..a46261ae5 100644 --- a/locales/ja/plugin__gitops-plugin.json +++ b/locales/ja/plugin__gitops-plugin.json @@ -102,6 +102,7 @@ "Argo CD project that this ApplicationSet belongs to.": "この ApplicationSet が属する Argo CD プロジェクト。", "Git repository URL where the ApplicationSet configuration is stored.": "ApplicationSet の設定が保存されている Git リポジトリーの URL。", "Progressive Sync Step {{x}}": "Progressive Sync ステップ {{x}}", + "Application": "アプリケーション", "Applications": "アプリケーション", "Show all match expressions": "すべてのマッチ式の表示", "Edit ApplicationSet": "ApplicationSet の編集", @@ -342,6 +343,14 @@ "Created at": "作成日時", "Time is a wrapper around time. Time which supports correct marshaling to YAML and JSON.": "Time は time のラッパーです。YAML と JSON への正しいマーシャリングをサポートする時間。", "Owner references link this resource to its parent object. For example, Applications generated by an ApplicationSet will have that ApplicationSet as their owner. This relationship enables proper resource lifecycle management and garbage collection.": "オーナーリファレンスは、このリソースをその親オブジェクトにリンクします。たとえば、ApplicationSet によって生成されたアプリケーションは、その ApplicationSet を所有者として持つことになります。この関係により、適切なリソースライフサイクル管理とガベージコレクションが可能になります。", + "Manage columns": "列の管理", + "Selected columns will appear in the table.": "選択した列がテーブルに表示されます。", + "The namespace column is only shown when in \"All projects\"": "namespace 列は \"すべてのプロジェクト\" の場合にのみ表示されます", + "Default {{resourceKind}} columns": "デフォルトの {{resourceKind}} 列", + "Additional columns": "追加の列", + "Restore default columns": "デフォルト列の復元", + "Save": "保存", + "Cancel": "キャンセル", "Pagination": "Pagination", "Go to first page": "Go to first page", "Go to previous page": "Go to previous page", diff --git a/locales/ko/plugin__gitops-plugin.json b/locales/ko/plugin__gitops-plugin.json index 5319036c3..ebf95c3bc 100644 --- a/locales/ko/plugin__gitops-plugin.json +++ b/locales/ko/plugin__gitops-plugin.json @@ -102,6 +102,7 @@ "Argo CD project that this ApplicationSet belongs to.": "이 ApplicationSet이 속한 Argo CD 프로젝트입니다.", "Git repository URL where the ApplicationSet configuration is stored.": "ApplicationSet 구성이 저장되는 Git 리포지토리 URL입니다.", "Progressive Sync Step {{x}}": "점진적 동기화 단계 {{x}}", + "Application": "애플리케이션", "Applications": "애플리케이션", "Show all match expressions": "모든 일치 표현식 표시", "Edit ApplicationSet": "ApplicationSet 편집", @@ -342,6 +343,14 @@ "Created at": "생성 시간", "Time is a wrapper around time. Time which supports correct marshaling to YAML and JSON.": "Time은 시간을 나타내는 래퍼입니다. Time 타입은 올바른 YAML 및 JSON 마샬링을 지원합니다.", "Owner references link this resource to its parent object. For example, Applications generated by an ApplicationSet will have that ApplicationSet as their owner. This relationship enables proper resource lifecycle management and garbage collection.": "소유자 참조는 이 리소스를 상위 객체에 연결합니다. 예를 들어 ApplicationSet에서 생성된 애플리케이션은 해당 ApplicationSet이 소유자로 지정됩니다. 이 관계를 통해 적절한 리소스 라이프사이클 관리 및 가비지 컬렉션이 가능합니다.", + "Manage columns": "열 관리", + "Selected columns will appear in the table.": "선택한 열이 테이블에 표시됩니다.", + "The namespace column is only shown when in \"All projects\"": "네임 스페이스 열은 \"All projects\"에 있을 때만 표시됩니다.", + "Default {{resourceKind}} columns": "기본 {{resourceKind}} 열", + "Additional columns": "추가 열", + "Restore default columns": "기본 열 복원", + "Save": "저장", + "Cancel": "취소", "Pagination": "Pagination", "Go to first page": "Go to first page", "Go to previous page": "Go to previous page", diff --git a/locales/zh/plugin__gitops-plugin.json b/locales/zh/plugin__gitops-plugin.json index 3db9df646..386626779 100644 --- a/locales/zh/plugin__gitops-plugin.json +++ b/locales/zh/plugin__gitops-plugin.json @@ -102,6 +102,7 @@ "Argo CD project that this ApplicationSet belongs to.": "此 ApplicationSet 所属的 Argo CD 项目。", "Git repository URL where the ApplicationSet configuration is stored.": "存储 ApplicationSet 配置的 Git 存储库 URL。", "Progressive Sync Step {{x}}": "渐进式同步步骤 {{x}}", + "Application": "应用程序", "Applications": "应用程序", "Show all match expressions": "显示所有匹配表达式", "Edit ApplicationSet": "编辑 ApplicationSet", @@ -342,6 +343,14 @@ "Created at": "创建于", "Time is a wrapper around time. Time which supports correct marshaling to YAML and JSON.": "Time 是对 time 的封装。支持正确进行 YAML 和 JSON 编组的 Time 类型。", "Owner references link this resource to its parent object. For example, Applications generated by an ApplicationSet will have that ApplicationSet as their owner. This relationship enables proper resource lifecycle management and garbage collection.": "所有者引用将此资源关联到其父对象。例如,一个 ApplicationSet 生成的应用程序会将 ApplicationSet 作为所有者。这种关联关系可实现正确的资源生命周期管理和垃圾回收。", + "Manage columns": "管理列", + "Selected columns will appear in the table.": "选定的列将出现在表中。", + "The namespace column is only shown when in \"All projects\"": "仅在“所有项目”中显示名称空间列", + "Default {{resourceKind}} columns": "默认 {{resourceKind}} 列", + "Additional columns": "额外列", + "Restore default columns": "恢复默认列", + "Save": "保存", + "Cancel": "取消", "Pagination": "Pagination", "Go to first page": "Go to first page", "Go to previous page": "Go to previous page", diff --git a/src/gitops/components/imageupdater/ImageUpdaterList.tsx b/src/gitops/components/imageupdater/ImageUpdaterList.tsx index d65bce4bd..73b2cd6f1 100644 --- a/src/gitops/components/imageupdater/ImageUpdaterList.tsx +++ b/src/gitops/components/imageupdater/ImageUpdaterList.tsx @@ -19,6 +19,7 @@ import { } from '@openshift-console/dynamic-plugin-sdk'; import { ErrorState } from '@patternfly/react-component-groups'; import { EmptyState, EmptyStateBody } from '@patternfly/react-core'; +import type { DataViewTd } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import { CubesIcon } from '@patternfly/react-icons'; import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; @@ -32,12 +33,26 @@ import { ShowOperandsInAllNamespacesRadioGroup, useShowOperandsInAllNamespaces, } from '../shared/AllNamespaces'; +import { + type GitOpsManagedColumn, + NAMESPACE_COLUMN_ID, + useGitOpsColumnManagement, +} from '../shared/ColumnManagement'; import { GitOpsDataViewTable, useGitOpsDataViewSort, useGitOpsListPagePagination, } from '../shared/DataView'; -import { filterByConsoleNameAndLabels, getLabelsSortKey, parseLabelFilterParam } from '../shared/listPageTextFilters'; +import { GitOpsListPageToolbar } from '../shared/GitOpsListPageToolbar'; +import { + getImageUpdaterManagedColumns, + IMAGE_UPDATER_LIST_COLUMN_MANAGEMENT_ID, +} from '../shared/imageUpdaterListColumns'; +import { + filterByConsoleNameAndLabels, + getLabelsSortKey, + parseLabelFilterParam, +} from '../shared/listPageTextFilters'; import MetadataLabels from '../shared/MetadataLabels'; import { useImageUpdaterActionsProvider } from './hooks/useImageUpdaterActionsProvider'; @@ -67,19 +82,23 @@ const ImageUpdaterList: React.FC = ({ namespace: effectiveNamespace, }); - const showNamespaceColumn = !effectiveNamespace || effectiveNamespace === ''; - const columnSortConfig = React.useMemo(() => { - return [ - 'name', - ...(showNamespaceColumn ? ['namespace'] : []), - 'apps', - 'images', - 'last-checked', - 'ready', - 'labels', - 'actions', - ].map((key) => ({ key })); - }, [showNamespaceColumn]); + const { t } = useTranslation('plugin__gitops-plugin'); + const includeNamespaceColumn = !effectiveNamespace || effectiveNamespace === ''; + const managedColumns = React.useMemo(() => getImageUpdaterManagedColumns(true, t), [t]); + + const { activeColumnIds, columnManagement, filterDataView } = useGitOpsColumnManagement({ + columnManagementID: IMAGE_UPDATER_LIST_COLUMN_MANAGEMENT_ID, + columns: managedColumns, + resourceType: t('ImageUpdaters'), + includeNamespaceColumn, + showNamespaceHelp: includeNamespaceColumn, + }); + + const columnSortConfig = React.useMemo( + () => + managedColumns.filter((column) => !column.alwaysShown).map((column) => ({ key: column.id })), + [managedColumns], + ); const { searchParams, sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); @@ -89,9 +108,7 @@ const ImageUpdaterList: React.FC = ({ const nameQuery = searchParams.get('name') || ''; const labelsParam = searchParams.get('labels') || ''; - const { t } = useTranslation('plugin__gitops-plugin'); - - const columnsDV = useColumnsDV(effectiveNamespace, getSortParams); + const columnsDV = useColumnsDV(managedColumns, getSortParams, columnSortConfig); const sortedItems = React.useMemo(() => { return sortData(imageUpdaters as ImageUpdaterKind[], sortBy, direction); }, [imageUpdaters, sortBy, direction]); @@ -129,7 +146,15 @@ const ImageUpdaterList: React.FC = ({ namespace: effectiveNamespace, searchParams, }); - const rows = useImageUpdaterRowsDV(pagedItems, effectiveNamespace); + const rows = useImageUpdaterRowsDV(pagedItems, managedColumns); + const columnIds = React.useMemo( + () => managedColumns.map((column) => column.id), + [managedColumns], + ); + const { columns: visibleColumnsDV, rows: visibleRows } = React.useMemo( + () => filterDataView(columnsDV, rows, columnIds, activeColumnIds), + [filterDataView, columnsDV, rows, columnIds, activeColumnIds], + ); const hasItems = React.useMemo(() => { return sortedItems.length > 0; @@ -156,7 +181,7 @@ const ImageUpdaterList: React.FC = ({ const empty = ( - + = ({ const error = loadError && ( - + = ({ ); - const isEmptyState = !loadError && rows.length === 0; + const isEmptyState = !loadError && visibleRows.length === 0; + + const listPageFilter = !hideNameLabelFilters && hasItems && ( + + ); return (
@@ -202,18 +237,12 @@ const ImageUpdaterList: React.FC = ({ )} - {!hideNameLabelFilters && hasItems && ( - + {hasItems && ( + )} ThProps['sort'], + columnSortConfig: { key: string }[], ): DataViewTh[] => { - const showNamespace = !namespace || namespace === ''; - const i: number = showNamespace ? 1 : 0; const { t } = useTranslation('plugin__gitops-plugin'); - const columns: DataViewTh[] = [ - { - cell: t('Name'), - props: { - 'aria-label': 'name', - className: 'pf-m-width-30', - sort: getSortParams(0), - style: { minWidth: '200px' }, - }, - }, - ...(showNamespace - ? [ - { - cell: t('Namespace'), - props: { - 'aria-label': 'namespace', - className: 'pf-m-width-15', - sort: getSortParams(1), - style: { minWidth: '150px' }, - }, - }, - ] - : []), - { - cell: t('Apps'), - props: { - 'aria-label': 'apps', - className: 'pf-m-width-20', - sort: getSortParams(1 + i), - }, - }, - { - cell: t('Images'), - props: { - 'aria-label': 'images', - className: 'pf-m-width-20', - sort: getSortParams(2 + i), - }, - }, - { - cell: t('Last Checked'), - props: { - 'aria-label': 'last checked', - className: 'pf-m-width-20', - sort: getSortParams(3 + i), - }, - }, - { - cell: t('Ready'), - props: { - 'aria-label': 'ready', - className: 'pf-m-width-10', - sort: getSortParams(4 + i), - }, - }, - { - cell: t('Labels'), + + const titleById: Record = { + name: t('Name'), + [NAMESPACE_COLUMN_ID]: t('Namespace'), + apps: t('Apps'), + images: t('Images'), + 'last-checked': t('Last Checked'), + ready: t('Ready'), + labels: t('Labels'), + actions: '', + }; + + const widthById: Record = { + name: 'pf-m-width-30', + [NAMESPACE_COLUMN_ID]: 'pf-m-width-15', + apps: 'pf-m-width-20', + images: 'pf-m-width-20', + 'last-checked': 'pf-m-width-20', + ready: 'pf-m-width-10', + labels: 'pf-m-width-10', + }; + + const styleById: Record = { + name: { minWidth: '200px' }, + [NAMESPACE_COLUMN_ID]: { minWidth: '150px' }, + }; + + return managedColumns.map((column) => { + if (column.id === 'actions') { + return { + cell: '', + props: { 'aria-label': 'actions' }, + }; + } + const sortIndex = columnSortConfig.findIndex((entry) => entry.key === column.id); + return { + cell: titleById[column.id] ?? column.title, props: { - 'aria-label': 'labels', - className: 'pf-m-width-10', - sort: getSortParams(5 + i), + 'aria-label': column.id, + className: widthById[column.id], + ...(styleById[column.id] ? { style: styleById[column.id] } : {}), + ...(sortIndex >= 0 ? { sort: getSortParams(sortIndex) } : {}), }, - }, - { - cell: '', - props: { 'aria-label': 'actions' }, - }, - ]; - - return columns; + }; + }); }; export const useImageUpdaterRowsDV = ( imageUpdaterList: ImageUpdaterKind[], - namespace: string | null | undefined, + managedColumns: GitOpsManagedColumn[], ): DataViewTr[] => { const rows: DataViewTr[] = []; if (imageUpdaterList === undefined || imageUpdaterList.length === 0) { return rows; } - const showNamespace = !namespace || namespace === ''; imageUpdaterList.forEach((obj, index) => { const readyCondition = obj.status?.conditions?.find((c) => c.type === 'Ready'); const isReady = readyCondition?.status === 'True'; - rows.push([ - { + const cellsById: Record = { + name: { cell: (
, - id: obj.metadata.namespace, - dataLabel: 'Namespace', - }, - ] - : []), - { + [NAMESPACE_COLUMN_ID]: { + cell: , + id: obj.metadata.namespace, + dataLabel: 'Namespace', + }, + apps: { id: 'apps', cell: obj.status?.applicationsMatched != null ? String(obj.status.applicationsMatched) : '-', dataLabel: 'Apps', }, - { + images: { id: 'images', cell: obj.status?.imagesManaged != null ? String(obj.status.imagesManaged) : '-', dataLabel: 'Images', }, - { + 'last-checked': { id: 'last-checked', cell: obj.status?.lastCheckedAt ? (
@@ -418,12 +416,12 @@ export const useImageUpdaterRowsDV = ( ), dataLabel: 'Last Checked', }, - { + ready: { id: 'ready', cell: readyCondition ? String(isReady) : '-', dataLabel: 'Ready', }, - { + labels: { id: 'labels', dataLabel: 'Labels', cell: ( @@ -442,12 +440,14 @@ export const useImageUpdaterRowsDV = (
), }, - { + actions: { id: 'actions-' + index, cell: , props: { className: 'gitops-imageupdater-list__actions-cell' }, }, - ]); + }; + + rows.push(managedColumns.map((column) => cellsById[column.id])); }); return rows; }; diff --git a/src/gitops/components/project/ProjectList.tsx b/src/gitops/components/project/ProjectList.tsx index 44493112d..8dc0dccc8 100644 --- a/src/gitops/components/project/ProjectList.tsx +++ b/src/gitops/components/project/ProjectList.tsx @@ -19,6 +19,7 @@ import { import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; import { ErrorState } from '@patternfly/react-component-groups'; import { EmptyState, EmptyStateBody } from '@patternfly/react-core'; +import type { DataViewTd } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import { CubesIcon } from '@patternfly/react-icons'; import { ThProps } from '@patternfly/react-table'; @@ -30,13 +31,27 @@ import { ShowOperandsInAllNamespacesRadioGroup, useShowOperandsInAllNamespaces, } from '../shared/AllNamespaces'; +import { + type GitOpsManagedColumn, + NAMESPACE_COLUMN_ID, + useGitOpsColumnManagement, +} from '../shared/ColumnManagement'; import { GitOpsDataViewTable, useGitOpsDataViewSort, useGitOpsListPagePagination, } from '../shared/DataView'; -import { filterByConsoleNameAndLabels, getLabelsSortKey, parseLabelFilterParam } from '../shared/listPageTextFilters'; +import { GitOpsListPageToolbar } from '../shared/GitOpsListPageToolbar'; +import { + filterByConsoleNameAndLabels, + getLabelsSortKey, + parseLabelFilterParam, +} from '../shared/listPageTextFilters'; import { MetadataLabels } from '../shared/MetadataLabels/MetadataLabels'; +import { + getProjectManagedColumns, + PROJECT_LIST_COLUMN_MANAGEMENT_ID, +} from '../shared/projectListColumns'; import { useProjectActionsProvider } from './hooks/useProjectActionsProvider'; @@ -83,17 +98,23 @@ const ProjectList: React.FC = ({ namespace, }); - const columnSortConfig = React.useMemo(() => { - return [ - 'name', - ...(!listAllNamespaces || !namespace || namespace === '' ? ['namespace'] : []), - 'description', - 'applications', - 'labels', - 'last-updated', - 'actions', - ].map((key) => ({ key })); - }, [listAllNamespaces, namespace]); + const { t } = useTranslation('plugin__gitops-plugin'); + const includeNamespaceColumn = !namespace || namespace === ''; + const managedColumns = React.useMemo(() => getProjectManagedColumns(true, t), [t]); + + const { activeColumnIds, columnManagement, filterDataView } = useGitOpsColumnManagement({ + columnManagementID: PROJECT_LIST_COLUMN_MANAGEMENT_ID, + columns: managedColumns, + resourceType: t('AppProjects'), + includeNamespaceColumn, + showNamespaceHelp: includeNamespaceColumn, + }); + + const columnSortConfig = React.useMemo( + () => + managedColumns.filter((column) => !column.alwaysShown).map((column) => ({ key: column.id })), + [managedColumns], + ); const { searchParams, sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); @@ -103,9 +124,7 @@ const ProjectList: React.FC = ({ const nameQuery = searchParams.get('name') || ''; const labelsParam = searchParams.get('labels') || ''; - const { t } = useTranslation('plugin__gitops-plugin'); - - const columnsDV = useColumnsDV(namespace, getSortParams); + const columnsDV = useColumnsDV(managedColumns, getSortParams, columnSortConfig); const sortedProjects = React.useMemo(() => { return sortData(appProjects as AppProjectKind[], sortBy, direction, applications, appsLoaded); }, [appProjects, sortBy, direction, applications, appsLoaded]); @@ -149,8 +168,15 @@ const ProjectList: React.FC = ({ namespace, searchParams, }); - const rows = useProjectsRowsDV(pagedItems, namespace, applications, appsLoaded); - const showNamespaceColumn = !namespace || namespace === ''; + const rows = useProjectsRowsDV(pagedItems, managedColumns, applications, appsLoaded); + const columnIds = React.useMemo( + () => managedColumns.map((column) => column.id), + [managedColumns], + ); + const { columns: visibleColumnsDV, rows: visibleRows } = React.useMemo( + () => filterDataView(columnsDV, rows, columnIds, activeColumnIds), + [filterDataView, columnsDV, rows, columnIds, activeColumnIds], + ); // Check if there are projects initially (before search) const hasProjects = React.useMemo(() => { @@ -176,7 +202,7 @@ const ProjectList: React.FC = ({ const empty = ( - + = ({ const error = loadError && ( - + = ({ ); - const isEmptyState = !loadError && rows.length === 0; + const isEmptyState = !loadError && visibleRows.length === 0; + + const listPageFilter = !hideNameLabelFilters && hasProjects && ( + + ); return (
@@ -224,25 +260,19 @@ const ProjectList: React.FC = ({ )} - {!hideNameLabelFilters && hasProjects && ( - + {hasProjects && ( + )}
({ - 'aria-label': ariaLabel, - className, - sort, - tooltip: '', -}); - export const useColumnsDV = ( - namespace: string | undefined, + managedColumns: GitOpsManagedColumn[], getSortParams: (columnIndex: number) => ThProps['sort'], + columnSortConfig: { key: string }[], ): DataViewTh[] => { - const showNamespace = !namespace || namespace === ''; - const i: number = showNamespace ? 1 : 0; const { t } = useTranslation('plugin__gitops-plugin'); - const columns: DataViewTh[] = [ - { - cell: t('Name'), - props: sortableHeaderProps('name', 'pf-m-width-20', getSortParams(0)), - }, - ...(showNamespace - ? [ - { - cell: t('Namespace'), - props: sortableHeaderProps('namespace', 'pf-m-width-15', getSortParams(1)), - }, - ] - : []), - { - cell: t('Description'), - props: sortableHeaderProps('description', 'pf-m-width-10', getSortParams(1 + i)), - }, - { - cell: t('Applications'), - props: sortableHeaderProps('applications', 'pf-m-width-15', getSortParams(2 + i)), - }, - { - cell: t('Labels'), - props: sortableHeaderProps('labels', 'pf-m-width-20', getSortParams(3 + i)), - }, - { - cell: t('Last Updated'), - props: sortableHeaderProps( - 'last updated', - 'pf-m-width-15', - getSortParams(showNamespace ? 5 : 4), - ), - }, - { - cell: '', - props: { 'aria-label': 'actions' }, - }, - ]; - return columns; + const titleById: Record = { + name: t('Name'), + [NAMESPACE_COLUMN_ID]: t('Namespace'), + description: t('Description'), + applications: t('Applications'), + labels: t('Labels'), + 'last-updated': t('Last Updated'), + actions: '', + }; + + const widthById: Record = { + name: 'pf-m-width-20', + [NAMESPACE_COLUMN_ID]: 'pf-m-width-15', + description: 'pf-m-width-10', + applications: 'pf-m-width-15', + labels: 'pf-m-width-20', + 'last-updated': 'pf-m-width-15', + }; + + return managedColumns.map((column) => { + if (column.id === 'actions') { + return { + cell: '', + props: { 'aria-label': 'actions' }, + }; + } + const sortIndex = columnSortConfig.findIndex((entry) => entry.key === column.id); + return { + cell: titleById[column.id] ?? column.title, + props: { + 'aria-label': column.id, + className: widthById[column.id], + tooltip: '', + ...(sortIndex >= 0 ? { sort: getSortParams(sortIndex) } : {}), + }, + }; + }); }; export const useProjectsRowsDV = ( projectsList: AppProjectKind[], - namespace: string | undefined, + managedColumns: GitOpsManagedColumn[], applications: ApplicationKind[], appsLoaded: boolean, ): DataViewTr[] => { @@ -460,12 +476,11 @@ export const useProjectsRowsDV = ( if (projectsList == undefined || projectsList.length == 0) { return rows; } - const showNamespace = !namespace || namespace === ''; projectsList.forEach((obj, index) => { const appsCount = getApplicationsCount(obj, applications, appsLoaded); - rows.push([ - { + const cellsById: Record = { + name: { cell: ( , - id: obj.metadata.namespace, - dataLabel: 'Namespace', - }, - ] - : []), - { + [NAMESPACE_COLUMN_ID]: { + cell: , + id: obj.metadata.namespace, + dataLabel: 'Namespace', + }, + description: { id: 'description', dataLabel: 'Description', cell: ( {obj.spec?.description || '-'} ), }, - { + applications: { id: 'applications', dataLabel: 'Applications', cell: ( @@ -502,7 +513,7 @@ export const useProjectsRowsDV = ( ), }, - { + labels: { id: 'labels', dataLabel: 'Labels', cell: ( @@ -521,7 +532,7 @@ export const useProjectsRowsDV = (
), }, - { + 'last-updated': { id: 'last-updated', cell: (() => { const lastUpdate = getLastUpdateTimestamp(obj); @@ -532,12 +543,14 @@ export const useProjectsRowsDV = ( ); })(), }, - { + actions: { id: 'actions-' + index, cell: , props: { style: { paddingTop: 8, paddingRight: 0, paddingLeft: 0, width: 10 } }, }, - ]); + }; + + rows.push(managedColumns.map((column) => cellsById[column.id])); }); return rows; }; diff --git a/src/gitops/components/rollout/RolloutList.tsx b/src/gitops/components/rollout/RolloutList.tsx index 8c32e14b0..f8edbdd94 100644 --- a/src/gitops/components/rollout/RolloutList.tsx +++ b/src/gitops/components/rollout/RolloutList.tsx @@ -21,6 +21,7 @@ import { import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; import { ErrorState } from '@patternfly/react-component-groups'; import { EmptyState, EmptyStateBody, Spinner } from '@patternfly/react-core'; +import type { DataViewTd } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import { CubesIcon, SearchIcon } from '@patternfly/react-icons'; import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; @@ -29,11 +30,17 @@ import { ShowOperandsInAllNamespacesRadioGroup, useShowOperandsInAllNamespaces, } from '../shared/AllNamespaces'; +import { + type GitOpsManagedColumn, + NAMESPACE_COLUMN_ID, + useGitOpsColumnManagement, +} from '../shared/ColumnManagement'; import { GitOpsDataViewTable, useGitOpsDataViewSort, useGitOpsListPagePagination, } from '../shared/DataView'; +import { GitOpsListPageToolbar } from '../shared/GitOpsListPageToolbar'; import { filterByConsoleNameAndLabels, filterResourcesByLabelQuery, @@ -41,6 +48,10 @@ import { parseLabelFilterParam, } from '../shared/listPageTextFilters'; import { MetadataLabels } from '../shared/MetadataLabels/MetadataLabels'; +import { + getRolloutManagedColumns, + ROLLOUT_LIST_COLUMN_MANAGEMENT_ID, +} from '../shared/rolloutListColumns'; import { useRolloutActionsProvider } from './hooks/useRolloutActionsProvider'; import { RolloutKind, RolloutModel } from './model/RolloutModel'; @@ -95,19 +106,23 @@ const RolloutList: React.FC = ({ namespaced: !listAllNamespaces, namespace, }); + + const { t } = useGitOpsTranslation(); + const includeNamespaceColumn = !namespace; + const managedColumns = React.useMemo(() => getRolloutManagedColumns(true, t), [t]); + + const { activeColumnIds, columnManagement, filterDataView } = useGitOpsColumnManagement({ + columnManagementID: ROLLOUT_LIST_COLUMN_MANAGEMENT_ID, + columns: managedColumns, + resourceType: t('Rollouts'), + includeNamespaceColumn, + showNamespaceHelp: includeNamespaceColumn, + }); + const columnSortConfig = React.useMemo( () => - [ - 'name', - ...(!listAllNamespaces || !namespace ? ['namespace'] : []), - 'status', - 'pods', - 'labels', - 'selector', - 'last-updated', - 'actions', - ].map((key) => ({ key })), - [listAllNamespaces, namespace], + managedColumns.filter((column) => !column.alwaysShown).map((column) => ({ key: column.id })), + [managedColumns], ); const { searchParams, sortBy, direction, getSortParams } = @@ -118,11 +133,9 @@ const RolloutList: React.FC = ({ const nameQuery = searchParams.get('name') || ''; const labelsParam = searchParams.get('labels') || ''; - const { t } = useGitOpsTranslation(); - - const columnsDV = useColumnsDV(namespace, getSortParams, t); + const columnsDV = useColumnsDV(managedColumns, getSortParams, columnSortConfig, t); const sortedRollouts = React.useMemo(() => { - return sortData(rollouts, sortBy, direction); + return sortData(rollouts as RolloutKind[], sortBy, direction); }, [rollouts, sortBy, direction]); const filters = getFilters(t); @@ -143,12 +156,22 @@ const RolloutList: React.FC = ({ namespace, searchParams, }); - const rows = useRolloutsRowsDV(pagedItems, namespace, t); + const rows = useRolloutsRowsDV(pagedItems, managedColumns, t); + const columnIds = React.useMemo( + () => managedColumns.map((column) => column.id), + [managedColumns], + ); + const { columns: visibleColumnsDV, rows: visibleRows } = React.useMemo( + () => filterDataView(columnsDV, rows, columnIds, activeColumnIds), + [filterDataView, columnsDV, rows, columnIds, activeColumnIds], + ); + + const hasRollouts = sortedRollouts.length > 0; const empty = ( - + {namespace @@ -163,7 +186,7 @@ const RolloutList: React.FC = ({ const error = loadError && ( - + = ({ ); - const isEmptyState = !loadError && rows.length === 0; + const isEmptyState = !loadError && visibleRows.length === 0; const topologyUrl = namespace ? '/topology/ns/' + namespace + '?view=graph' : '/topology/all-namespaces?view=graph'; + const listPageFilter = !hideNameLabelFilters && ( + + ); + + const topologyControl = + filteredBySearch.length > 0 && !loadError ? ( + {topologyLink(topologyUrl, t)} + ) : null; + + const columnAndTopologyControls = + hasRollouts || topologyControl ? ( + + {hasRollouts ? columnManagement : null} + {topologyControl} + + ) : undefined; + return ( <> {showTitle == undefined && ( @@ -196,26 +241,15 @@ const RolloutList: React.FC = ({ )} - {!hideNameLabelFilters && ( - - - - - {filteredBySearch.length > 0 && !loadError && ( - - {topologyLink(topologyUrl, t)} - - )} - + {(hasRollouts || !hideNameLabelFilters) && ( + )} ThProps['sort'], + columnSortConfig: { key: string }[], t: (key: string) => string, -) => { - const i: number = namespace ? 0 : 1; - const columns: DataViewTh[] = [ - { - cell: t('Name'), - props: { - 'aria-label': 'name', - className: 'pf-m-width-25', - sort: getSortParams(0), - }, - }, - ...(!namespace - ? [ - { - cell: t('Namespace'), - props: { - 'aria-label': 'namespace', - className: 'pf-m-width-15', - sort: getSortParams(1), - }, - }, - ] - : []), - { - cell: t('Status'), - props: { - 'aria-label': 'status', - className: 'pf-m-width-10', - sort: getSortParams(1 + i), - }, - }, - { - cell: t('Pods'), - props: { - 'aria-label': 'pods', - className: 'pf-m-width-10', - sort: getSortParams(2 + i), - }, - }, - { - cell: t('Labels'), - props: { - 'aria-label': 'labels', - className: 'pf-m-width-15', - sort: getSortParams(3 + i), - }, - }, - { - cell: t('Selector'), - props: { - 'aria-label': 'selector', - className: 'pf-m-width-15', - sort: getSortParams(4 + i), - }, - }, - { - cell: t('Last Updated'), +): DataViewTh[] => { + const titleById: Record = { + name: t('Name'), + [NAMESPACE_COLUMN_ID]: t('Namespace'), + status: t('Status'), + pods: t('Pods'), + labels: t('Labels'), + selector: t('Selector'), + 'last-updated': t('Last Updated'), + actions: '', + }; + + const widthById: Record = { + name: 'pf-m-width-25', + [NAMESPACE_COLUMN_ID]: 'pf-m-width-15', + status: 'pf-m-width-10', + pods: 'pf-m-width-10', + labels: 'pf-m-width-15', + selector: 'pf-m-width-15', + 'last-updated': 'pf-m-width-15', + }; + + return managedColumns.map((column) => { + if (column.id === 'actions') { + return { + cell: '', + props: { 'aria-label': 'actions' }, + }; + } + const sortIndex = columnSortConfig.findIndex((entry) => entry.key === column.id); + return { + cell: titleById[column.id] ?? column.title, props: { - 'aria-label': 'last updated', - className: 'pf-m-width-15', - sort: getSortParams(5 + i), + 'aria-label': column.id, + className: widthById[column.id], + ...(sortIndex >= 0 ? { sort: getSortParams(sortIndex) } : {}), }, - }, - { - cell: '', - props: { 'aria-label': 'actions' }, - }, - ]; - - return columns; + }; + }); }; export const useRolloutsRowsDV = ( - rolloutsList, - namespace, + rolloutsList: RolloutKind[], + managedColumns: GitOpsManagedColumn[], t: (key: string) => string, ): DataViewTr[] => { const rows: DataViewTr[] = []; @@ -365,8 +369,8 @@ export const useRolloutsRowsDV = ( return rows; } rolloutsList.forEach((obj, index) => { - rows.push([ - { + const cellsById: Record = { + name: { cell: (
, - id: obj.metadata.namespace, - dataLabel: 'Namespace', - }, - ] - : []), - { + [NAMESPACE_COLUMN_ID]: { + cell: , + id: obj.metadata.namespace, + dataLabel: 'Namespace', + }, + status: { id: 'status', cell: ( ), }, - { + pods: { id: 'pods', cell: ( <> @@ -418,7 +418,7 @@ export const useRolloutsRowsDV = ( ), }, - { + labels: { id: 'labels', dataLabel: 'Labels', cell: ( @@ -431,7 +431,7 @@ export const useRolloutsRowsDV = (
), }, - { + selector: { id: 'selector', cell: ( <> @@ -456,7 +456,7 @@ export const useRolloutsRowsDV = ( ), }, - { + 'last-updated': { id: 'last-updated', cell: ( <> @@ -464,12 +464,14 @@ export const useRolloutsRowsDV = ( ), }, - { + actions: { id: 'actions-' + index, cell: , props: { style: { paddingTop: 8, paddingRight: 0, paddingLeft: 0, width: 10 } }, }, - ]); + }; + + rows.push(managedColumns.map((column) => cellsById[column.id])); }); return rows; }; diff --git a/src/gitops/components/rollout/rollout-list.scss b/src/gitops/components/rollout/rollout-list.scss index d953da526..dab7c3fcc 100644 --- a/src/gitops/components/rollout/rollout-list.scss +++ b/src/gitops/components/rollout/rollout-list.scss @@ -1,6 +1,14 @@ .rollout-list-page { - &__topology-link { - margin-bottom: 15px; - margin-left: 10px; - } -} \ No newline at end of file + &__column-controls { + display: inline-flex; + align-items: center; + gap: var(--pf-t--global--spacer--sm); + min-height: var(--pf-t--global--spacer--2xl); + } + + &__topology-link { + display: inline-flex; + align-items: center; + line-height: 1; + } +} diff --git a/src/gitops/components/shared/ApplicationList.tsx b/src/gitops/components/shared/ApplicationList.tsx index 8cf9df0cc..0da83ca3e 100644 --- a/src/gitops/components/shared/ApplicationList.tsx +++ b/src/gitops/components/shared/ApplicationList.tsx @@ -15,6 +15,7 @@ import { } from '@openshift-console/dynamic-plugin-sdk'; import { ErrorState } from '@patternfly/react-component-groups'; import { EmptyState, EmptyStateBody, Flex, FlexItem, Spinner, Title } from '@patternfly/react-core'; +import type { DataViewTd } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import { CubesIcon } from '@patternfly/react-icons'; import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; @@ -40,6 +41,10 @@ import { ShowOperandsInAllNamespacesRadioGroup, useShowOperandsInAllNamespaces, } from './AllNamespaces'; +import { + APPLICATION_LIST_COLUMN_MANAGEMENT_ID, + getApplicationManagedColumns, +} from './applicationListColumns'; import { APPLICATION_HEALTH_FILTER_PARAM, APPLICATION_SYNC_FILTER_PARAM, @@ -48,11 +53,17 @@ import { parseRowFilterParam, } from './applicationListFilters'; import ApplicationSetApplicationsView from './ApplicationSetApplicationsView'; +import { + type GitOpsManagedColumn, + NAMESPACE_COLUMN_ID, + useGitOpsColumnManagement, +} from './ColumnManagement'; import { GitOpsDataViewTable, useGitOpsDataViewSort, useGitOpsListPagePagination, } from './DataView'; +import { GitOpsListPageToolbar } from './GitOpsListPageToolbar'; import { filterByConsoleNameAndLabels, filterResourcesByLabelQuery, @@ -114,19 +125,21 @@ const ApplicationList: React.FC = ({ }); const { t } = useTranslation('plugin__gitops-plugin'); + const includeNamespaceColumn = !namespace; + const managedColumns = React.useMemo(() => getApplicationManagedColumns(true, t), [t]); + + const { activeColumnIds, columnManagement, filterDataView } = useGitOpsColumnManagement({ + columnManagementID: APPLICATION_LIST_COLUMN_MANAGEMENT_ID, + columns: managedColumns, + resourceType: t('Application'), + includeNamespaceColumn, + showNamespaceHelp: includeNamespaceColumn, + }); + const columnSortConfig = React.useMemo( () => - [ - 'name', - ...(!listAllNamespaces || !namespace ? ['namespace'] : []), - 'sync-status', - 'health-status', - 'revision', - 'labels', - 'project', - 'actions', - ].map((key) => ({ key })), - [listAllNamespaces, namespace], + managedColumns.filter((column) => !column.alwaysShown).map((column) => ({ key: column.id })), + [managedColumns], ); const { searchParams, sortBy, direction, getSortParams } = @@ -135,7 +148,7 @@ const ApplicationList: React.FC = ({ // Get search query from URL parameters const searchQuery = searchParams.get('q') || ''; - const columnsDV = useColumnsDV(namespace, getSortParams); + const columnsDV = useColumnsDV(managedColumns, getSortParams, columnSortConfig); const sortedApplications = React.useMemo(() => { return sortData(applications, sortBy, direction); }, [applications, sortBy, direction]); @@ -185,14 +198,22 @@ const ApplicationList: React.FC = ({ namespace, searchParams, }); - const rows = useApplicationRowsDV(pagedApplications, namespace); + const rows = useApplicationRowsDV(pagedApplications, managedColumns); + const columnIds = React.useMemo( + () => managedColumns.map((column) => column.id), + [managedColumns], + ); + const { columns: visibleColumnsDV, rows: visibleRows } = React.useMemo( + () => filterDataView(columnsDV, rows, columnIds, activeColumnIds), + [filterDataView, columnsDV, rows, columnIds, activeColumnIds], + ); // Check if there are applications owned by this ApplicationSet initially (before filters/search) const hasOwnedApplications = ownedApps.length > 0; const empty = ( - + {(() => { @@ -228,7 +249,7 @@ const ApplicationList: React.FC = ({ const error = loadError && ( - + = ({ ); + + const listPageFilter = !hideNameLabelFilters && hasOwnedApplications && ( + + ); + return (
{showTitle == undefined && (project == undefined || appset == undefined) && ( @@ -270,14 +302,8 @@ const ApplicationList: React.FC = ({ )} - {!appset && !hideNameLabelFilters && hasOwnedApplications && ( - + {!appset && hasOwnedApplications && ( + )} {appset && ( = ({ onFilterChange={onFilterChange} nameFilterPlaceholder={t('plugin__gitops-plugin~Search by name...')} loaded={loaded} - columns={columnsDV} - rows={rows} + columns={visibleColumnsDV} + rows={visibleRows} + columnManagement={columnManagement} isEmpty={filteredBySearch.length === 0} emptyState={empty} errorState={error || undefined} @@ -302,8 +329,8 @@ const ApplicationList: React.FC = ({ )} {!appset && ( ); }; -const useApplicationRowsDV = (applicationsList, namespace): DataViewTr[] => { +const useApplicationRowsDV = ( + applicationsList: ApplicationKind[], + managedColumns: GitOpsManagedColumn[], +): DataViewTr[] => { + const includeNamespaceColumn = managedColumns.some((column) => column.id === NAMESPACE_COLUMN_ID); const rows: DataViewTr[] = []; applicationsList.forEach((app, index) => { let sources: ApplicationSource[]; @@ -403,8 +434,9 @@ const useApplicationRowsDV = (applicationsList, namespace): DataViewTr[] => { sources = []; revisions = []; } - rows.push([ - { + + const cellsById: Record = { + name: { cell: (
{
), - id: app.metadata?.name, + id: 'name', dataLabel: 'Name', }, - ...(!namespace - ? [ - { + ...(includeNamespaceColumn + ? { + [NAMESPACE_COLUMN_ID]: { cell: , - id: app.metadata.namespace, + id: NAMESPACE_COLUMN_ID, dataLabel: 'Namespace', }, - ] - : []), - { - id: app.status?.sync?.status, + } + : {}), + 'sync-status': { + id: 'sync-status', cell: (
@@ -446,12 +478,12 @@ const useApplicationRowsDV = (applicationsList, namespace): DataViewTr[] => {
), }, - { - id: app.status?.health?.status, + 'health-status': { + id: 'health-status', cell: , }, - { - id: app?.status?.sync?.revision, + revision: { + id: 'revision', cell: ( <> {sources[0]?.targetRevision ? sources[0].targetRevision : 'HEAD'}  @@ -466,7 +498,7 @@ const useApplicationRowsDV = (applicationsList, namespace): DataViewTr[] => { ), }, - { + labels: { id: 'labels', dataLabel: 'Labels', cell: ( @@ -485,8 +517,8 @@ const useApplicationRowsDV = (applicationsList, namespace): DataViewTr[] => {
), }, - { - id: app.spec?.project, + project: { + id: 'project', cell: app.spec?.project && ( { /> ), }, - { - id: 'actions-' + index, + actions: { + id: 'actions', cell: , props: { style: { paddingTop: 8, paddingRight: 0, paddingLeft: 0, width: 10 } }, }, - ]); + }; + + rows.push(managedColumns.map((column) => cellsById[column.id])); }); return rows; }; const useColumnsDV = ( - namespace: string, + managedColumns: GitOpsManagedColumn[], getSortParams: (columnIndex: number) => ThProps['sort'], + columnSortConfig: { key: string }[], ): DataViewTh[] => { - const i: number = namespace ? 0 : 1; const { t } = useTranslation('plugin__gitops-plugin'); - const columns: DataViewTh[] = [ - { - cell: t('Name'), - props: { - 'aria-label': 'name', - className: 'pf-m-width-25', - sort: getSortParams(0), - }, - }, - ...(!namespace - ? [ - { - cell: t('Namespace'), - props: { - 'aria-label': 'namespace', - className: 'pf-m-width-15', - sort: getSortParams(1), - }, - }, - ] - : []), - { - cell: t('Sync Status'), - props: { - 'aria-label': 'sync status', - className: 'pf-m-width-15', - sort: getSortParams(1 + i), - }, - }, - { - cell: t('Health Status'), - props: { - 'aria-label': 'health status', - className: 'pf-m-width-15', - sort: getSortParams(2 + i), - }, - }, - { - cell: t('Revision'), - props: { - 'aria-label': 'revision', - className: 'pf-m-width-12', - sort: getSortParams(3 + i), - }, - }, - { - cell: t('Labels'), - props: { - 'aria-label': 'labels', - className: 'pf-m-width-20', - sort: getSortParams(4 + i), - }, - }, - { - cell: t('App Project'), + + const titleById: Record = { + name: t('Name'), + [NAMESPACE_COLUMN_ID]: t('Namespace'), + 'sync-status': t('Sync Status'), + 'health-status': t('Health Status'), + revision: t('Revision'), + labels: t('Labels'), + project: t('App Project'), + actions: '', + }; + + const widthById: Record = { + name: 'pf-m-width-25', + [NAMESPACE_COLUMN_ID]: 'pf-m-width-15', + 'sync-status': 'pf-m-width-15', + 'health-status': 'pf-m-width-15', + revision: 'pf-m-width-12', + labels: 'pf-m-width-20', + project: 'pf-m-width-20', + }; + + return managedColumns.map((column) => { + if (column.id === 'actions') { + return { + cell: '', + props: { 'aria-label': 'actions' }, + }; + } + const sortIndex = columnSortConfig.findIndex((entry) => entry.key === column.id); + return { + cell: titleById[column.id] ?? column.title, props: { - 'aria-label': 'project', - className: 'pf-m-width-20', - sort: getSortParams(5 + i), + 'aria-label': column.id, + className: widthById[column.id], + ...(sortIndex >= 0 ? { sort: getSortParams(sortIndex) } : {}), }, - }, - { - cell: '', - props: { 'aria-label': 'actions' }, - }, - ]; - return columns; + }; + }); }; export default ApplicationList; diff --git a/src/gitops/components/shared/ApplicationSetApplicationsView.tsx b/src/gitops/components/shared/ApplicationSetApplicationsView.tsx index 367aa5f1a..a2249841e 100644 --- a/src/gitops/components/shared/ApplicationSetApplicationsView.tsx +++ b/src/gitops/components/shared/ApplicationSetApplicationsView.tsx @@ -3,13 +3,14 @@ import * as React from 'react'; import { ApplicationKind } from '@gitops/models/ApplicationModel'; import { ApplicationSetKind } from '@gitops/models/ApplicationSetModel'; import { ListPageFilter, RowFilter, useUserSettings } from '@openshift-console/dynamic-plugin-sdk'; -import { Flex, FlexItem, Stack, StackItem } from '@patternfly/react-core'; +import { Stack, StackItem } from '@patternfly/react-core'; import { DataViewState } from '@patternfly/react-data-view/dist/esm/DataView'; import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import { ApplicationSetGraphView } from '../appset/graph/ApplicationSetGraphView'; import { type GitOpsDataViewPagination, GitOpsDataViewTable } from './DataView'; +import { GitOpsListPageToolbar } from './GitOpsListPageToolbar'; import GitOpsViewSwitcher from './GitOpsViewSwitcher'; import { APPLICATION_SET_APPLICATIONS_VIEW_SETTING_KEY, GitOpsViewType } from './GitOpsViewType'; @@ -27,6 +28,7 @@ type ApplicationSetApplicationsViewProps = { loaded: boolean; columns: DataViewTh[]; rows: DataViewTr[]; + columnManagement?: React.ReactNode; emptyState: React.ReactNode; errorState?: React.ReactNode; isError?: boolean; @@ -47,6 +49,7 @@ const ApplicationSetApplicationsView: React.FC - - - {!hideNameLabelFilters && hasOwnedApplications && ( + - )} - - + ) : undefined + } + columnManagement={hasOwnedApplications ? columnManagement : undefined} + actions={ - - + } + />
= ({ }); const { t } = useTranslation('plugin__gitops-plugin'); + const includeNamespaceColumn = !namespace; + const managedColumns = React.useMemo(() => getApplicationSetManagedColumns(true, t), [t]); + + const { activeColumnIds, columnManagement, filterDataView } = useGitOpsColumnManagement({ + columnManagementID: APPLICATION_SET_LIST_COLUMN_MANAGEMENT_ID, + columns: managedColumns, + resourceType: t('ApplicationSet'), + includeNamespaceColumn, + showNamespaceHelp: includeNamespaceColumn, + }); const columnSortConfig = React.useMemo( () => - [ - 'name', - ...(!listAllNamespaces || !namespace ? ['namespace'] : []), - 'status', - 'generated-apps', - 'generators', - 'labels', - 'created-at', - 'actions', - ].map((key) => ({ key })), - [listAllNamespaces, namespace], + managedColumns.filter((column) => !column.alwaysShown).map((column) => ({ key: column.id })), + [managedColumns], ); const { searchParams, sortBy, direction, getSortParams } = @@ -146,7 +158,7 @@ const ApplicationSetList: React.FC = ({ const nameQuery = searchParams.get('name') || ''; const labelsParam = searchParams.get('labels') || ''; - const columnsDV = useColumnsDV(namespace, getSortParams); + const columnsDV = useColumnsDV(managedColumns, getSortParams, columnSortConfig); const sortedApplicationSets = React.useMemo(() => { return sortData( applicationSets as ApplicationSetKind[], @@ -175,7 +187,15 @@ const ApplicationSetList: React.FC = ({ namespace, searchParams, }); - const rows = useApplicationSetRowsDV(pagedItems, namespace, applications, appsLoaded); + const rows = useApplicationSetRowsDV(pagedItems, managedColumns, applications, appsLoaded); + const columnIds = React.useMemo( + () => managedColumns.map((column) => column.id), + [managedColumns], + ); + const { columns: visibleColumnsDV, rows: visibleRows } = React.useMemo( + () => filterDataView(columnsDV, rows, columnIds, activeColumnIds), + [filterDataView, columnsDV, rows, columnIds, activeColumnIds], + ); // Check if there are ApplicationSets initially (before search) const hasApplicationSets = React.useMemo(() => { @@ -200,7 +220,7 @@ const ApplicationSetList: React.FC = ({ const empty = ( - + = ({ const error = loadError && ( - + = ({ sortedApplicationSets.length === 0 || filteredBySearch.length === 0; + const listPageFilter = !hideNameLabelFilters && hasApplicationSets && ( + + ); + return (
{showTitle == undefined && ( @@ -255,17 +284,12 @@ const ApplicationSetList: React.FC = ({ )} - {!hideNameLabelFilters && hasApplicationSets && ( - + {hasApplicationSets && ( + )} { const rows: DataViewTr[] = []; applicationSetsList.forEach((appSet: ApplicationSetKind, index: number) => { - rows.push([ - { + const cellsById: Record = { + name: { cell: (
, - id: appSet.metadata.namespace, - dataLabel: 'Namespace', - }, - ] - : []), - { + [NAMESPACE_COLUMN_ID]: { + cell: , + id: appSet.metadata.namespace, + dataLabel: 'Namespace', + }, + status: { id: getAppSetStatus(appSet), cell: , }, - { + 'generated-apps': { id: 'generated-apps-' + index, cell:
{getGeneratedAppsCount(appSet, applications, appsLoaded).toString()}
, }, - { + generators: { id: 'generators-' + index, cell:
{getAppSetGeneratorCount(appSet).toString()}
, }, - { + labels: { id: 'labels', dataLabel: 'Labels', cell: ( @@ -362,93 +382,67 @@ const useApplicationSetRowsDV = (
), }, - { + 'created-at': { id: 'created-at-' + index, cell:
{formatCreationTimestamp(appSet.metadata.creationTimestamp)}
, }, - { + actions: { id: 'actions-' + index, cell: , props: { style: { paddingTop: 8, paddingRight: 0, paddingLeft: 0, width: 10 } }, }, - ]); + }; + + rows.push(managedColumns.map((column) => cellsById[column.id])); }); return rows; }; const useColumnsDV = ( - namespace: string, + managedColumns: GitOpsManagedColumn[], getSortParams: (columnIndex: number) => ThProps['sort'], + columnSortConfig: { key: string }[], ): DataViewTh[] => { - const i: number = namespace ? 0 : 1; const { t } = useTranslation('plugin__gitops-plugin'); - const columns: DataViewTh[] = [ - { - cell: t('Name'), - props: { - 'aria-label': 'name', - className: 'pf-m-width-25', - sort: getSortParams(0), - }, - }, - ...(!namespace - ? [ - { - cell: t('Namespace'), - props: { - 'aria-label': 'namespace', - className: 'pf-m-width-15', - sort: getSortParams(1), - }, - }, - ] - : []), - { - cell: t('Health Status'), - props: { - 'aria-label': 'health status', - className: 'pf-m-width-15', - sort: getSortParams(1 + i), - }, - }, - { - cell: t('Generated Apps'), - props: { - 'aria-label': 'generated apps', - className: 'pf-m-width-15', - sort: getSortParams(2 + i), - }, - }, - { - cell: t('Generators'), - props: { - 'aria-label': 'generators', - className: 'pf-m-width-15', - sort: getSortParams(3 + i), - }, - }, - { - cell: t('Labels'), - props: { - 'aria-label': 'labels', - className: 'pf-m-width-20', - sort: getSortParams(4 + i), - }, - }, - { - cell: t('Created At'), + + const titleById: Record = { + name: t('Name'), + [NAMESPACE_COLUMN_ID]: t('Namespace'), + status: t('Health Status'), + 'generated-apps': t('Generated Apps'), + generators: t('Generators'), + labels: t('Labels'), + 'created-at': t('Created At'), + actions: '', + }; + + const widthById: Record = { + name: 'pf-m-width-25', + [NAMESPACE_COLUMN_ID]: 'pf-m-width-15', + status: 'pf-m-width-15', + 'generated-apps': 'pf-m-width-15', + generators: 'pf-m-width-15', + labels: 'pf-m-width-20', + 'created-at': 'pf-m-width-15', + }; + + return managedColumns.map((column) => { + if (column.id === 'actions') { + return { + cell: '', + props: { 'aria-label': 'actions' }, + }; + } + const sortIndex = columnSortConfig.findIndex((entry) => entry.key === column.id); + return { + cell: titleById[column.id] ?? column.title, props: { - 'aria-label': 'created at', - className: 'pf-m-width-15', - sort: getSortParams(5 + i), + 'aria-label': column.id, + className: widthById[column.id], + ...(sortIndex >= 0 ? { sort: getSortParams(sortIndex) } : {}), }, - }, - { - cell: '', - props: { 'aria-label': 'actions' }, - }, - ]; - return columns; + }; + }); }; const getFilters = (t: (key: string) => string): RowFilter[] => [ diff --git a/src/gitops/components/shared/ColumnManagement/GitOpsColumnManagementControl.tsx b/src/gitops/components/shared/ColumnManagement/GitOpsColumnManagementControl.tsx new file mode 100644 index 000000000..97660090f --- /dev/null +++ b/src/gitops/components/shared/ColumnManagement/GitOpsColumnManagementControl.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; + +import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; +import { Button, ButtonVariant, Tooltip } from '@patternfly/react-core'; +import { ColumnsIcon } from '@patternfly/react-icons'; + +import { GitOpsColumnManagementModal } from './GitOpsColumnManagementModal'; +import type { GitOpsColumnManagementModalColumn } from './types'; + +export type GitOpsColumnManagementControlProps = { + appliedColumns: GitOpsColumnManagementModalColumn[]; + applyColumns: (columns: GitOpsColumnManagementModalColumn[]) => void; + resourceType: string; + showNamespaceHelp?: boolean; +}; + +export const GitOpsColumnManagementControl: React.FC = ({ + appliedColumns, + applyColumns, + resourceType, + showNamespaceHelp, +}) => { + const { t } = useGitOpsTranslation(); + const [isOpen, setIsOpen] = React.useState(false); + const manageColumnsLabel = t('Manage columns'); + + const onClose = React.useCallback(() => { + setIsOpen(false); + }, []); + + const onApplyColumns = React.useCallback( + (columns: GitOpsColumnManagementModalColumn[]) => { + applyColumns(columns); + setIsOpen(false); + }, + [applyColumns], + ); + + return ( + <> + + + + + + + ); +}; diff --git a/src/gitops/components/shared/ColumnManagement/columnManagementUtils.test.ts b/src/gitops/components/shared/ColumnManagement/columnManagementUtils.test.ts new file mode 100644 index 000000000..d9c3681ed --- /dev/null +++ b/src/gitops/components/shared/ColumnManagement/columnManagementUtils.test.ts @@ -0,0 +1,242 @@ +import type { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/DataViewTable'; + +import { + filterDataViewColumnsAndRows, + getDefaultActiveColumnIds, + getManageableColumns, + getSavableColumnIds, + resolveActiveColumnIds, + toColumnManagementModalColumns, +} from './columnManagementUtils'; +import { + type GitOpsManagedColumn, + getGitOpsColumnManagementSettingKey, + NAMESPACE_COLUMN_ID, +} from './types'; + +const baseColumns: GitOpsManagedColumn[] = [ + { id: 'name', title: 'Name', isShownByDefault: true, isUntoggleable: true }, + { id: NAMESPACE_COLUMN_ID, title: 'Namespace', isShownByDefault: true }, + { id: 'status', title: 'Status', isShownByDefault: true }, + { id: 'labels', title: 'Labels', isShownByDefault: true }, + { id: 'extra', title: 'Extra', isShownByDefault: false }, + { id: 'actions', title: '', isShownByDefault: true, alwaysShown: true }, +]; + +describe('getGitOpsColumnManagementSettingKey', () => { + it('prefixes the table id', () => { + expect(getGitOpsColumnManagementSettingKey('applications')).toBe( + 'gitops.columnManagement.applications', + ); + }); +}); + +describe('getManageableColumns', () => { + it('omits always-shown columns from the modal list', () => { + expect(getManageableColumns(baseColumns).map((c) => c.id)).toEqual([ + 'name', + 'namespace', + 'status', + 'labels', + 'extra', + ]); + }); +}); + +describe('getDefaultActiveColumnIds', () => { + it('includes default and always-shown columns in definition order', () => { + expect(getDefaultActiveColumnIds(baseColumns)).toEqual([ + 'name', + 'namespace', + 'status', + 'labels', + 'actions', + ]); + }); +}); + +describe('resolveActiveColumnIds', () => { + it('uses defaults when nothing is saved', () => { + expect(resolveActiveColumnIds({ columns: baseColumns })).toEqual([ + 'name', + 'namespace', + 'status', + 'labels', + 'actions', + ]); + }); + + it('applies saved preferences and keeps always-shown columns', () => { + expect( + resolveActiveColumnIds({ + columns: baseColumns, + savedColumnIds: ['name', 'extra'], + }), + ).toEqual(['name', 'extra', 'actions']); + }); + + it('drops unknown saved ids', () => { + expect( + resolveActiveColumnIds({ + columns: baseColumns, + savedColumnIds: ['name', 'stale', 'labels'], + }), + ).toEqual(['name', 'labels', 'actions']); + }); + + it('always keeps untoggleable columns', () => { + expect( + resolveActiveColumnIds({ + columns: baseColumns, + savedColumnIds: ['labels'], + }), + ).toEqual(['name', 'labels', 'actions']); + }); + + it('hides namespace when includeNamespaceColumn is false', () => { + expect( + resolveActiveColumnIds({ + columns: baseColumns, + savedColumnIds: ['name', 'namespace', 'status'], + includeNamespaceColumn: false, + }), + ).toEqual(['name', 'status', 'actions']); + }); +}); + +describe('toColumnManagementModalColumns', () => { + it('maps manageable columns for the Manage columns modal', () => { + const active = new Set(['name', 'status', 'actions']); + expect(toColumnManagementModalColumns(baseColumns, active)).toEqual([ + { + id: 'name', + title: 'Name', + isShown: true, + isShownByDefault: true, + isUntoggleable: true, + additional: undefined, + }, + { + id: 'namespace', + title: 'Namespace', + isShown: false, + isShownByDefault: true, + isUntoggleable: undefined, + additional: undefined, + }, + { + id: 'status', + title: 'Status', + isShown: true, + isShownByDefault: true, + isUntoggleable: undefined, + additional: undefined, + }, + { + id: 'labels', + title: 'Labels', + isShown: false, + isShownByDefault: true, + isUntoggleable: undefined, + additional: undefined, + }, + { + id: 'extra', + title: 'Extra', + isShown: false, + isShownByDefault: false, + isUntoggleable: undefined, + additional: undefined, + }, + ]); + }); + + it('omits namespace from the modal when includeNamespaceColumn is false', () => { + const ids = toColumnManagementModalColumns(baseColumns, ['name', 'status'], false).map( + (c) => c.id, + ); + expect(ids).not.toContain('namespace'); + }); +}); + +describe('getSavableColumnIds', () => { + it('persists only shown manageable column ids in definition order', () => { + const modalColumns = toColumnManagementModalColumns( + baseColumns, + new Set(['name', 'extra', 'actions']), + ); + expect(getSavableColumnIds(baseColumns, modalColumns)).toEqual(['name', 'extra']); + }); + + it('keeps previously saved ids that the current modal does not manage', () => { + const modalColumns = toColumnManagementModalColumns( + baseColumns, + new Set(['name', 'status', 'actions']), + false, + ); + + expect( + getSavableColumnIds(baseColumns, modalColumns, ['name', 'namespace', 'labels', 'extra']), + ).toEqual(['namespace', 'name', 'status']); + }); + + it('preserves Namespace after a first save in a project-scoped view', () => { + const defaultIds = getDefaultActiveColumnIds(baseColumns); + expect(defaultIds).toContain('namespace'); + + const projectModalColumns = toColumnManagementModalColumns( + baseColumns, + new Set(['name', 'status', 'labels', 'actions']), + false, + ); + expect(projectModalColumns.map((column) => column.id)).not.toContain('namespace'); + + const savedAfterProjectEdit = getSavableColumnIds(baseColumns, projectModalColumns, defaultIds); + expect(savedAfterProjectEdit).toContain('namespace'); + + expect( + resolveActiveColumnIds({ + columns: baseColumns, + savedColumnIds: savedAfterProjectEdit, + includeNamespaceColumn: true, + }), + ).toContain('namespace'); + }); +}); + +describe('filterDataViewColumnsAndRows', () => { + const columns: DataViewTh[] = [ + { cell: 'Name' }, + { cell: 'Namespace' }, + { cell: 'Status' }, + { cell: '' }, + ]; + const columnIds = ['name', 'namespace', 'status', 'actions']; + const rows: DataViewTr[] = [ + [{ cell: 'a' }, { cell: 'ns-a' }, { cell: 'ok' }, { cell: '…' }], + { + id: 'row-2', + row: [{ cell: 'b' }, { cell: 'ns-b' }, { cell: 'err' }, { cell: '…' }], + }, + ]; + + it('filters headers and both array and object rows by active ids', () => { + const result = filterDataViewColumnsAndRows( + columns, + rows, + columnIds, + new Set(['name', 'status', 'actions']), + ); + + expect( + result.columns.map((c) => (typeof c === 'object' && c && 'cell' in c ? c.cell : c)), + ).toEqual(['Name', 'Status', '']); + expect(result.rows).toEqual([ + [{ cell: 'a' }, { cell: 'ok' }, { cell: '…' }], + { + id: 'row-2', + row: [{ cell: 'b' }, { cell: 'err' }, { cell: '…' }], + }, + ]); + }); +}); diff --git a/src/gitops/components/shared/ColumnManagement/columnManagementUtils.ts b/src/gitops/components/shared/ColumnManagement/columnManagementUtils.ts new file mode 100644 index 000000000..cd7778073 --- /dev/null +++ b/src/gitops/components/shared/ColumnManagement/columnManagementUtils.ts @@ -0,0 +1,118 @@ +import type { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/DataViewTable'; + +import { + type GitOpsColumnManagementModalColumn, + type GitOpsManagedColumn, + type ResolveActiveColumnIdsOptions, + NAMESPACE_COLUMN_ID, +} from './types'; + +export const getManageableColumns = (columns: GitOpsManagedColumn[]): GitOpsManagedColumn[] => + columns.filter((column) => !column.alwaysShown); + +export const getAlwaysShownColumnIds = (columns: GitOpsManagedColumn[]): string[] => + columns.filter((column) => column.alwaysShown).map((column) => column.id); + +export const getDefaultActiveColumnIds = (columns: GitOpsManagedColumn[]): string[] => { + const active = new Set( + columns + .filter((column) => column.alwaysShown || column.isShownByDefault) + .map((column) => column.id), + ); + return columns.map((column) => column.id).filter((id) => active.has(id)); +}; + +export const resolveActiveColumnIds = ({ + columns, + savedColumnIds, + includeNamespaceColumn = true, +}: ResolveActiveColumnIdsOptions): string[] => { + const manageable = getManageableColumns(columns); + const knownIds = new Set(manageable.map((column) => column.id)); + const alwaysShownIds = getAlwaysShownColumnIds(columns); + + const defaultManageableIds = manageable + .filter((column) => column.isShownByDefault) + .map((column) => column.id); + + let activeManageableIds = + savedColumnIds && savedColumnIds.length > 0 + ? savedColumnIds.filter((id) => knownIds.has(id)) + : defaultManageableIds; + + manageable.forEach((column) => { + if (column.isUntoggleable && !activeManageableIds.includes(column.id)) { + activeManageableIds = [...activeManageableIds, column.id]; + } + }); + + if (!includeNamespaceColumn) { + activeManageableIds = activeManageableIds.filter((id) => id !== NAMESPACE_COLUMN_ID); + } + + const active = new Set([...activeManageableIds, ...alwaysShownIds]); + return columns.map((column) => column.id).filter((id) => active.has(id)); +}; + +export const toColumnManagementModalColumns = ( + columns: GitOpsManagedColumn[], + activeColumnIds: ReadonlySet | string[], + includeNamespaceColumn = true, +): GitOpsColumnManagementModalColumn[] => { + const active = activeColumnIds instanceof Set ? activeColumnIds : new Set(activeColumnIds); + + return getManageableColumns(columns) + .filter((column) => includeNamespaceColumn || column.id !== NAMESPACE_COLUMN_ID) + .map((column) => ({ + id: column.id, + title: column.title, + isShown: active.has(column.id), + isShownByDefault: column.isShownByDefault, + isUntoggleable: column.isUntoggleable, + additional: column.additional, + })); +}; + +export const getSavableColumnIds = ( + columns: GitOpsManagedColumn[], + modalColumns: GitOpsColumnManagementModalColumn[], + previouslySavedIds?: string[] | null, +): string[] => { + const managedByModal = new Set(modalColumns.map((column) => column.id)); + const shownFromModal = new Set( + modalColumns.filter((column) => column.isShown).map((column) => column.id), + ); + + const preserved = previouslySavedIds?.filter((id) => !managedByModal.has(id)) ?? []; + + const fromModal = getManageableColumns(columns) + .map((column) => column.id) + .filter((id) => shownFromModal.has(id)); + + return [...new Set([...preserved, ...fromModal])]; +}; + +export const filterDataViewColumnsAndRows = ( + columns: DataViewTh[], + rows: DataViewTr[], + columnIds: string[], + activeColumnIds: ReadonlySet | string[], +): { columns: DataViewTh[]; rows: DataViewTr[] } => { + const active = activeColumnIds instanceof Set ? activeColumnIds : new Set(activeColumnIds); + const indexes = columnIds + .map((id, index) => (active.has(id) ? index : -1)) + .filter((index) => index >= 0); + + const filteredColumns = indexes.map((index) => columns[index]); + const filteredRows = rows.map((row) => { + if (Array.isArray(row)) { + return indexes.map((index) => row[index]); + } + return { + ...row, + row: indexes.map((index) => row.row[index]), + }; + }); + + return { columns: filteredColumns, rows: filteredRows }; +}; diff --git a/src/gitops/components/shared/ColumnManagement/index.ts b/src/gitops/components/shared/ColumnManagement/index.ts new file mode 100644 index 000000000..a1b756460 --- /dev/null +++ b/src/gitops/components/shared/ColumnManagement/index.ts @@ -0,0 +1,26 @@ +export { + filterDataViewColumnsAndRows, + getAlwaysShownColumnIds, + getDefaultActiveColumnIds, + getManageableColumns, + getSavableColumnIds, + resolveActiveColumnIds, + toColumnManagementModalColumns, +} from './columnManagementUtils'; +export type { GitOpsColumnManagementControlProps } from './GitOpsColumnManagementControl'; +export { GitOpsColumnManagementControl } from './GitOpsColumnManagementControl'; +export type { GitOpsColumnManagementModalProps } from './GitOpsColumnManagementModal'; +export { GitOpsColumnManagementModal } from './GitOpsColumnManagementModal'; +export type { + GitOpsColumnManagementModalColumn, + GitOpsManagedColumn, + ResolveActiveColumnIdsOptions, + UseGitOpsColumnManagementOptions, +} from './types'; +export { + getGitOpsColumnManagementSettingKey, + GITOPS_COLUMN_MANAGEMENT_SETTING_PREFIX, + NAMESPACE_COLUMN_ID, +} from './types'; +export type { UseGitOpsColumnManagementResult } from './useGitOpsColumnManagement'; +export { useGitOpsColumnManagement } from './useGitOpsColumnManagement'; diff --git a/src/gitops/components/shared/ColumnManagement/types.ts b/src/gitops/components/shared/ColumnManagement/types.ts new file mode 100644 index 000000000..0247b8096 --- /dev/null +++ b/src/gitops/components/shared/ColumnManagement/types.ts @@ -0,0 +1,42 @@ +export const NAMESPACE_COLUMN_ID = 'namespace'; + +export const GITOPS_COLUMN_MANAGEMENT_SETTING_PREFIX = 'gitops.columnManagement.'; + +export const getGitOpsColumnManagementSettingKey = (columnManagementID: string): string => + `${GITOPS_COLUMN_MANAGEMENT_SETTING_PREFIX}${columnManagementID}`; + +export type GitOpsManagedColumn = { + id: string; + title: string; + isShownByDefault: boolean; + isUntoggleable?: boolean; + /** Listed under "Additional columns" in the Manage columns modal. */ + additional?: boolean; + /** Always visible in the table; omitted from the modal (e.g. actions). */ + alwaysShown?: boolean; +}; + +export type GitOpsColumnManagementModalColumn = { + id: string; + title: string; + isShown: boolean; + isShownByDefault: boolean; + isUntoggleable?: boolean; + additional?: boolean; +}; + +export type ResolveActiveColumnIdsOptions = { + columns: GitOpsManagedColumn[]; + savedColumnIds?: string[] | null; + /** When false, omit the namespace column (project-scoped views). */ + includeNamespaceColumn?: boolean; +}; + +export type UseGitOpsColumnManagementOptions = { + columnManagementID: string; + /** Full catalog for prefs; use includeNamespaceColumn to hide Namespace in project views. */ + columns: GitOpsManagedColumn[]; + resourceType: string; + includeNamespaceColumn?: boolean; + showNamespaceHelp?: boolean; +}; diff --git a/src/gitops/components/shared/ColumnManagement/useGitOpsColumnManagement.tsx b/src/gitops/components/shared/ColumnManagement/useGitOpsColumnManagement.tsx new file mode 100644 index 000000000..398b9d109 --- /dev/null +++ b/src/gitops/components/shared/ColumnManagement/useGitOpsColumnManagement.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; + +import { useUserSettings } from '@openshift-console/dynamic-plugin-sdk'; + +import { + filterDataViewColumnsAndRows, + getDefaultActiveColumnIds, + getSavableColumnIds, + resolveActiveColumnIds, + toColumnManagementModalColumns, +} from './columnManagementUtils'; +import { GitOpsColumnManagementControl } from './GitOpsColumnManagementControl'; +import { + type GitOpsColumnManagementModalColumn, + type GitOpsManagedColumn, + type UseGitOpsColumnManagementOptions, + getGitOpsColumnManagementSettingKey, +} from './types'; + +export type { UseGitOpsColumnManagementOptions }; + +export type UseGitOpsColumnManagementResult = { + loaded: boolean; + activeColumnIds: string[]; + activeColumnIdSet: Set; + visibleColumns: GitOpsManagedColumn[]; + isColumnActive: (columnId: string) => boolean; + columnManagement: React.ReactElement; + applyColumns: (modalColumns: GitOpsColumnManagementModalColumn[]) => void; + filterDataView: typeof filterDataViewColumnsAndRows; +}; + +export const useGitOpsColumnManagement = ({ + columnManagementID, + columns, + resourceType, + includeNamespaceColumn = true, + showNamespaceHelp = false, +}: UseGitOpsColumnManagementOptions): UseGitOpsColumnManagementResult => { + const settingKey = getGitOpsColumnManagementSettingKey(columnManagementID); + const defaultIds = React.useMemo(() => getDefaultActiveColumnIds(columns), [columns]); + + const [savedColumnIds, setSavedColumnIds, loaded] = useUserSettings( + settingKey, + defaultIds, + true, + ); + + const activeColumnIds = React.useMemo( + () => + resolveActiveColumnIds({ + columns, + savedColumnIds: loaded ? savedColumnIds : defaultIds, + includeNamespaceColumn, + }), + [columns, savedColumnIds, loaded, defaultIds, includeNamespaceColumn], + ); + + const activeColumnIdSet = React.useMemo(() => new Set(activeColumnIds), [activeColumnIds]); + + const visibleColumns = React.useMemo( + () => columns.filter((column) => activeColumnIdSet.has(column.id)), + [columns, activeColumnIdSet], + ); + + const appliedModalColumns = React.useMemo( + () => toColumnManagementModalColumns(columns, activeColumnIdSet, includeNamespaceColumn), + [columns, activeColumnIdSet, includeNamespaceColumn], + ); + + const applyColumns = React.useCallback( + (modalColumns: GitOpsColumnManagementModalColumn[]) => { + setSavedColumnIds(getSavableColumnIds(columns, modalColumns, savedColumnIds)); + }, + [columns, savedColumnIds, setSavedColumnIds], + ); + + const isColumnActive = React.useCallback( + (columnId: string) => activeColumnIdSet.has(columnId), + [activeColumnIdSet], + ); + + const columnManagement = React.useMemo( + () => ( + + ), + [appliedModalColumns, applyColumns, resourceType, showNamespaceHelp], + ); + + return { + loaded, + activeColumnIds, + activeColumnIdSet, + visibleColumns, + isColumnActive, + columnManagement, + applyColumns, + filterDataView: filterDataViewColumnsAndRows, + }; +}; diff --git a/src/gitops/components/shared/GitOpsListPageToolbar.scss b/src/gitops/components/shared/GitOpsListPageToolbar.scss new file mode 100644 index 000000000..bfda88be4 --- /dev/null +++ b/src/gitops/components/shared/GitOpsListPageToolbar.scss @@ -0,0 +1,53 @@ +.gitops-list-page-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: var(--pf-t--global--spacer--sm); + margin-bottom: var(--pf-t--global--spacer--sm); + + &__primary { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--pf-t--global--spacer--sm); + min-width: 0; + } + + &__filters { + flex: 0 1 auto; + min-width: 0; + + // ListPageFilter's toolbar is full-width by default; shrink so columns sit beside search. + .pf-v6-c-toolbar, + .pf-c-toolbar { + width: auto; + padding-block: 0; + } + + .pf-v6-c-toolbar__content, + .pf-c-toolbar__content { + padding-inline: 0; + } + + .pf-v6-c-toolbar__content-section, + .pf-c-toolbar__content-section { + flex-wrap: wrap; + row-gap: var(--pf-t--global--spacer--sm); + } + } + + &__columns { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + min-height: var(--pf-t--global--spacer--2xl); + } + + &__actions { + display: flex; + flex: 0 0 auto; + align-items: center; + margin-inline-start: auto; + } +} diff --git a/src/gitops/components/shared/GitOpsListPageToolbar.tsx b/src/gitops/components/shared/GitOpsListPageToolbar.tsx new file mode 100644 index 000000000..e8086996e --- /dev/null +++ b/src/gitops/components/shared/GitOpsListPageToolbar.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; + +import './GitOpsListPageToolbar.scss'; + +export type GitOpsListPageToolbarProps = { + filters?: React.ReactNode; + columnManagement?: React.ReactNode; + actions?: React.ReactNode; +}; + +export const GitOpsListPageToolbar: React.FC = ({ + filters, + columnManagement, + actions, +}) => { + if (!filters && !columnManagement && !actions) { + return null; + } + + return ( +
+
+ {filters &&
{filters}
} + {columnManagement && ( +
{columnManagement}
+ )} +
+ {actions &&
{actions}
} +
+ ); +}; diff --git a/src/gitops/components/shared/applicationListColumns.test.ts b/src/gitops/components/shared/applicationListColumns.test.ts new file mode 100644 index 000000000..f78ad3109 --- /dev/null +++ b/src/gitops/components/shared/applicationListColumns.test.ts @@ -0,0 +1,70 @@ +import { + APPLICATION_LIST_COLUMN_MANAGEMENT_ID, + getApplicationManagedColumns, +} from './applicationListColumns'; +import { getDefaultActiveColumnIds } from './ColumnManagement'; + +const t = (key: string) => key; + +describe('APPLICATION_LIST_COLUMN_MANAGEMENT_ID', () => { + it('uses a stable user-settings key', () => { + expect(APPLICATION_LIST_COLUMN_MANAGEMENT_ID).toBe('gitops.applications'); + }); +}); + +describe('getApplicationManagedColumns', () => { + it('locks Name, keeps Labels default-visible, and always shows actions', () => { + const columns = getApplicationManagedColumns(true, t); + const byId = Object.fromEntries(columns.map((column) => [column.id, column])); + + expect(byId.name.isUntoggleable).toBe(true); + expect(byId.labels.isShownByDefault).toBe(true); + expect(byId.actions.alwaysShown).toBe(true); + }); + + it('puts Revision and App Project under Additional columns', () => { + const columns = getApplicationManagedColumns(true, t); + const byId = Object.fromEntries(columns.map((column) => [column.id, column])); + + expect(byId.revision).toMatchObject({ isShownByDefault: false, additional: true }); + expect(byId.project).toMatchObject({ isShownByDefault: false, additional: true }); + expect(columns.filter((column) => column.additional).map((column) => column.id)).toEqual([ + 'revision', + 'project', + ]); + }); + + it('includes Namespace only for All projects', () => { + expect(getApplicationManagedColumns(true, t).map((column) => column.id)).toEqual([ + 'name', + 'namespace', + 'sync-status', + 'health-status', + 'revision', + 'labels', + 'project', + 'actions', + ]); + expect(getApplicationManagedColumns(false, t).map((column) => column.id)).toEqual([ + 'name', + 'sync-status', + 'health-status', + 'revision', + 'labels', + 'project', + 'actions', + ]); + }); + + it('defaults to Default columns only (excludes Additional)', () => { + const columns = getApplicationManagedColumns(true, t); + expect(getDefaultActiveColumnIds(columns)).toEqual([ + 'name', + 'namespace', + 'sync-status', + 'health-status', + 'labels', + 'actions', + ]); + }); +}); diff --git a/src/gitops/components/shared/applicationListColumns.ts b/src/gitops/components/shared/applicationListColumns.ts new file mode 100644 index 000000000..1eb4fb790 --- /dev/null +++ b/src/gitops/components/shared/applicationListColumns.ts @@ -0,0 +1,28 @@ +import { type GitOpsManagedColumn, NAMESPACE_COLUMN_ID } from './ColumnManagement/types'; + +export const APPLICATION_LIST_COLUMN_MANAGEMENT_ID = 'gitops.applications'; + +export const getApplicationManagedColumns = ( + includeNamespaceColumn: boolean, + t: (key: string) => string, +): GitOpsManagedColumn[] => { + const columns: GitOpsManagedColumn[] = [ + { id: 'name', title: t('Name'), isShownByDefault: true, isUntoggleable: true }, + ]; + if (includeNamespaceColumn) { + columns.push({ + id: NAMESPACE_COLUMN_ID, + title: t('Namespace'), + isShownByDefault: true, + }); + } + columns.push( + { id: 'sync-status', title: t('Sync Status'), isShownByDefault: true }, + { id: 'health-status', title: t('Health Status'), isShownByDefault: true }, + { id: 'revision', title: t('Revision'), isShownByDefault: false, additional: true }, + { id: 'labels', title: t('Labels'), isShownByDefault: true }, + { id: 'project', title: t('App Project'), isShownByDefault: false, additional: true }, + { id: 'actions', title: '', isShownByDefault: true, alwaysShown: true }, + ); + return columns; +}; diff --git a/src/gitops/components/shared/applicationSetListColumns.test.ts b/src/gitops/components/shared/applicationSetListColumns.test.ts new file mode 100644 index 000000000..0ec68bd56 --- /dev/null +++ b/src/gitops/components/shared/applicationSetListColumns.test.ts @@ -0,0 +1,70 @@ +import { + APPLICATION_SET_LIST_COLUMN_MANAGEMENT_ID, + getApplicationSetManagedColumns, +} from './applicationSetListColumns'; +import { getDefaultActiveColumnIds } from './ColumnManagement'; + +const t = (key: string) => key; + +describe('APPLICATION_SET_LIST_COLUMN_MANAGEMENT_ID', () => { + it('uses a stable user-settings key', () => { + expect(APPLICATION_SET_LIST_COLUMN_MANAGEMENT_ID).toBe('gitops.applicationsets'); + }); +}); + +describe('getApplicationSetManagedColumns', () => { + it('locks Name, keeps Labels default-visible, and always shows actions', () => { + const columns = getApplicationSetManagedColumns(true, t); + const byId = Object.fromEntries(columns.map((column) => [column.id, column])); + + expect(byId.name.isUntoggleable).toBe(true); + expect(byId.labels.isShownByDefault).toBe(true); + expect(byId.actions.alwaysShown).toBe(true); + }); + + it('puts Generators and Created At under Additional columns', () => { + const columns = getApplicationSetManagedColumns(true, t); + const byId = Object.fromEntries(columns.map((column) => [column.id, column])); + + expect(byId.generators).toMatchObject({ isShownByDefault: false, additional: true }); + expect(byId['created-at']).toMatchObject({ isShownByDefault: false, additional: true }); + expect(columns.filter((column) => column.additional).map((column) => column.id)).toEqual([ + 'generators', + 'created-at', + ]); + }); + + it('includes Namespace only for All projects', () => { + expect(getApplicationSetManagedColumns(true, t).map((column) => column.id)).toEqual([ + 'name', + 'namespace', + 'status', + 'generated-apps', + 'generators', + 'labels', + 'created-at', + 'actions', + ]); + expect(getApplicationSetManagedColumns(false, t).map((column) => column.id)).toEqual([ + 'name', + 'status', + 'generated-apps', + 'generators', + 'labels', + 'created-at', + 'actions', + ]); + }); + + it('defaults to Default columns only (excludes Additional)', () => { + const columns = getApplicationSetManagedColumns(true, t); + expect(getDefaultActiveColumnIds(columns)).toEqual([ + 'name', + 'namespace', + 'status', + 'generated-apps', + 'labels', + 'actions', + ]); + }); +}); diff --git a/src/gitops/components/shared/applicationSetListColumns.ts b/src/gitops/components/shared/applicationSetListColumns.ts new file mode 100644 index 000000000..101feeaa3 --- /dev/null +++ b/src/gitops/components/shared/applicationSetListColumns.ts @@ -0,0 +1,28 @@ +import { type GitOpsManagedColumn, NAMESPACE_COLUMN_ID } from './ColumnManagement/types'; + +export const APPLICATION_SET_LIST_COLUMN_MANAGEMENT_ID = 'gitops.applicationsets'; + +export const getApplicationSetManagedColumns = ( + includeNamespaceColumn: boolean, + t: (key: string) => string, +): GitOpsManagedColumn[] => { + const columns: GitOpsManagedColumn[] = [ + { id: 'name', title: t('Name'), isShownByDefault: true, isUntoggleable: true }, + ]; + if (includeNamespaceColumn) { + columns.push({ + id: NAMESPACE_COLUMN_ID, + title: t('Namespace'), + isShownByDefault: true, + }); + } + columns.push( + { id: 'status', title: t('Health Status'), isShownByDefault: true }, + { id: 'generated-apps', title: t('Generated Apps'), isShownByDefault: true }, + { id: 'generators', title: t('Generators'), isShownByDefault: false, additional: true }, + { id: 'labels', title: t('Labels'), isShownByDefault: true }, + { id: 'created-at', title: t('Created At'), isShownByDefault: false, additional: true }, + { id: 'actions', title: '', isShownByDefault: true, alwaysShown: true }, + ); + return columns; +}; diff --git a/src/gitops/components/shared/imageUpdaterListColumns.test.ts b/src/gitops/components/shared/imageUpdaterListColumns.test.ts new file mode 100644 index 000000000..e8512232b --- /dev/null +++ b/src/gitops/components/shared/imageUpdaterListColumns.test.ts @@ -0,0 +1,69 @@ +import { getDefaultActiveColumnIds } from './ColumnManagement'; +import { + getImageUpdaterManagedColumns, + IMAGE_UPDATER_LIST_COLUMN_MANAGEMENT_ID, +} from './imageUpdaterListColumns'; + +const t = (key: string) => key; + +describe('IMAGE_UPDATER_LIST_COLUMN_MANAGEMENT_ID', () => { + it('uses a stable user-settings key', () => { + expect(IMAGE_UPDATER_LIST_COLUMN_MANAGEMENT_ID).toBe('gitops.imageupdaters'); + }); +}); + +describe('getImageUpdaterManagedColumns', () => { + it('locks Name, keeps Labels default-visible, and always shows actions', () => { + const columns = getImageUpdaterManagedColumns(true, t); + const byId = Object.fromEntries(columns.map((column) => [column.id, column])); + + expect(byId.name.isUntoggleable).toBe(true); + expect(byId.labels.isShownByDefault).toBe(true); + expect(byId.actions.alwaysShown).toBe(true); + }); + + it('puts Last Checked under Additional columns', () => { + const columns = getImageUpdaterManagedColumns(true, t); + const byId = Object.fromEntries(columns.map((column) => [column.id, column])); + + expect(byId['last-checked']).toMatchObject({ isShownByDefault: false, additional: true }); + expect(columns.filter((column) => column.additional).map((column) => column.id)).toEqual([ + 'last-checked', + ]); + }); + + it('includes Namespace only for All projects', () => { + expect(getImageUpdaterManagedColumns(true, t).map((column) => column.id)).toEqual([ + 'name', + 'namespace', + 'apps', + 'images', + 'last-checked', + 'ready', + 'labels', + 'actions', + ]); + expect(getImageUpdaterManagedColumns(false, t).map((column) => column.id)).toEqual([ + 'name', + 'apps', + 'images', + 'last-checked', + 'ready', + 'labels', + 'actions', + ]); + }); + + it('defaults to Default columns only (excludes Additional)', () => { + const columns = getImageUpdaterManagedColumns(true, t); + expect(getDefaultActiveColumnIds(columns)).toEqual([ + 'name', + 'namespace', + 'apps', + 'images', + 'ready', + 'labels', + 'actions', + ]); + }); +}); diff --git a/src/gitops/components/shared/imageUpdaterListColumns.ts b/src/gitops/components/shared/imageUpdaterListColumns.ts new file mode 100644 index 000000000..c62695b83 --- /dev/null +++ b/src/gitops/components/shared/imageUpdaterListColumns.ts @@ -0,0 +1,28 @@ +import { type GitOpsManagedColumn, NAMESPACE_COLUMN_ID } from './ColumnManagement/types'; + +export const IMAGE_UPDATER_LIST_COLUMN_MANAGEMENT_ID = 'gitops.imageupdaters'; + +export const getImageUpdaterManagedColumns = ( + includeNamespaceColumn: boolean, + t: (key: string) => string, +): GitOpsManagedColumn[] => { + const columns: GitOpsManagedColumn[] = [ + { id: 'name', title: t('Name'), isShownByDefault: true, isUntoggleable: true }, + ]; + if (includeNamespaceColumn) { + columns.push({ + id: NAMESPACE_COLUMN_ID, + title: t('Namespace'), + isShownByDefault: true, + }); + } + columns.push( + { id: 'apps', title: t('Apps'), isShownByDefault: true }, + { id: 'images', title: t('Images'), isShownByDefault: true }, + { id: 'last-checked', title: t('Last Checked'), isShownByDefault: false, additional: true }, + { id: 'ready', title: t('Ready'), isShownByDefault: true }, + { id: 'labels', title: t('Labels'), isShownByDefault: true }, + { id: 'actions', title: '', isShownByDefault: true, alwaysShown: true }, + ); + return columns; +}; diff --git a/src/gitops/components/shared/projectListColumns.test.ts b/src/gitops/components/shared/projectListColumns.test.ts new file mode 100644 index 000000000..b169bf0b3 --- /dev/null +++ b/src/gitops/components/shared/projectListColumns.test.ts @@ -0,0 +1,64 @@ +import { getDefaultActiveColumnIds } from './ColumnManagement'; +import { getProjectManagedColumns, PROJECT_LIST_COLUMN_MANAGEMENT_ID } from './projectListColumns'; + +const t = (key: string) => key; + +describe('PROJECT_LIST_COLUMN_MANAGEMENT_ID', () => { + it('uses a stable user-settings key', () => { + expect(PROJECT_LIST_COLUMN_MANAGEMENT_ID).toBe('gitops.appprojects'); + }); +}); + +describe('getProjectManagedColumns', () => { + it('locks Name, keeps Labels default-visible, and always shows actions', () => { + const columns = getProjectManagedColumns(true, t); + const byId = Object.fromEntries(columns.map((column) => [column.id, column])); + + expect(byId.name.isUntoggleable).toBe(true); + expect(byId.labels.isShownByDefault).toBe(true); + expect(byId.actions.alwaysShown).toBe(true); + }); + + it('puts Description and Last Updated under Additional columns', () => { + const columns = getProjectManagedColumns(true, t); + const byId = Object.fromEntries(columns.map((column) => [column.id, column])); + + expect(byId.description).toMatchObject({ isShownByDefault: false, additional: true }); + expect(byId['last-updated']).toMatchObject({ isShownByDefault: false, additional: true }); + expect(columns.filter((column) => column.additional).map((column) => column.id)).toEqual([ + 'description', + 'last-updated', + ]); + }); + + it('includes Namespace only for All projects', () => { + expect(getProjectManagedColumns(true, t).map((column) => column.id)).toEqual([ + 'name', + 'namespace', + 'description', + 'applications', + 'labels', + 'last-updated', + 'actions', + ]); + expect(getProjectManagedColumns(false, t).map((column) => column.id)).toEqual([ + 'name', + 'description', + 'applications', + 'labels', + 'last-updated', + 'actions', + ]); + }); + + it('defaults to Default columns only (excludes Additional)', () => { + const columns = getProjectManagedColumns(true, t); + expect(getDefaultActiveColumnIds(columns)).toEqual([ + 'name', + 'namespace', + 'applications', + 'labels', + 'actions', + ]); + }); +}); diff --git a/src/gitops/components/shared/projectListColumns.ts b/src/gitops/components/shared/projectListColumns.ts new file mode 100644 index 000000000..e90c6f4d4 --- /dev/null +++ b/src/gitops/components/shared/projectListColumns.ts @@ -0,0 +1,27 @@ +import { type GitOpsManagedColumn, NAMESPACE_COLUMN_ID } from './ColumnManagement/types'; + +export const PROJECT_LIST_COLUMN_MANAGEMENT_ID = 'gitops.appprojects'; + +export const getProjectManagedColumns = ( + includeNamespaceColumn: boolean, + t: (key: string) => string, +): GitOpsManagedColumn[] => { + const columns: GitOpsManagedColumn[] = [ + { id: 'name', title: t('Name'), isShownByDefault: true, isUntoggleable: true }, + ]; + if (includeNamespaceColumn) { + columns.push({ + id: NAMESPACE_COLUMN_ID, + title: t('Namespace'), + isShownByDefault: true, + }); + } + columns.push( + { id: 'description', title: t('Description'), isShownByDefault: false, additional: true }, + { id: 'applications', title: t('Applications'), isShownByDefault: true }, + { id: 'labels', title: t('Labels'), isShownByDefault: true }, + { id: 'last-updated', title: t('Last Updated'), isShownByDefault: false, additional: true }, + { id: 'actions', title: '', isShownByDefault: true, alwaysShown: true }, + ); + return columns; +}; diff --git a/src/gitops/components/shared/rolloutListColumns.test.ts b/src/gitops/components/shared/rolloutListColumns.test.ts new file mode 100644 index 000000000..79e669259 --- /dev/null +++ b/src/gitops/components/shared/rolloutListColumns.test.ts @@ -0,0 +1,67 @@ +import { getDefaultActiveColumnIds } from './ColumnManagement'; +import { getRolloutManagedColumns, ROLLOUT_LIST_COLUMN_MANAGEMENT_ID } from './rolloutListColumns'; + +const t = (key: string) => key; + +describe('ROLLOUT_LIST_COLUMN_MANAGEMENT_ID', () => { + it('uses a stable user-settings key', () => { + expect(ROLLOUT_LIST_COLUMN_MANAGEMENT_ID).toBe('gitops.rollouts'); + }); +}); + +describe('getRolloutManagedColumns', () => { + it('locks Name, keeps Labels default-visible, and always shows actions', () => { + const columns = getRolloutManagedColumns(true, t); + const byId = Object.fromEntries(columns.map((column) => [column.id, column])); + + expect(byId.name.isUntoggleable).toBe(true); + expect(byId.labels.isShownByDefault).toBe(true); + expect(byId.actions.alwaysShown).toBe(true); + }); + + it('puts Selector and Last Updated under Additional columns', () => { + const columns = getRolloutManagedColumns(true, t); + const byId = Object.fromEntries(columns.map((column) => [column.id, column])); + + expect(byId.selector).toMatchObject({ isShownByDefault: false, additional: true }); + expect(byId['last-updated']).toMatchObject({ isShownByDefault: false, additional: true }); + expect(columns.filter((column) => column.additional).map((column) => column.id)).toEqual([ + 'selector', + 'last-updated', + ]); + }); + + it('includes Namespace only for All projects', () => { + expect(getRolloutManagedColumns(true, t).map((column) => column.id)).toEqual([ + 'name', + 'namespace', + 'status', + 'pods', + 'labels', + 'selector', + 'last-updated', + 'actions', + ]); + expect(getRolloutManagedColumns(false, t).map((column) => column.id)).toEqual([ + 'name', + 'status', + 'pods', + 'labels', + 'selector', + 'last-updated', + 'actions', + ]); + }); + + it('defaults to Default columns only (excludes Additional)', () => { + const columns = getRolloutManagedColumns(true, t); + expect(getDefaultActiveColumnIds(columns)).toEqual([ + 'name', + 'namespace', + 'status', + 'pods', + 'labels', + 'actions', + ]); + }); +}); diff --git a/src/gitops/components/shared/rolloutListColumns.ts b/src/gitops/components/shared/rolloutListColumns.ts new file mode 100644 index 000000000..d8817f35d --- /dev/null +++ b/src/gitops/components/shared/rolloutListColumns.ts @@ -0,0 +1,28 @@ +import { type GitOpsManagedColumn, NAMESPACE_COLUMN_ID } from './ColumnManagement/types'; + +export const ROLLOUT_LIST_COLUMN_MANAGEMENT_ID = 'gitops.rollouts'; + +export const getRolloutManagedColumns = ( + includeNamespaceColumn: boolean, + t: (key: string) => string, +): GitOpsManagedColumn[] => { + const columns: GitOpsManagedColumn[] = [ + { id: 'name', title: t('Name'), isShownByDefault: true, isUntoggleable: true }, + ]; + if (includeNamespaceColumn) { + columns.push({ + id: NAMESPACE_COLUMN_ID, + title: t('Namespace'), + isShownByDefault: true, + }); + } + columns.push( + { id: 'status', title: t('Status'), isShownByDefault: true }, + { id: 'pods', title: t('Pods'), isShownByDefault: true }, + { id: 'labels', title: t('Labels'), isShownByDefault: true }, + { id: 'selector', title: t('Selector'), isShownByDefault: false, additional: true }, + { id: 'last-updated', title: t('Last Updated'), isShownByDefault: false, additional: true }, + { id: 'actions', title: '', isShownByDefault: true, alwaysShown: true }, + ); + return columns; +}; diff --git a/src/gitops/models/ApplicationModel.ts b/src/gitops/models/ApplicationModel.ts index e9e521957..8c9ff3414 100644 --- a/src/gitops/models/ApplicationModel.ts +++ b/src/gitops/models/ApplicationModel.ts @@ -136,6 +136,8 @@ export type OperationState = { export type CurrentSyncStatus = { revision?: string; + /** Multi-source Applications (one entry per source). */ + revisions?: string[]; status?: SyncStatus; };