diff --git a/console-extensions.json b/console-extensions.json index f2f3587b..df049e22 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -351,6 +351,29 @@ } } }, + { + "type": "console.navigation/resource-ns", + "properties": { + "id": "appproject", + "name": "AppProjects", + "perspective": "admin", + "section": "gitops-navigation-section", + "model": { + "group": "argoproj.io", + "kind": "AppProject", + "version": "v1alpha1" + } + } + }, + { + "type": "console.navigation/separator", + "properties": { + "perspective": "admin", + "section": "gitops-navigation-section", + "id": "argocd-separator", + "insertAfter": "appproject" + } + }, { "type": "console.navigation/resource-ns", "properties": { @@ -401,6 +424,24 @@ } } }, + { + "type": "console.page/resource/list", + "flags": { + "required": [ + "APPLICATION" + ] + }, + "properties": { + "model": { + "group": "argoproj.io", + "kind": "AppProject", + "version": "v1alpha1" + }, + "component": { + "$codeRef": "ProjectList" + } + } + }, { "type": "console.page/resource/details", "flags": { @@ -512,7 +553,25 @@ } } }, - + { + "type": "console.yaml-template", + "flags": { + "required": [ + "APPLICATION" + ] + }, + "properties": { + "name": "default", + "model": { + "group": "argoproj.io", + "kind": "AppProject", + "version": "v1alpha1" + }, + "template": { + "$codeRef": "yamlTemplates.defaultAppProjectYamlTemplate" + } + } + }, { "type": "console.page/resource/details", "flags": { diff --git a/locales/en/plugin__gitops-plugin.json b/locales/en/plugin__gitops-plugin.json index 5640f4b2..8ed82ae7 100644 --- a/locales/en/plugin__gitops-plugin.json +++ b/locales/en/plugin__gitops-plugin.json @@ -127,14 +127,42 @@ "App Project": "App Project", "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.", - "Applications": "Argo CD Applications", + "Applications": "ArgoCD Applications", + "Edit labels": "Edit labels", + "Edit annotations": "Edit annotations", + "Edit App Project": "Edit App Project", + "Delete": "Delete", + "No Argo CD App Projects match the search filter": "No Argo CD App Projects match the search filter", + "Try removing the filter or searching for a different term to see more App Projects.": "Try removing the filter or searching for a different term to see more App Projects.", + "There are no Argo CD App Projects in this project.": "There are no Argo CD App Projects in this project.", + "There are no Argo CD App Projects in all projects.": "There are no Argo CD App Projects in all projects.", + "No matching Argo CD App Projects": "No matching Argo CD App Projects", + "No Argo CD App Projects": "No Argo CD App Projects", + "Unable to load data": "Unable to load data", + "There was an error retrieving App Projects. Check your connection and reload the page.": "There was an error retrieving App Projects. Check your connection and reload the page.", + "App Projects": "App Projects", + "Create App Project": "Create App Project", + "Search by name...": "Search by name...", + "Description": "Description", + "Last Updated": "Last Updated", + "Has Description": "Has Description", + "No Description": "No Description", + "Has Applications": "Has Applications", + "No Applications": "No Applications", + "Project Type": "Project Type", + "Default Project": "Default Project", + "Custom Projects": "Custom Projects", + "Source Repositories": "Source Repositories", + "Has Source Repos": "Has Source Repos", + "No Source Repos": "No Source Repos", + "Destinations": "Destinations", + "Has Destinations": "Has Destinations", + "No Destinations": "No Destinations", "Promote": "Promote", "Full Promote": "Full Promote", "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", - "Edit labels": "Edit labels", - "Edit annotations": "Edit annotations", "Edit Rollout": "Edit Rollout", "Delete": "Delete", "Rollout details": "Rollout details", @@ -149,7 +177,6 @@ "No Argo Rollouts": "No Argo Rollouts", "There are no Argo Rollouts in this project.": "There are no Argo Rollouts in this project.", "There are no Argo Rollouts in all projects.": "There are no Argo Rollouts in all projects.", - "Unable to load data": "Unable to load data", "There was an error retrieving applications. Check your connection and reload the page.": "There was an error retrieving applications. Check your connection and reload the page.", "Argo Rollouts": "Argo Rollouts", "Rollouts": "Argo Rollouts", @@ -158,7 +185,6 @@ "Pods": "Pods", "Labels": "Labels", "Selector": "Selector", - "Last Updated": "Last Updated", "No labels": "No labels", "Rollout Status": "Rollout Status", "Revisions": "Revisions", @@ -180,7 +206,6 @@ "Try removing the filter or selecting a different label to see more applications.": "Try removing the filter or selecting a different label to see more applications.", "There are no Argo CD Applications in this project.": "There are no Argo CD Applications in this project.", "There are no Argo CD Applications in all projects.": "There are no Argo CD Applications in all projects.", - "Search by name...": "Search by name...", "No Argo CD ApplicationSets match the label filter": "No Argo CD ApplicationSets match the label filter", "Try removing the filter or selecting a different label to see more ApplicationSets.": "Try removing the filter or selecting a different label to see more ApplicationSets.", "There are no Argo CD ApplicationSets in this project.": "There are no Argo CD ApplicationSets in this project.", diff --git a/locales/ja/plugin__gitops-plugin.json b/locales/ja/plugin__gitops-plugin.json index 900745e7..131a5dbf 100644 --- a/locales/ja/plugin__gitops-plugin.json +++ b/locales/ja/plugin__gitops-plugin.json @@ -128,13 +128,41 @@ "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.", "Applications": "Applications", + "Edit labels": "Edit labels", + "Edit annotations": "Edit annotations", + "Edit App Project": "Edit App Project", + "Delete": "Delete", + "No Argo CD App Projects match the search filter": "No Argo CD App Projects match the search filter", + "Try removing the filter or searching for a different term to see more App Projects.": "Try removing the filter or searching for a different term to see more App Projects.", + "There are no Argo CD App Projects in this project.": "There are no Argo CD App Projects in this project.", + "There are no Argo CD App Projects in all projects.": "There are no Argo CD App Projects in all projects.", + "No matching Argo CD App Projects": "No matching Argo CD App Projects", + "No Argo CD App Projects": "No Argo CD App Projects", + "Unable to load data": "Unable to load data", + "There was an error retrieving App Projects. Check your connection and reload the page.": "There was an error retrieving App Projects. Check your connection and reload the page.", + "App Projects": "App Projects", + "Create App Project": "Create App Project", + "Search by name...": "Search by name...", + "Description": "Description", + "Last Updated": "Last Updated", + "Has Description": "Has Description", + "No Description": "No Description", + "Has Applications": "Has Applications", + "No Applications": "No Applications", + "Project Type": "Project Type", + "Default Project": "Default Project", + "Custom Projects": "Custom Projects", + "Source Repositories": "Source Repositories", + "Has Source Repos": "Has Source Repos", + "No Source Repos": "No Source Repos", + "Destinations": "Destinations", + "Has Destinations": "Has Destinations", + "No Destinations": "No Destinations", "Promote": "Promote", "Full Promote": "Full Promote", "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", - "Edit labels": "Edit labels", - "Edit annotations": "Edit annotations", "Edit Rollout": "Edit Rollout", "Delete": "Delete", "Rollout details": "Rollout details", @@ -149,7 +177,6 @@ "No Argo Rollouts": "No Argo Rollouts", "There are no Argo Rollouts in this project.": "There are no Argo Rollouts in this project.", "There are no Argo Rollouts in all projects.": "There are no Argo Rollouts in all projects.", - "Unable to load data": "Unable to load data", "There was an error retrieving applications. Check your connection and reload the page.": "There was an error retrieving applications. Check your connection and reload the page.", "Argo Rollouts": "Argo Rollouts", "Create Rollout": "Create Rollout", @@ -157,7 +184,6 @@ "Pods": "Pods", "Labels": "Labels", "Selector": "Selector", - "Last Updated": "Last Updated", "No labels": "No labels", "Rollout Status": "Rollout Status", "Revisions": "Revisions", @@ -179,7 +205,6 @@ "Try removing the filter or selecting a different label to see more applications.": "Try removing the filter or selecting a different label to see more applications.", "There are no Argo CD Applications in this project.": "There are no Argo CD Applications in this project.", "There are no Argo CD Applications in all projects.": "There are no Argo CD Applications in all projects.", - "Search by name...": "Search by name...", "No Argo CD ApplicationSets match the label filter": "No Argo CD ApplicationSets match the label filter", "Try removing the filter or selecting a different label to see more ApplicationSets.": "Try removing the filter or selecting a different label to see more ApplicationSets.", "There are no Argo CD ApplicationSets in this project.": "There are no Argo CD ApplicationSets in this project.", diff --git a/locales/ko/plugin__gitops-plugin.json b/locales/ko/plugin__gitops-plugin.json index 6d181961..c85d93aa 100644 --- a/locales/ko/plugin__gitops-plugin.json +++ b/locales/ko/plugin__gitops-plugin.json @@ -128,13 +128,41 @@ "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.", "Applications": "Applications", + "Edit labels": "Edit labels", + "Edit annotations": "Edit annotations", + "Edit App Project": "Edit App Project", + "Delete": "Delete", + "No Argo CD App Projects match the search filter": "No Argo CD App Projects match the search filter", + "Try removing the filter or searching for a different term to see more App Projects.": "Try removing the filter or searching for a different term to see more App Projects.", + "There are no Argo CD App Projects in this project.": "There are no Argo CD App Projects in this project.", + "There are no Argo CD App Projects in all projects.": "There are no Argo CD App Projects in all projects.", + "No matching Argo CD App Projects": "No matching Argo CD App Projects", + "No Argo CD App Projects": "No Argo CD App Projects", + "Unable to load data": "Unable to load data", + "There was an error retrieving App Projects. Check your connection and reload the page.": "There was an error retrieving App Projects. Check your connection and reload the page.", + "App Projects": "App Projects", + "Create App Project": "Create App Project", + "Search by name...": "Search by name...", + "Description": "Description", + "Last Updated": "Last Updated", + "Has Description": "Has Description", + "No Description": "No Description", + "Has Applications": "Has Applications", + "No Applications": "No Applications", + "Project Type": "Project Type", + "Default Project": "Default Project", + "Custom Projects": "Custom Projects", + "Source Repositories": "Source Repositories", + "Has Source Repos": "Has Source Repos", + "No Source Repos": "No Source Repos", + "Destinations": "Destinations", + "Has Destinations": "Has Destinations", + "No Destinations": "No Destinations", "Promote": "Promote", "Full Promote": "Full Promote", "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", - "Edit labels": "Edit labels", - "Edit annotations": "Edit annotations", "Edit Rollout": "Edit Rollout", "Delete": "Delete", "Rollout details": "Rollout details", @@ -149,7 +177,6 @@ "No Argo Rollouts": "No Argo Rollouts", "There are no Argo Rollouts in this project.": "There are no Argo Rollouts in this project.", "There are no Argo Rollouts in all projects.": "There are no Argo Rollouts in all projects.", - "Unable to load data": "Unable to load data", "There was an error retrieving applications. Check your connection and reload the page.": "There was an error retrieving applications. Check your connection and reload the page.", "Argo Rollouts": "Argo Rollouts", "Create Rollout": "Create Rollout", @@ -157,7 +184,6 @@ "Pods": "Pods", "Labels": "Labels", "Selector": "Selector", - "Last Updated": "Last Updated", "No labels": "No labels", "Rollout Status": "Rollout Status", "Revisions": "Revisions", @@ -179,7 +205,6 @@ "Try removing the filter or selecting a different label to see more applications.": "Try removing the filter or selecting a different label to see more applications.", "There are no Argo CD Applications in this project.": "There are no Argo CD Applications in this project.", "There are no Argo CD Applications in all projects.": "There are no Argo CD Applications in all projects.", - "Search by name...": "Search by name...", "No Argo CD ApplicationSets match the label filter": "No Argo CD ApplicationSets match the label filter", "Try removing the filter or selecting a different label to see more ApplicationSets.": "Try removing the filter or selecting a different label to see more ApplicationSets.", "There are no Argo CD ApplicationSets in this project.": "There are no Argo CD ApplicationSets in this project.", diff --git a/locales/zh/plugin__gitops-plugin.json b/locales/zh/plugin__gitops-plugin.json index 9f6b8537..530c5a62 100644 --- a/locales/zh/plugin__gitops-plugin.json +++ b/locales/zh/plugin__gitops-plugin.json @@ -128,13 +128,41 @@ "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.", "Applications": "Applications", + "Edit labels": "Edit labels", + "Edit annotations": "Edit annotations", + "Edit App Project": "Edit App Project", + "Delete": "Delete", + "No Argo CD App Projects match the search filter": "No Argo CD App Projects match the search filter", + "Try removing the filter or searching for a different term to see more App Projects.": "Try removing the filter or searching for a different term to see more App Projects.", + "There are no Argo CD App Projects in this project.": "There are no Argo CD App Projects in this project.", + "There are no Argo CD App Projects in all projects.": "There are no Argo CD App Projects in all projects.", + "No matching Argo CD App Projects": "No matching Argo CD App Projects", + "No Argo CD App Projects": "No Argo CD App Projects", + "Unable to load data": "Unable to load data", + "There was an error retrieving App Projects. Check your connection and reload the page.": "There was an error retrieving App Projects. Check your connection and reload the page.", + "App Projects": "App Projects", + "Create App Project": "Create App Project", + "Search by name...": "Search by name...", + "Description": "Description", + "Last Updated": "Last Updated", + "Has Description": "Has Description", + "No Description": "No Description", + "Has Applications": "Has Applications", + "No Applications": "No Applications", + "Project Type": "Project Type", + "Default Project": "Default Project", + "Custom Projects": "Custom Projects", + "Source Repositories": "Source Repositories", + "Has Source Repos": "Has Source Repos", + "No Source Repos": "No Source Repos", + "Destinations": "Destinations", + "Has Destinations": "Has Destinations", + "No Destinations": "No Destinations", "Promote": "Promote", "Full Promote": "Full Promote", "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", - "Edit labels": "Edit labels", - "Edit annotations": "Edit annotations", "Edit Rollout": "Edit Rollout", "Delete": "Delete", "Rollout details": "Rollout details", @@ -149,7 +177,6 @@ "No Argo Rollouts": "No Argo Rollouts", "There are no Argo Rollouts in this project.": "There are no Argo Rollouts in this project.", "There are no Argo Rollouts in all projects.": "There are no Argo Rollouts in all projects.", - "Unable to load data": "Unable to load data", "There was an error retrieving applications. Check your connection and reload the page.": "There was an error retrieving applications. Check your connection and reload the page.", "Argo Rollouts": "Argo Rollouts", "Create Rollout": "Create Rollout", @@ -157,7 +184,6 @@ "Pods": "Pods", "Labels": "Labels", "Selector": "Selector", - "Last Updated": "Last Updated", "No labels": "No labels", "Rollout Status": "Rollout Status", "Revisions": "Revisions", @@ -179,7 +205,6 @@ "Try removing the filter or selecting a different label to see more applications.": "Try removing the filter or selecting a different label to see more applications.", "There are no Argo CD Applications in this project.": "There are no Argo CD Applications in this project.", "There are no Argo CD Applications in all projects.": "There are no Argo CD Applications in all projects.", - "Search by name...": "Search by name...", "No Argo CD ApplicationSets match the label filter": "No Argo CD ApplicationSets match the label filter", "Try removing the filter or selecting a different label to see more ApplicationSets.": "Try removing the filter or selecting a different label to see more ApplicationSets.", "There are no Argo CD ApplicationSets in this project.": "There are no Argo CD ApplicationSets in this project.", diff --git a/plugin-metadata.ts b/plugin-metadata.ts index 7ec53449..b2d6c4e5 100644 --- a/plugin-metadata.ts +++ b/plugin-metadata.ts @@ -19,6 +19,7 @@ const metadata: ConsolePluginBuildMetadata = { RolloutDetails: "./gitops/components/rollout/RolloutNavPage.tsx", ApplicationSetList: "./gitops/components/application/ApplicationSetListTab.tsx", ApplicationSetDetailsPage: "./gitops/components/appset/ApplicationSetDetailsPage.tsx", + ProjectList: "./gitops/components/project/ProjectListTab.tsx", yamlTemplates: "./gitops/templates/index.ts" } }; diff --git a/src/gitops/components/project/ProjectList.tsx b/src/gitops/components/project/ProjectList.tsx new file mode 100644 index 00000000..9b733305 --- /dev/null +++ b/src/gitops/components/project/ProjectList.tsx @@ -0,0 +1,652 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom-v5-compat'; +import classNames from 'classnames'; +import DevPreviewBadge from 'src/components/import/badges/DevPreviewBadge'; + +import ActionsDropdown from '@gitops/utils/components/ActionDropDown/ActionDropDown'; +import { + getSelectorSearchURL, + kindForReference, + modelToGroupVersionKind, + modelToRef, +} from '@gitops/utils/utils'; +import { + Action, + K8sResourceCommon, + K8sResourceKindReference, + ListPageBody, + ListPageCreate, + ListPageFilter, + ListPageHeader, + RowFilter, + Timestamp, + useK8sWatchResource, + useListPageFilter, +} from '@openshift-console/dynamic-plugin-sdk'; +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { ErrorState } from '@patternfly/react-component-groups'; +import { EmptyState, EmptyStateBody, LabelGroup } from '@patternfly/react-core'; +import { Label as PfLabel } from '@patternfly/react-core'; +import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/DataViewTable'; +import { CubesIcon } from '@patternfly/react-icons'; +import { ThProps } from '@patternfly/react-table'; +import { Tbody, Td, Tr } from '@patternfly/react-table'; + +import { ApplicationKind } from '../../models/ApplicationModel'; +import { AppProjectKind, AppProjectModel } from '../../models/AppProjectModel'; +import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; + +import { useProjectActionsProvider } from './hooks/useProjectActionsProvider'; + +import './project-list.scss'; + +type ProjectListTabProps = { + namespace?: string; + hideNameLabelFilters?: boolean; + showTitle?: boolean; +}; + +const ProjectList: React.FC = ({ + namespace, + hideNameLabelFilters, + showTitle, +}) => { + const location = useLocation(); + const [appProjects, loaded, loadError] = useK8sWatchResource({ + isList: true, + groupVersionKind: { + group: 'argoproj.io', + kind: 'AppProject', + version: 'v1alpha1', + }, + namespaced: true, + namespace, + }); + + // Watch Applications to count apps per project + const [applications, appsLoaded] = useK8sWatchResource({ + isList: true, + groupVersionKind: { + group: 'argoproj.io', + kind: 'Application', + version: 'v1alpha1', + }, + namespaced: true, + namespace, + }); + + const columnSortConfig = React.useMemo(() => { + const showNamespace = !namespace || namespace === ''; + return [ + 'name', + ...(showNamespace ? ['namespace'] : []), + 'description', + 'applications', + 'labels', + 'last-updated', + 'actions', + ].map((key) => ({ key })); + }, [namespace]); + + const { searchParams, sortBy, direction, getSortParams } = + useGitOpsDataViewSort(columnSortConfig); + + // Get search query from URL parameters + const searchQuery = searchParams.get('q') || ''; + + const { t } = useTranslation('plugin__gitops-plugin'); + + const columnsDV = useColumnsDV(namespace, getSortParams); + const sortedProjects = React.useMemo(() => { + return sortData(appProjects as AppProjectKind[], sortBy, direction, applications, appsLoaded); + }, [appProjects, sortBy, direction, applications, appsLoaded]); + + const filters = getFilters(t, applications, appsLoaded); + const [data, filteredData, onFilterChange] = useListPageFilter(sortedProjects, filters); + + // Filter by search query if present (after other filters) + const filteredBySearch = React.useMemo(() => { + if (!searchQuery) return filteredData; + + return filteredData.filter((project) => { + const name = project.metadata?.name || ''; + const description = project.spec?.description || ''; + const labels = project.metadata?.labels || {}; + + // Check if name, description, or labels match the search query + return ( + name.toLowerCase().includes(searchQuery.toLowerCase()) || + description.toLowerCase().includes(searchQuery.toLowerCase()) || + Object.entries(labels).some(([key, value]) => { + const labelValue = value || ''; + const labelSelector = `${key}=${labelValue}`; + const lowerSearchQuery = searchQuery.toLowerCase(); + return ( + labelSelector.toLowerCase().includes(lowerSearchQuery) || + key.toLowerCase().includes(lowerSearchQuery) + ); + }) + ); + }); + }, [filteredData, searchQuery]); + + const rows = useProjectsRowsDV(filteredBySearch, namespace, applications, appsLoaded); + + // Check if there are projects initially (before search) + const hasProjects = React.useMemo(() => { + return sortedProjects.length > 0; + }, [sortedProjects]); + + const getEmptyStateBody = () => { + if (searchQuery) { + return ( + <> + {t('No Argo CD App Projects match the search filter')}{' '} + "{searchQuery}". +
+ {t('Try removing the filter or searching for a different term to see more App Projects.')} + + ); + } + return namespace + ? t('There are no Argo CD App Projects in this project.') + : t('There are no Argo CD App Projects in all projects.'); + }; + + const empty = ( + + + + + {getEmptyStateBody()} + + + + + ); + const error = loadError && ( + + + + + + + + ); + const isEmptyState = !loadError && rows.length === 0; + + return ( +
+ {showTitle == undefined && ( + + } + hideFavoriteButton={false} + > + + {t('Create AppProject')} + + + )} + + {!hideNameLabelFilters && hasProjects && ( + + )} + + +
+ ); +}; + +// Helper function to get the last update timestamp +// Uses metadata.managedFields (most recent update time) or falls back to creationTimestamp +const getLastUpdateTimestamp = (project: AppProjectKind): string => { + // Check if managedFields exists and has entries + if (project.metadata?.managedFields && project.metadata.managedFields.length > 0) { + // Find the most recent managedFields entry by time + const mostRecent = project.metadata.managedFields.reduce((latest, current) => { + const latestTime = latest?.time ? new Date(latest.time).getTime() : 0; + const currentTime = current?.time ? new Date(current.time).getTime() : 0; + return currentTime > latestTime ? current : latest; + }); + if (mostRecent?.time) { + return mostRecent.time; + } + } + // Fall back to creationTimestamp + return project.metadata?.creationTimestamp || ''; +}; + +// Helper function to count applications in a project +const getApplicationsCount = ( + project: AppProjectKind, + applications: ApplicationKind[], + appsLoaded: boolean, +): number => { + if (!applications || !appsLoaded) return 0; + return applications.filter((app) => app.spec?.project === project.metadata?.name).length; +}; + +export const sortData = ( + data: AppProjectKind[], + sortBy: string | undefined, + direction: 'asc' | 'desc' | undefined, + applications: ApplicationKind[], + appsLoaded: boolean, +) => { + if (!(sortBy && direction)) return data || []; + if (!data) return []; + + return [...data].sort((a, b) => { + let aValue: any, bValue: any; + + switch (sortBy) { + case 'name': + aValue = a.metadata?.name || ''; + bValue = b.metadata?.name || ''; + break; + case 'namespace': + aValue = a.metadata?.namespace || ''; + bValue = b.metadata?.namespace || ''; + break; + case 'description': + aValue = a.spec?.description || ''; + bValue = b.spec?.description || ''; + break; + case 'applications': + aValue = getApplicationsCount(a, applications, appsLoaded); + bValue = getApplicationsCount(b, applications, appsLoaded); + break; + case 'labels': + aValue = a.metadata?.labels || {}; + bValue = b.metadata?.labels || {}; + break; + case 'last-updated': + aValue = getLastUpdateTimestamp(a) || ''; + bValue = getLastUpdateTimestamp(b) || ''; + break; + default: + return 0; + } + + if (direction === 'asc') { + // eslint-disable-next-line no-nested-ternary + return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; + } else { + // eslint-disable-next-line no-nested-ternary + return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; + } + }); +}; + +export const useColumnsDV = ( + namespace: string | undefined, + getSortParams: (columnIndex: number) => ThProps['sort'], +): 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-18', + sort: getSortParams(0), + }, + }, + ...(showNamespace + ? [ + { + cell: t('Namespace'), + props: { + 'aria-label': 'namespace', + className: 'pf-m-width-14', + sort: getSortParams(1), + }, + }, + ] + : []), + { + cell: t('Description'), + props: { + 'aria-label': 'description', + className: 'pf-m-width-15', + sort: getSortParams(1 + i), + }, + }, + { + cell: t('Applications'), + props: { + 'aria-label': 'applications', + className: 'pf-m-width-25', + sort: getSortParams(2 + i), + }, + }, + { + cell: t('Labels'), + props: { + 'aria-label': 'labels', + className: 'pf-m-width-15', + }, + }, + { + cell: t('Last Updated'), + props: { + 'aria-label': 'last updated', + className: 'pf-m-width-18', + sort: getSortParams(showNamespace ? 5 : 4), + }, + }, + { + cell: '', + props: { 'aria-label': 'actions' }, + }, + ]; + + return columns; +}; + +type MetadataLabelsProps = { + kind: K8sResourceKindReference; + labels?: { [key: string]: string }; +}; + +const MetadataLabels: React.FC = ({ kind, labels }) => { + const { t } = useTranslation('plugin__gitops-plugin'); + return labels && Object.keys(labels).length > 0 ? ( + + {Object.keys(labels || {})?.map((key) => { + return ( + + {labels[key] ? `${key}=${labels[key]}` : key} + + ); + })} + + ) : ( + {t('No labels')} + ); +}; + +type LabelProps = { + kind: K8sResourceKindReference; + name: string; + value: string; + expand: boolean; +}; + +const LabelL: React.FC = ({ kind, name, value, expand }) => { + const selector = value ? `${name}=${value}` : name; + const href = getSelectorSearchURL('', kind, selector); + const kindOf = `co-m-${kindForReference(kind.toLowerCase())}`; + const klass = classNames(kindOf, { 'co-m-expand': expand }, 'co-label'); + return ( + <> + + + {name} + + {value && =} + {value && {value}} + + + ); +}; + +export const useProjectsRowsDV = ( + projectsList: AppProjectKind[], + namespace: string | undefined, + applications: ApplicationKind[], + appsLoaded: boolean, +): DataViewTr[] => { + const rows: DataViewTr[] = []; + if (projectsList == undefined || projectsList.length == 0) { + return rows; + } + const showNamespace = !namespace || namespace === ''; + projectsList.forEach((obj, index) => { + const appsCount = getApplicationsCount(obj, applications, appsLoaded); + + rows.push([ + { + cell: ( +
+ +
+ ), + id: 'name', + dataLabel: 'Name', + }, + ...(showNamespace + ? [ + { + cell: , + id: obj.metadata.namespace, + dataLabel: 'Namespace', + }, + ] + : []), + { + id: 'description', + cell: obj.spec?.description || '-', + }, + { + id: 'applications', + cell: appsLoaded ? appsCount.toString() : '-', + }, + { + id: 'labels', + cell: ( +
+ +
+ ), + }, + { + id: 'last-updated', + cell: (() => { + const lastUpdate = getLastUpdateTimestamp(obj); + return ( +
+ {lastUpdate ? : '-'} +
+ ); + })(), + }, + { + id: 'actions-' + index, + cell: , + props: { style: { paddingTop: 8, paddingRight: 0, paddingLeft: 0, width: 10 } }, + }, + ]); + }); + return rows; +}; + +const ProjectActionsCell: React.FC<{ + project: AppProjectKind; +}> = ({ project }) => { + const actionList: Action[] = useProjectActionsProvider(project); + return ( +
+ +
+ ); +}; + +const getFilters = ( + t: (key: string) => string, + applications: ApplicationKind[], + appsLoaded: boolean, +): RowFilter[] => [ + { + filterGroupName: t('Description'), + type: 'has-description', + reducer: (project) => { + const hasDescription = project?.spec?.description && project.spec.description.trim() !== ''; + return hasDescription ? 'has-description' : 'no-description'; + }, + filter: (input, project) => { + if (input.selected?.length) { + const hasDescription = project?.spec?.description && project.spec.description.trim() !== ''; + if (input.selected.includes('has-description')) { + return hasDescription; + } + if (input.selected.includes('no-description')) { + return !hasDescription; + } + } + return true; + }, + items: [ + { id: 'has-description', title: t('Has Description') }, + { id: 'no-description', title: t('No Description') }, + ], + }, + { + filterGroupName: t('Applications'), + type: 'has-applications', + reducer: (project) => { + if (!applications || !appsLoaded) return 'unknown'; + const appsCount = getApplicationsCount(project as AppProjectKind, applications, appsLoaded); + return appsCount > 0 ? 'has-applications' : 'no-applications'; + }, + filter: (input, project) => { + if (input.selected?.length) { + if (!applications || !appsLoaded) return true; + const appsCount = getApplicationsCount(project as AppProjectKind, applications, appsLoaded); + if (input.selected.includes('has-applications')) { + return appsCount > 0; + } + if (input.selected.includes('no-applications')) { + return appsCount === 0; + } + } + return true; + }, + items: [ + { id: 'has-applications', title: t('Has Applications') }, + { id: 'no-applications', title: t('No Applications') }, + ], + }, + { + filterGroupName: t('Project Type'), + type: 'project-type', + reducer: (project) => { + const isDefault = project?.metadata?.name === 'default'; + return isDefault ? 'default' : 'custom'; + }, + filter: (input, project) => { + if (input.selected?.length) { + const isDefault = project?.metadata?.name === 'default'; + if (input.selected.includes('default')) { + return isDefault; + } + if (input.selected.includes('custom')) { + return !isDefault; + } + } + return true; + }, + items: [ + { id: 'default', title: t('Default Project') }, + { id: 'custom', title: t('Custom Projects') }, + ], + }, + { + filterGroupName: t('Source Repositories'), + type: 'has-source-repos', + reducer: (project) => { + const hasSourceRepos = project?.spec?.sourceRepos && project.spec.sourceRepos.length > 0; + return hasSourceRepos ? 'has-source-repos' : 'no-source-repos'; + }, + filter: (input, project) => { + if (input.selected?.length) { + const hasSourceRepos = project?.spec?.sourceRepos && project.spec.sourceRepos.length > 0; + if (input.selected.includes('has-source-repos')) { + return hasSourceRepos; + } + if (input.selected.includes('no-source-repos')) { + return !hasSourceRepos; + } + } + return true; + }, + items: [ + { id: 'has-source-repos', title: t('Has Source Repos') }, + { id: 'no-source-repos', title: t('No Source Repos') }, + ], + }, + { + filterGroupName: t('Destinations'), + type: 'has-destinations', + reducer: (project) => { + const hasDestinations = project?.spec?.destinations && project.spec.destinations.length > 0; + return hasDestinations ? 'has-destinations' : 'no-destinations'; + }, + filter: (input, project) => { + if (input.selected?.length) { + const hasDestinations = project?.spec?.destinations && project.spec.destinations.length > 0; + if (input.selected.includes('has-destinations')) { + return hasDestinations; + } + if (input.selected.includes('no-destinations')) { + return !hasDestinations; + } + } + return true; + }, + items: [ + { id: 'has-destinations', title: t('Has Destinations') }, + { id: 'no-destinations', title: t('No Destinations') }, + ], + }, +]; + +export default ProjectList; diff --git a/src/gitops/components/project/ProjectListTab.tsx b/src/gitops/components/project/ProjectListTab.tsx new file mode 100644 index 00000000..80df6ba5 --- /dev/null +++ b/src/gitops/components/project/ProjectListTab.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import ProjectList from './ProjectList'; + +type ProjectListTabProps = { + namespace?: string; + hideNameLabelFilters?: boolean; + showTitle?: boolean; +}; + +const ProjectListTab: React.FC = ({ + namespace, + hideNameLabelFilters, + showTitle, +}) => { + return ( + + ); +}; + +export default ProjectListTab; diff --git a/src/gitops/components/project/hooks/useProjectActionsProvider.tsx b/src/gitops/components/project/hooks/useProjectActionsProvider.tsx new file mode 100644 index 00000000..fa48ffd8 --- /dev/null +++ b/src/gitops/components/project/hooks/useProjectActionsProvider.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { + Action, + K8sVerb, + useAnnotationsModal, + useDeleteModal, + useLabelsModal, +} from '@openshift-console/dynamic-plugin-sdk'; + +import { + AppProjectKind, + AppProjectModel, + appProjectModelRef, +} from '../../../models/AppProjectModel'; +import { useGitOpsTranslation } from '../../../utils/hooks/useGitOpsTranslation'; + +type UseProjectActionsProvider = (project: AppProjectKind) => Action[]; + +export const useProjectActionsProvider: UseProjectActionsProvider = (project) => { + const { t } = useGitOpsTranslation(); + const navigate = useNavigate(); + const launchLabelsModal = useLabelsModal(project); + const launchAnnotationsModal = useAnnotationsModal(project); + const launchDeleteModal = useDeleteModal(project); + + const actions = React.useMemo( + () => [ + { + id: 'gitops-action-edit-labels-project', + disabled: false, + label: t('Edit labels'), + accessReview: { + group: AppProjectModel.apiGroup, + verb: 'patch' as K8sVerb, + resource: AppProjectModel.plural, + namespace: project?.metadata?.namespace, + }, + cta: () => { + launchLabelsModal(); + }, + }, + { + id: 'gitops-action-edit-annotations-project', + disabled: false, + label: t('Edit annotations'), + accessReview: { + group: AppProjectModel.apiGroup, + verb: 'patch' as K8sVerb, + resource: AppProjectModel.plural, + namespace: project?.metadata?.namespace, + }, + cta: () => { + launchAnnotationsModal(); + }, + }, + { + id: 'gitops-action-edit-project', + disabled: !project?.metadata?.namespace || !project?.metadata?.name, + label: t('Edit AppProject'), + accessReview: { + group: AppProjectModel.apiGroup, + verb: 'update' as K8sVerb, + resource: AppProjectModel.plural, + namespace: project?.metadata?.namespace, + }, + cta: () => { + if (!project?.metadata?.namespace || !project?.metadata?.name) { + return; + } + navigate( + `/k8s/ns/${project?.metadata?.namespace}/${appProjectModelRef}/${project?.metadata?.name}/yaml`, + ); + }, + }, + { + id: 'gitops-action-delete-project', + disabled: false, + label: t('Delete'), + accessReview: { + group: AppProjectModel.apiGroup, + verb: 'delete' as K8sVerb, + resource: AppProjectModel.plural, + namespace: project?.metadata?.namespace, + }, + cta: () => launchDeleteModal(), + }, + ], + [project, launchLabelsModal, launchAnnotationsModal, launchDeleteModal, navigate, t], + ); + + return actions; +}; diff --git a/src/gitops/components/project/project-list.scss b/src/gitops/components/project/project-list.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/gitops/components/rollout/strategy/CanaryServices.tsx b/src/gitops/components/rollout/strategy/CanaryServices.tsx index a9da9822..f578c41a 100644 --- a/src/gitops/components/rollout/strategy/CanaryServices.tsx +++ b/src/gitops/components/rollout/strategy/CanaryServices.tsx @@ -16,8 +16,7 @@ type CanaryServicesProps = { const getAnalysisTemplates = (steps, namespace: string, t: TFunction) => { const analysisTemplateUrl = '/k8s/ns/' + namespace + '/argoproj.io~v1alpha1~AnalysisTemplate/'; - const clusterAnalysisTemplateUrl = - '/k8s/cluster/argoproj.io~v1alpha1~ClusterAnalysisTemplate/'; + const clusterAnalysisTemplateUrl = '/k8s/cluster/argoproj.io~v1alpha1~ClusterAnalysisTemplate/'; const classes = css('co-resource-item', { 'co-resource-item--inline': true, 'co-resource-item--truncate': true, diff --git a/src/gitops/models/AppProjectModel.ts b/src/gitops/models/AppProjectModel.ts index 6a31fa99..b7404429 100644 --- a/src/gitops/models/AppProjectModel.ts +++ b/src/gitops/models/AppProjectModel.ts @@ -56,7 +56,7 @@ export type AppProjectKind = K8sResourceCommon & { namespaceResourceWhitelist?: ResourceAllowDeny[]; namespaceResourceBlacklist?: ResourceAllowDeny[]; roles?: Role[]; - syncWindows: SyncWindow[]; + syncWindows?: SyncWindow[]; }; status?: { [key: string]: any }; }; diff --git a/src/gitops/templates/appproject-yaml.ts b/src/gitops/templates/appproject-yaml.ts new file mode 100644 index 00000000..0b235271 --- /dev/null +++ b/src/gitops/templates/appproject-yaml.ts @@ -0,0 +1,20 @@ +export const defaultAppProjectYamlTemplate = ` +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: my-app-project +spec: + description: "Starter AppProject template - WARNING: Uses wildcards (*) allowing any repo/namespace/cluster. Restrict for production." + sourceRepos: + - "*" # Allow any repo (user can tighten later) + destinations: + - namespace: "*" + server: "*" # Allow any destination (user can tighten later) + clusterResourceWhitelist: + - group: "*" + kind: "*" # Allow all cluster-scoped resources initially + namespaceResourceWhitelist: + - group: "*" + kind: "*" # Allow all namespaced resources initially + namespaceResourceBlacklist: [] +`; diff --git a/src/gitops/templates/index.ts b/src/gitops/templates/index.ts index b08e4a76..5cd82580 100644 --- a/src/gitops/templates/index.ts +++ b/src/gitops/templates/index.ts @@ -1,3 +1,4 @@ export * from './application-yaml'; export * from './applicationset-yaml'; +export * from './appproject-yaml'; export * from './rollout-yaml';