From 10302183acad53dd774e5dd4b51c43129ec2a5a2 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 16 Dec 2025 16:07:46 -0500 Subject: [PATCH] feat: Show ArgoCD AppProjects in Details page Signed-off-by: Atif Ali --- console-extensions.json | 18 + locales/en/plugin__gitops-plugin.json | 76 +++- locales/ja/plugin__gitops-plugin.json | 76 +++- locales/ko/plugin__gitops-plugin.json | 76 +++- locales/zh/plugin__gitops-plugin.json | 76 +++- plugin-metadata.ts | 1 + .../components/project/DestinationsList.tsx | 76 ++++ .../project/ProjectAllowDenyTab.tsx | 172 ++++++++ .../project/ProjectApplicationsTab.tsx | 25 ++ .../components/project/ProjectDetailsPage.tsx | 16 + .../components/project/ProjectDetailsTab.tsx | 191 +++++++++ src/gitops/components/project/ProjectList.tsx | 72 +--- .../components/project/ProjectNavPage.tsx | 114 ++++++ .../components/project/ProjectRolesTab.tsx | 221 +++++++++++ .../project/ProjectSyncWindowsTab.tsx | 373 ++++++++++++++++++ .../project/ResourceAllowDenyList.tsx | 47 +++ .../components/project/project-list.scss | 22 ++ .../BaseDetailsSummary/BaseDetailsSummary.tsx | 26 +- .../shared/MetadataLabels/MetadataLabels.tsx | 57 +++ .../components/shared/MetadataLabels/index.ts | 1 + src/gitops/models/AppProjectModel.ts | 1 + src/gitops/utils/project-utils.ts | 18 + 22 files changed, 1640 insertions(+), 115 deletions(-) create mode 100644 src/gitops/components/project/DestinationsList.tsx create mode 100644 src/gitops/components/project/ProjectAllowDenyTab.tsx create mode 100644 src/gitops/components/project/ProjectApplicationsTab.tsx create mode 100644 src/gitops/components/project/ProjectDetailsPage.tsx create mode 100644 src/gitops/components/project/ProjectDetailsTab.tsx create mode 100644 src/gitops/components/project/ProjectNavPage.tsx create mode 100644 src/gitops/components/project/ProjectRolesTab.tsx create mode 100644 src/gitops/components/project/ProjectSyncWindowsTab.tsx create mode 100644 src/gitops/components/project/ResourceAllowDenyList.tsx create mode 100644 src/gitops/components/shared/MetadataLabels/MetadataLabels.tsx create mode 100644 src/gitops/components/shared/MetadataLabels/index.ts create mode 100644 src/gitops/utils/project-utils.ts diff --git a/console-extensions.json b/console-extensions.json index df049e220..b823fec3c 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -590,6 +590,24 @@ } } }, + { + "type": "console.page/resource/details", + "flags": { + "required": [ + "APPLICATION" + ] + }, + "properties": { + "model": { + "group": "argoproj.io", + "kind": "AppProject", + "version": "v1alpha1" + }, + "component": { + "$codeRef": "ProjectDetailsPage" + } + } + }, { "type": "console.page/resource/search", "properties": { diff --git a/locales/en/plugin__gitops-plugin.json b/locales/en/plugin__gitops-plugin.json index 310f0a130..3485d2a62 100644 --- a/locales/en/plugin__gitops-plugin.json +++ b/locales/en/plugin__gitops-plugin.json @@ -128,10 +128,56 @@ "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": "ArgoCD Applications", + "Server": "Server", + "Deny": "Deny", + "Allow": "Allow", + "No destinations configured": "No destinations configured", + "This AppProject does not have any destinations configured.": "This AppProject does not have any destinations configured.", "Edit labels": "Edit labels", "Edit annotations": "Edit annotations", "Edit AppProject": "Edit AppProject", "Delete": "Delete", + "Allowed Sources": "Allowed Sources", + "Repositories": "Repositories", + "Namespaces": "Namespaces", + "Allowed Destinations": "Allowed Destinations", + "Resource Allow/Deny Lists": "Resource Allow/Deny Lists", + "Cluster Resource Allow List": "Cluster Resource Allow List", + "Cluster Resource Deny List": "Cluster Resource Deny List", + "Namespace Resource Allow List": "Namespace Resource Allow List", + "Namespace Resource Deny List": "Namespace Resource Deny List", + "AppProject details": "AppProject details", + "Project Type": "Project Type", + "The default project is created automatically and cannot be deleted. It can be modified but is recommended to create dedicated projects for production use.": "The default project is created automatically and cannot be deleted. It can be modified but is recommended to create dedicated projects for production use.", + "Default Project": "Default Project", + "Description": "Description", + "Description of the AppProject.": "Description of the AppProject.", + "Number of applications using this AppProject.": "Number of applications using this AppProject.", + "Destinations": "Destinations", + "Number of allowed destinations (clusters/namespaces) for this AppProject.": "Number of allowed destinations (clusters/namespaces) for this AppProject.", + "destinations": "destinations", + "destination": "destination", + "Source Repositories": "Source Repositories", + "Number of allowed source repositories for this AppProject.": "Number of allowed source repositories for this AppProject.", + "repositories": "repositories", + "repository": "repository", + "Source Namespaces": "Source Namespaces", + "Number of allowed source namespaces for this AppProject.": "Number of allowed source namespaces for this AppProject.", + "namespaces": "namespaces", + "namespace": "namespace", + "Roles": "Roles", + "Number of roles configured in this AppProject.": "Number of roles configured in this AppProject.", + "roles": "roles", + "role": "role", + "Sync Windows": "Sync Windows", + "Number of sync windows configured in this AppProject.": "Number of sync windows configured in this AppProject.", + "sync windows": "sync windows", + "sync window": "sync window", + "Project-Scoped Clusters Only": "Project-Scoped Clusters Only", + "When enabled, applications can only be deployed to clusters that are scoped to this project. This prevents deploying to clusters that are not part of the project.": "When enabled, applications can only be deployed to clusters that are scoped to this project. This prevents deploying to clusters that are not part of the project.", + "Enabled": "Enabled", + "Disabled": "Disabled", + "Conditions": "Conditions", "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.", @@ -143,23 +189,35 @@ "AppProjects": "AppProjects", "Create AppProject": "Create AppProject", "Search by name...": "Search by name...", - "Description": "Description", "Labels": "Labels", "Last Updated": "Last Updated", - "No labels": "No labels", "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", + "Allow/Deny": "Allow/Deny", + "There was an error retrieving the AppProject. Check your connection and reload the page.": "There was an error retrieving the AppProject. Check your connection and reload the page.", + "No roles configured": "No roles configured", + "This AppProject does not have any roles configured.": "This AppProject does not have any roles configured.", + "Groups": "Groups", + "Policies": "Policies", + "No sync windows configured": "No sync windows configured", + "This AppProject does not have any sync windows configured.": "This AppProject does not have any sync windows configured.", + "Schedule": "Schedule", + "Clusters": "Clusters", + "Manual Sync": "Manual Sync", + "Time Zone": "Time Zone", + "All": "All", + "Allowed": "Allowed", + "Denied": "Denied", + "Group": "Group", + "No resources configured": "No resources configured", + "This list does not have any resources configured.": "This list does not have any resources configured.", "Traffic": "Traffic", "Ready": "Ready", "Restarts": "Restarts", @@ -176,15 +234,12 @@ "Retry": "Retry", "Restart": "Restart", "Edit Rollout": "Edit Rollout", - "No revisions": "No revisions", - "Revisions": "Revisions", "Rollout details": "Rollout details", "Replicas": "Replicas", "The number of desired replicas for the rollout": "The number of desired replicas for the rollout", "The current status of the rollout": "The current status of the rollout", "Strategy": "Strategy", "Whether the rollout is using a blue-green or canary strategy": "Whether the rollout is using a blue-green or canary strategy", - "Conditions": "Conditions", "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.", @@ -193,10 +248,9 @@ "Create Rollout": "Create Rollout", "Pods": "Pods", "Selector": "Selector", + "No labels": "No labels", "Rollout Status": "Rollout Status", "There was an error retrieving the rollout. Check your connection and reload the page.": "There was an error retrieving the rollout. Check your connection and reload the page.", - "There was an error retrieving the rollout revisions. Check your connection and reload the page.": "There was an error retrieving the rollout revisions. Check your connection and reload the page.", - "Rollout Revisions": "Rollout Revisions", "Active Service": "Active Service", "The active blue-green service": "The active blue-green service", "Preview Service": "Preview Service", diff --git a/locales/ja/plugin__gitops-plugin.json b/locales/ja/plugin__gitops-plugin.json index 54e096f1f..7a77bf882 100644 --- a/locales/ja/plugin__gitops-plugin.json +++ b/locales/ja/plugin__gitops-plugin.json @@ -128,10 +128,56 @@ "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", + "Server": "Server", + "Deny": "Deny", + "Allow": "Allow", + "No destinations configured": "No destinations configured", + "This AppProject does not have any destinations configured.": "This AppProject does not have any destinations configured.", "Edit labels": "Edit labels", "Edit annotations": "Edit annotations", "Edit AppProject": "Edit AppProject", "Delete": "Delete", + "Allowed Sources": "Allowed Sources", + "Repositories": "Repositories", + "Namespaces": "Namespaces", + "Allowed Destinations": "Allowed Destinations", + "Resource Allow/Deny Lists": "Resource Allow/Deny Lists", + "Cluster Resource Allow List": "Cluster Resource Allow List", + "Cluster Resource Deny List": "Cluster Resource Deny List", + "Namespace Resource Allow List": "Namespace Resource Allow List", + "Namespace Resource Deny List": "Namespace Resource Deny List", + "AppProject details": "AppProject details", + "Project Type": "Project Type", + "The default project is created automatically and cannot be deleted. It can be modified but is recommended to create dedicated projects for production use.": "The default project is created automatically and cannot be deleted. It can be modified but is recommended to create dedicated projects for production use.", + "Default Project": "Default Project", + "Description": "Description", + "Description of the AppProject.": "Description of the AppProject.", + "Number of applications using this AppProject.": "Number of applications using this AppProject.", + "Destinations": "Destinations", + "Number of allowed destinations (clusters/namespaces) for this AppProject.": "Number of allowed destinations (clusters/namespaces) for this AppProject.", + "destinations": "destinations", + "destination": "destination", + "Source Repositories": "Source Repositories", + "Number of allowed source repositories for this AppProject.": "Number of allowed source repositories for this AppProject.", + "repositories": "repositories", + "repository": "repository", + "Source Namespaces": "Source Namespaces", + "Number of allowed source namespaces for this AppProject.": "Number of allowed source namespaces for this AppProject.", + "namespaces": "namespaces", + "namespace": "namespace", + "Roles": "Roles", + "Number of roles configured in this AppProject.": "Number of roles configured in this AppProject.", + "roles": "roles", + "role": "role", + "Sync Windows": "Sync Windows", + "Number of sync windows configured in this AppProject.": "Number of sync windows configured in this AppProject.", + "sync windows": "sync windows", + "sync window": "sync window", + "Project-Scoped Clusters Only": "Project-Scoped Clusters Only", + "When enabled, applications can only be deployed to clusters that are scoped to this project. This prevents deploying to clusters that are not part of the project.": "When enabled, applications can only be deployed to clusters that are scoped to this project. This prevents deploying to clusters that are not part of the project.", + "Enabled": "Enabled", + "Disabled": "Disabled", + "Conditions": "Conditions", "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.", @@ -143,23 +189,35 @@ "AppProjects": "AppProjects", "Create AppProject": "Create AppProject", "Search by name...": "Search by name...", - "Description": "Description", "Labels": "Labels", "Last Updated": "Last Updated", - "No labels": "No labels", "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", + "Allow/Deny": "Allow/Deny", + "There was an error retrieving the AppProject. Check your connection and reload the page.": "There was an error retrieving the AppProject. Check your connection and reload the page.", + "No roles configured": "No roles configured", + "This AppProject does not have any roles configured.": "This AppProject does not have any roles configured.", + "Groups": "Groups", + "Policies": "Policies", + "No sync windows configured": "No sync windows configured", + "This AppProject does not have any sync windows configured.": "This AppProject does not have any sync windows configured.", + "Schedule": "Schedule", + "Clusters": "Clusters", + "Manual Sync": "Manual Sync", + "Time Zone": "Time Zone", + "All": "All", + "Allowed": "Allowed", + "Denied": "Denied", + "Group": "Group", + "No resources configured": "No resources configured", + "This list does not have any resources configured.": "This list does not have any resources configured.", "Traffic": "Traffic", "Ready": "Ready", "Restarts": "Restarts", @@ -176,15 +234,12 @@ "Retry": "Retry", "Restart": "Restart", "Edit Rollout": "Edit Rollout", - "No revisions": "No revisions", - "Revisions": "Revisions", "Rollout details": "Rollout details", "Replicas": "Replicas", "The number of desired replicas for the rollout": "The number of desired replicas for the rollout", "The current status of the rollout": "The current status of the rollout", "Strategy": "Strategy", "Whether the rollout is using a blue-green or canary strategy": "Whether the rollout is using a blue-green or canary strategy", - "Conditions": "Conditions", "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.", @@ -193,10 +248,9 @@ "Create Rollout": "Create Rollout", "Pods": "Pods", "Selector": "Selector", + "No labels": "No labels", "Rollout Status": "Rollout Status", "There was an error retrieving the rollout. Check your connection and reload the page.": "There was an error retrieving the rollout. Check your connection and reload the page.", - "There was an error retrieving the rollout revisions. Check your connection and reload the page.": "There was an error retrieving the rollout revisions. Check your connection and reload the page.", - "Rollout Revisions": "Rollout Revisions", "Active Service": "Active Service", "The active blue-green service": "The active blue-green service", "Preview Service": "Preview Service", diff --git a/locales/ko/plugin__gitops-plugin.json b/locales/ko/plugin__gitops-plugin.json index 508387d75..3166558fb 100644 --- a/locales/ko/plugin__gitops-plugin.json +++ b/locales/ko/plugin__gitops-plugin.json @@ -128,10 +128,56 @@ "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", + "Server": "Server", + "Deny": "Deny", + "Allow": "Allow", + "No destinations configured": "No destinations configured", + "This AppProject does not have any destinations configured.": "This AppProject does not have any destinations configured.", "Edit labels": "Edit labels", "Edit annotations": "Edit annotations", "Edit AppProject": "Edit AppProject", "Delete": "Delete", + "Allowed Sources": "Allowed Sources", + "Repositories": "Repositories", + "Namespaces": "Namespaces", + "Allowed Destinations": "Allowed Destinations", + "Resource Allow/Deny Lists": "Resource Allow/Deny Lists", + "Cluster Resource Allow List": "Cluster Resource Allow List", + "Cluster Resource Deny List": "Cluster Resource Deny List", + "Namespace Resource Allow List": "Namespace Resource Allow List", + "Namespace Resource Deny List": "Namespace Resource Deny List", + "AppProject details": "AppProject details", + "Project Type": "Project Type", + "The default project is created automatically and cannot be deleted. It can be modified but is recommended to create dedicated projects for production use.": "The default project is created automatically and cannot be deleted. It can be modified but is recommended to create dedicated projects for production use.", + "Default Project": "Default Project", + "Description": "Description", + "Description of the AppProject.": "Description of the AppProject.", + "Number of applications using this AppProject.": "Number of applications using this AppProject.", + "Destinations": "Destinations", + "Number of allowed destinations (clusters/namespaces) for this AppProject.": "Number of allowed destinations (clusters/namespaces) for this AppProject.", + "destinations": "destinations", + "destination": "destination", + "Source Repositories": "Source Repositories", + "Number of allowed source repositories for this AppProject.": "Number of allowed source repositories for this AppProject.", + "repositories": "repositories", + "repository": "repository", + "Source Namespaces": "Source Namespaces", + "Number of allowed source namespaces for this AppProject.": "Number of allowed source namespaces for this AppProject.", + "namespaces": "namespaces", + "namespace": "namespace", + "Roles": "Roles", + "Number of roles configured in this AppProject.": "Number of roles configured in this AppProject.", + "roles": "roles", + "role": "role", + "Sync Windows": "Sync Windows", + "Number of sync windows configured in this AppProject.": "Number of sync windows configured in this AppProject.", + "sync windows": "sync windows", + "sync window": "sync window", + "Project-Scoped Clusters Only": "Project-Scoped Clusters Only", + "When enabled, applications can only be deployed to clusters that are scoped to this project. This prevents deploying to clusters that are not part of the project.": "When enabled, applications can only be deployed to clusters that are scoped to this project. This prevents deploying to clusters that are not part of the project.", + "Enabled": "Enabled", + "Disabled": "Disabled", + "Conditions": "Conditions", "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.", @@ -143,23 +189,35 @@ "AppProjects": "AppProjects", "Create AppProject": "Create AppProject", "Search by name...": "Search by name...", - "Description": "Description", "Labels": "Labels", "Last Updated": "Last Updated", - "No labels": "No labels", "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", + "Allow/Deny": "Allow/Deny", + "There was an error retrieving the AppProject. Check your connection and reload the page.": "There was an error retrieving the AppProject. Check your connection and reload the page.", + "No roles configured": "No roles configured", + "This AppProject does not have any roles configured.": "This AppProject does not have any roles configured.", + "Groups": "Groups", + "Policies": "Policies", + "No sync windows configured": "No sync windows configured", + "This AppProject does not have any sync windows configured.": "This AppProject does not have any sync windows configured.", + "Schedule": "Schedule", + "Clusters": "Clusters", + "Manual Sync": "Manual Sync", + "Time Zone": "Time Zone", + "All": "All", + "Allowed": "Allowed", + "Denied": "Denied", + "Group": "Group", + "No resources configured": "No resources configured", + "This list does not have any resources configured.": "This list does not have any resources configured.", "Traffic": "Traffic", "Ready": "Ready", "Restarts": "Restarts", @@ -176,15 +234,12 @@ "Retry": "Retry", "Restart": "Restart", "Edit Rollout": "Edit Rollout", - "No revisions": "No revisions", - "Revisions": "Revisions", "Rollout details": "Rollout details", "Replicas": "Replicas", "The number of desired replicas for the rollout": "The number of desired replicas for the rollout", "The current status of the rollout": "The current status of the rollout", "Strategy": "Strategy", "Whether the rollout is using a blue-green or canary strategy": "Whether the rollout is using a blue-green or canary strategy", - "Conditions": "Conditions", "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.", @@ -193,10 +248,9 @@ "Create Rollout": "Create Rollout", "Pods": "Pods", "Selector": "Selector", + "No labels": "No labels", "Rollout Status": "Rollout Status", "There was an error retrieving the rollout. Check your connection and reload the page.": "There was an error retrieving the rollout. Check your connection and reload the page.", - "There was an error retrieving the rollout revisions. Check your connection and reload the page.": "There was an error retrieving the rollout revisions. Check your connection and reload the page.", - "Rollout Revisions": "Rollout Revisions", "Active Service": "Active Service", "The active blue-green service": "The active blue-green service", "Preview Service": "Preview Service", diff --git a/locales/zh/plugin__gitops-plugin.json b/locales/zh/plugin__gitops-plugin.json index ea990e182..a432bf7bd 100644 --- a/locales/zh/plugin__gitops-plugin.json +++ b/locales/zh/plugin__gitops-plugin.json @@ -128,10 +128,56 @@ "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", + "Server": "Server", + "Deny": "Deny", + "Allow": "Allow", + "No destinations configured": "No destinations configured", + "This AppProject does not have any destinations configured.": "This AppProject does not have any destinations configured.", "Edit labels": "Edit labels", "Edit annotations": "Edit annotations", "Edit AppProject": "Edit AppProject", "Delete": "Delete", + "Allowed Sources": "Allowed Sources", + "Repositories": "Repositories", + "Namespaces": "Namespaces", + "Allowed Destinations": "Allowed Destinations", + "Resource Allow/Deny Lists": "Resource Allow/Deny Lists", + "Cluster Resource Allow List": "Cluster Resource Allow List", + "Cluster Resource Deny List": "Cluster Resource Deny List", + "Namespace Resource Allow List": "Namespace Resource Allow List", + "Namespace Resource Deny List": "Namespace Resource Deny List", + "AppProject details": "AppProject details", + "Project Type": "Project Type", + "The default project is created automatically and cannot be deleted. It can be modified but is recommended to create dedicated projects for production use.": "The default project is created automatically and cannot be deleted. It can be modified but is recommended to create dedicated projects for production use.", + "Default Project": "Default Project", + "Description": "Description", + "Description of the AppProject.": "Description of the AppProject.", + "Number of applications using this AppProject.": "Number of applications using this AppProject.", + "Destinations": "Destinations", + "Number of allowed destinations (clusters/namespaces) for this AppProject.": "Number of allowed destinations (clusters/namespaces) for this AppProject.", + "destinations": "destinations", + "destination": "destination", + "Source Repositories": "Source Repositories", + "Number of allowed source repositories for this AppProject.": "Number of allowed source repositories for this AppProject.", + "repositories": "repositories", + "repository": "repository", + "Source Namespaces": "Source Namespaces", + "Number of allowed source namespaces for this AppProject.": "Number of allowed source namespaces for this AppProject.", + "namespaces": "namespaces", + "namespace": "namespace", + "Roles": "Roles", + "Number of roles configured in this AppProject.": "Number of roles configured in this AppProject.", + "roles": "roles", + "role": "role", + "Sync Windows": "Sync Windows", + "Number of sync windows configured in this AppProject.": "Number of sync windows configured in this AppProject.", + "sync windows": "sync windows", + "sync window": "sync window", + "Project-Scoped Clusters Only": "Project-Scoped Clusters Only", + "When enabled, applications can only be deployed to clusters that are scoped to this project. This prevents deploying to clusters that are not part of the project.": "When enabled, applications can only be deployed to clusters that are scoped to this project. This prevents deploying to clusters that are not part of the project.", + "Enabled": "Enabled", + "Disabled": "Disabled", + "Conditions": "Conditions", "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.", @@ -143,23 +189,35 @@ "AppProjects": "AppProjects", "Create AppProject": "Create AppProject", "Search by name...": "Search by name...", - "Description": "Description", "Labels": "Labels", "Last Updated": "Last Updated", - "No labels": "No labels", "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", + "Allow/Deny": "Allow/Deny", + "There was an error retrieving the AppProject. Check your connection and reload the page.": "There was an error retrieving the AppProject. Check your connection and reload the page.", + "No roles configured": "No roles configured", + "This AppProject does not have any roles configured.": "This AppProject does not have any roles configured.", + "Groups": "Groups", + "Policies": "Policies", + "No sync windows configured": "No sync windows configured", + "This AppProject does not have any sync windows configured.": "This AppProject does not have any sync windows configured.", + "Schedule": "Schedule", + "Clusters": "Clusters", + "Manual Sync": "Manual Sync", + "Time Zone": "Time Zone", + "All": "All", + "Allowed": "Allowed", + "Denied": "Denied", + "Group": "Group", + "No resources configured": "No resources configured", + "This list does not have any resources configured.": "This list does not have any resources configured.", "Traffic": "Traffic", "Ready": "Ready", "Restarts": "Restarts", @@ -176,15 +234,12 @@ "Retry": "Retry", "Restart": "Restart", "Edit Rollout": "Edit Rollout", - "No revisions": "No revisions", - "Revisions": "Revisions", "Rollout details": "Rollout details", "Replicas": "Replicas", "The number of desired replicas for the rollout": "The number of desired replicas for the rollout", "The current status of the rollout": "The current status of the rollout", "Strategy": "Strategy", "Whether the rollout is using a blue-green or canary strategy": "Whether the rollout is using a blue-green or canary strategy", - "Conditions": "Conditions", "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.", @@ -193,10 +248,9 @@ "Create Rollout": "Create Rollout", "Pods": "Pods", "Selector": "Selector", + "No labels": "No labels", "Rollout Status": "Rollout Status", "There was an error retrieving the rollout. Check your connection and reload the page.": "There was an error retrieving the rollout. Check your connection and reload the page.", - "There was an error retrieving the rollout revisions. Check your connection and reload the page.": "There was an error retrieving the rollout revisions. Check your connection and reload the page.", - "Rollout Revisions": "Rollout Revisions", "Active Service": "Active Service", "The active blue-green service": "The active blue-green service", "Preview Service": "Preview Service", diff --git a/plugin-metadata.ts b/plugin-metadata.ts index b2d6c4e5a..0a75e6685 100644 --- a/plugin-metadata.ts +++ b/plugin-metadata.ts @@ -20,6 +20,7 @@ const metadata: ConsolePluginBuildMetadata = { ApplicationSetList: "./gitops/components/application/ApplicationSetListTab.tsx", ApplicationSetDetailsPage: "./gitops/components/appset/ApplicationSetDetailsPage.tsx", ProjectList: "./gitops/components/project/ProjectListTab.tsx", + ProjectDetailsPage: "./gitops/components/project/ProjectDetailsPage.tsx", yamlTemplates: "./gitops/templates/index.ts" } }; diff --git a/src/gitops/components/project/DestinationsList.tsx b/src/gitops/components/project/DestinationsList.tsx new file mode 100644 index 000000000..f30713d54 --- /dev/null +++ b/src/gitops/components/project/DestinationsList.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; + +import { Badge, EmptyState, EmptyStateBody, PageSection } from '@patternfly/react-core'; +import { CubesIcon } from '@patternfly/react-icons'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; + +import { Destination } from '../../models/AppProjectModel'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import { getDisplayValue, isDenyRule } from '../../utils/project-utils'; + +interface DestinationsListProps { + destinations?: Destination[]; +} + +const DestinationsList: React.FC = ({ destinations }) => { + const { t } = useGitOpsTranslation(); + + const destinationsList = destinations || []; + + return ( + + {destinationsList.length > 0 ? ( + + + + + + + + + + + {destinationsList.map((destination, index) => { + const serverIsDeny = isDenyRule(destination.server); + const namespaceIsDeny = isDenyRule(destination.namespace); + const hasDenyRule = serverIsDeny || namespaceIsDeny; + + return ( + + + + + + + ); + })} + +
{t('Type')}{t('Server')}{t('Name')}{t('Namespace')}
+ {hasDenyRule ? ( + + {t('Deny')} + + ) : ( + + {t('Allow')} + + )} + + {getDisplayValue(destination.server)} + + {getDisplayValue(destination.name)} + + {getDisplayValue(destination.namespace)} +
+ ) : ( + + + {t('This AppProject does not have any destinations configured.')} + + + )} +
+ ); +}; + +export default DestinationsList; diff --git a/src/gitops/components/project/ProjectAllowDenyTab.tsx b/src/gitops/components/project/ProjectAllowDenyTab.tsx new file mode 100644 index 000000000..a197039f9 --- /dev/null +++ b/src/gitops/components/project/ProjectAllowDenyTab.tsx @@ -0,0 +1,172 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; + +import { + Badge, + Card, + CardBody, + CardHeader, + Grid, + GridItem, + List, + ListItem, + PageSection, + PageSectionVariants, + Panel, + Title, +} from '@patternfly/react-core'; + +import { AppProjectKind } from '../../models/AppProjectModel'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import { getDisplayValue, isDenyRule } from '../../utils/project-utils'; + +import DestinationsList from './DestinationsList'; +import ResourceAllowDenyList from './ResourceAllowDenyList'; + +const renderStringArray = (items: string[] | undefined, t: (key: string) => string) => { + if (items && items.length > 0) { + return ( + + {items.map((el, idx) => { + const denyRule = isDenyRule(el); + const displayValue = getDisplayValue(el); + return ( + + {denyRule ? ( + + + {t('Deny')} + + {displayValue} + + ) : ( + + + {t('Allow')} + + {displayValue} + + )} + + ); + })} + + ); + } else { + return
{'-'}
; + } +}; + +type ProjectAllowDenyTabProps = RouteComponentProps<{ + ns: string; + name: string; +}> & { + obj?: AppProjectKind; +}; + +const ProjectAllowDenyTab: React.FC = ({ obj }) => { + const { t } = useGitOpsTranslation(); + + if (!obj) return null; + + const spec = obj.spec || {}; + + return ( + <> + + + {t('Allowed Sources')} + + + + + + + {t('Repositories')} + + {renderStringArray(spec.sourceRepos, t)} + + + + + + {t('Namespaces')} + + {renderStringArray(spec.sourceNamespaces, t)} + + + + + + + + + {t('Allowed Destinations')} + + + + + + + + + + + + + + + + + {t('Resource Allow/Deny Lists')} + + + + + + + {t('Cluster Resource Allow List')} + + + + + + + + + + {t('Cluster Resource Deny List')} + + + + + + + + + + {t('Namespace Resource Allow List')} + + + + + + + + + + {t('Namespace Resource Deny List')} + + + + + + + + + + + ); +}; + +export default ProjectAllowDenyTab; diff --git a/src/gitops/components/project/ProjectApplicationsTab.tsx b/src/gitops/components/project/ProjectApplicationsTab.tsx new file mode 100644 index 000000000..2b8a32f8d --- /dev/null +++ b/src/gitops/components/project/ProjectApplicationsTab.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; + +import { AppProjectKind } from '../../models/AppProjectModel'; +import ApplicationList from '../shared/ApplicationList'; + +type ProjectApplicationsTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: AppProjectKind; +}; + +const ProjectApplicationsTab: React.FC = ({ obj }) => { + const namespace = obj?.metadata?.namespace; + if (!obj || !namespace) return null; + + return ( + + ); +}; + +export default ProjectApplicationsTab; diff --git a/src/gitops/components/project/ProjectDetailsPage.tsx b/src/gitops/components/project/ProjectDetailsPage.tsx new file mode 100644 index 000000000..cf2b4c225 --- /dev/null +++ b/src/gitops/components/project/ProjectDetailsPage.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; + +import ProjectNavPage from './ProjectNavPage'; + +const ProjectDetailsPage: React.FC = () => { + const { name, ns } = useParams<{ name?: string; ns?: string }>(); + + if (!name || !ns) { + return
Error: Missing required route parameters
; + } + + return ; +}; + +export default ProjectDetailsPage; diff --git a/src/gitops/components/project/ProjectDetailsTab.tsx b/src/gitops/components/project/ProjectDetailsTab.tsx new file mode 100644 index 000000000..7b9861f9d --- /dev/null +++ b/src/gitops/components/project/ProjectDetailsTab.tsx @@ -0,0 +1,191 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; +import classNames from 'classnames'; + +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { + Badge, + DescriptionList, + Flex, + FlexItem, + PageSection, + PageSectionVariants, + Title, +} from '@patternfly/react-core'; + +import { ApplicationKind, ApplicationModel } from '../../models/ApplicationModel'; +import { AppProjectKind, AppProjectModel } from '../../models/AppProjectModel'; +import { Conditions } from '../../utils/components/Conditions/Conditions'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import BaseDetailsSummary, { + DetailsDescriptionGroup, +} from '../shared/BaseDetailsSummary/BaseDetailsSummary'; + +type ProjectDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: AppProjectKind; +}; + +const ProjectDetailsTab: React.FC = ({ obj }) => { + const { t } = useGitOpsTranslation(); + const namespace = obj?.metadata?.namespace; + + const [applications] = useK8sWatchResource({ + groupVersionKind: { + group: ApplicationModel.apiGroup, + version: ApplicationModel.apiVersion, + kind: ApplicationModel.kind, + }, + namespace: namespace || obj?.metadata?.namespace, + isList: true, + }); + + if (!obj) return null; + + const spec = obj.spec || {}; + const status = obj.status || {}; + const isDefaultProject = obj.metadata?.name === 'default'; + + const applicationsCount = + applications?.filter((app) => app.spec?.project === obj.metadata?.name).length || 0; + + const destinationsCount = spec.destinations?.length || 0; + const sourceReposCount = spec.sourceRepos?.length || 0; + const sourceNamespacesCount = spec.sourceNamespaces?.length || 0; + const rolesCount = spec.roles?.length || 0; + const syncWindowsCount = spec.syncWindows?.length || 0; + + return ( + <> + + + {t('AppProject details')} + + + + + + + + + + + {isDefaultProject && ( + + + {t('Default Project')} + + + )} + + {spec.description && ( + + {spec.description} + + )} + + + + {applicationsCount}{' '} + {applicationsCount !== 1 ? t('applications') : t('application')} + + + + + + {destinationsCount}{' '} + {destinationsCount !== 1 ? t('destinations') : t('destination')} + + + + + + {sourceReposCount}{' '} + {sourceReposCount !== 1 ? t('repositories') : t('repository')} + + + + + + {sourceNamespacesCount}{' '} + {sourceNamespacesCount !== 1 ? t('namespaces') : t('namespace')} + + + + + + {rolesCount} {rolesCount !== 1 ? t('roles') : t('role')} + + + + + + {syncWindowsCount}{' '} + {syncWindowsCount !== 1 ? t('sync windows') : t('sync window')} + + + + {spec.permitOnlyProjectScopedClusters !== undefined && ( + + + {spec.permitOnlyProjectScopedClusters ? t('Enabled') : t('Disabled')} + + + )} + + + + + + + {status.conditions && status.conditions.length > 0 && ( + + + {t('Conditions')} + + + + )} + + ); +}; + +export default ProjectDetailsTab; diff --git a/src/gitops/components/project/ProjectList.tsx b/src/gitops/components/project/ProjectList.tsx index 9b733305c..18304698b 100644 --- a/src/gitops/components/project/ProjectList.tsx +++ b/src/gitops/components/project/ProjectList.tsx @@ -1,20 +1,13 @@ 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 { modelToGroupVersionKind, modelToRef } from '@gitops/utils/utils'; import { Action, K8sResourceCommon, - K8sResourceKindReference, ListPageBody, ListPageCreate, ListPageFilter, @@ -26,8 +19,7 @@ import { } 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 { EmptyState, EmptyStateBody } 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'; @@ -36,6 +28,7 @@ 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 { MetadataLabels } from '../shared/MetadataLabels/MetadataLabels'; import { useProjectActionsProvider } from './hooks/useProjectActionsProvider'; @@ -318,8 +311,9 @@ export const useColumnsDV = ( cell: t('Name'), props: { 'aria-label': 'name', - className: 'pf-m-width-18', + className: 'pf-m-width-25', sort: getSortParams(0), + style: { minWidth: '200px' }, }, }, ...(showNamespace @@ -328,8 +322,9 @@ export const useColumnsDV = ( cell: t('Namespace'), props: { 'aria-label': 'namespace', - className: 'pf-m-width-14', + className: 'pf-m-width-15', sort: getSortParams(1), + style: { minWidth: '150px' }, }, }, ] @@ -338,7 +333,7 @@ export const useColumnsDV = ( cell: t('Description'), props: { 'aria-label': 'description', - className: 'pf-m-width-15', + className: 'pf-m-width-25', sort: getSortParams(1 + i), }, }, @@ -346,7 +341,7 @@ export const useColumnsDV = ( cell: t('Applications'), props: { 'aria-label': 'applications', - className: 'pf-m-width-25', + className: 'pf-m-width-15', sort: getSortParams(2 + i), }, }, @@ -361,7 +356,7 @@ export const useColumnsDV = ( cell: t('Last Updated'), props: { 'aria-label': 'last updated', - className: 'pf-m-width-18', + className: 'pf-m-width-15', sort: getSortParams(showNamespace ? 5 : 4), }, }, @@ -374,53 +369,6 @@ export const useColumnsDV = ( 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, diff --git a/src/gitops/components/project/ProjectNavPage.tsx b/src/gitops/components/project/ProjectNavPage.tsx new file mode 100644 index 000000000..76f0a5f5e --- /dev/null +++ b/src/gitops/components/project/ProjectNavPage.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; + +import { HorizontalNav, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { ErrorState } from '@patternfly/react-component-groups'; +import { Bullseye, Spinner } from '@patternfly/react-core'; + +import { AppProjectKind, AppProjectModel } from '../../models/AppProjectModel'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import DetailsPageHeader from '../shared/DetailsPageHeader/DetailsPageHeader'; +import EventsTab from '../shared/EventsTab/EventsTab'; +import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; + +import { useProjectActionsProvider } from './hooks/useProjectActionsProvider'; +import ProjectAllowDenyTab from './ProjectAllowDenyTab'; +import ProjectApplicationsTab from './ProjectApplicationsTab'; +import ProjectDetailsTab from './ProjectDetailsTab'; +import ProjectRolesTab from './ProjectRolesTab'; +import ProjectSyncWindowsTab from './ProjectSyncWindowsTab'; + +type ProjectPageProps = { + name: string; + namespace: string; + kind: string; +}; + +const ProjectNavPage: React.FC = ({ name, namespace, kind }) => { + const { t } = useGitOpsTranslation(); + const [project, loaded, loadError] = useK8sWatchResource({ + groupVersionKind: { + group: 'argoproj.io', + kind: 'AppProject', + version: 'v1alpha1', + }, + kind, + name, + namespace, + }); + + const actions = useProjectActionsProvider(project); + + const pages = React.useMemo( + () => [ + { + href: '', + name: t('Details'), + component: ProjectDetailsTab, + }, + { + href: 'yaml', + name: t('YAML'), + component: ResourceYAMLTab, + }, + { + href: 'allowdeny', + name: t('Allow/Deny'), + component: ProjectAllowDenyTab, + }, + { + href: 'applications', + name: t('Applications'), + component: ProjectApplicationsTab, + }, + { + href: 'roles', + name: t('Roles'), + component: ProjectRolesTab, + }, + { + href: 'syncWindows', + name: t('Sync Windows'), + component: ProjectSyncWindowsTab, + }, + { + href: 'events', + name: t('Events'), + component: EventsTab, + }, + ], + [t], + ); + + return ( + <> + + {/* eslint-disable-next-line no-nested-ternary */} + {loaded && !loadError ? ( +
+ +
+ ) : loadError ? ( + + ) : ( + + + + )} + + ); +}; + +export default ProjectNavPage; diff --git a/src/gitops/components/project/ProjectRolesTab.tsx b/src/gitops/components/project/ProjectRolesTab.tsx new file mode 100644 index 000000000..ab116ec2f --- /dev/null +++ b/src/gitops/components/project/ProjectRolesTab.tsx @@ -0,0 +1,221 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; + +import { Badge, EmptyState, EmptyStateBody, PageSection, Title } 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 { AppProjectKind, Role } from '../../models/AppProjectModel'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; + +type ProjectRolesTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: AppProjectKind; +}; + +const ProjectRolesTab: React.FC = ({ obj }) => { + const { t } = useGitOpsTranslation(); + + const roles = React.useMemo(() => obj?.spec?.roles || [], [obj?.spec?.roles]); + + const columnSortConfig = React.useMemo( + () => ['name', 'description', 'groups', 'policies'].map((key) => ({ key })), + [], + ); + + const { sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); + const columnsDV = useRolesColumnsDV(getSortParams, t); + const sortedRoles = React.useMemo(() => { + return sortRolesData(roles, sortBy, direction); + }, [roles, sortBy, direction]); + + const rows = useRolesRowsDV(sortedRoles, t); + + if (!obj) return null; + + const empty = ( + + + + + + {t('This AppProject does not have any roles configured.')} + + + + + + ); + + return ( + + + {t('Roles')} + + + + ); +}; + +const sortRolesData = ( + data: Role[], + sortBy: string | undefined, + direction: 'asc' | 'desc' | undefined, +) => { + if (!sortBy || !direction) return data; + + return [...data].sort((a, b) => { + let aValue: any, bValue: any; + + switch (sortBy) { + case 'name': + aValue = a.name || ''; + bValue = b.name || ''; + break; + case 'description': + aValue = a.description || ''; + bValue = b.description || ''; + break; + case 'groups': + const aGroupsCount = a.groups?.length || 0; + const bGroupsCount = b.groups?.length || 0; + if (aGroupsCount !== bGroupsCount) { + aValue = aGroupsCount; + bValue = bGroupsCount; + } else { + aValue = a.groups?.slice().sort().join(', ') || ''; + bValue = b.groups?.slice().sort().join(', ') || ''; + } + break; + case 'policies': + const aPoliciesCount = a.policies?.length || 0; + const bPoliciesCount = b.policies?.length || 0; + if (aPoliciesCount !== bPoliciesCount) { + aValue = aPoliciesCount; + bValue = bPoliciesCount; + } else { + aValue = a.policies?.slice().sort().join(', ') || ''; + bValue = b.policies?.slice().sort().join(', ') || ''; + } + break; + default: + return 0; + } + + if (direction === 'asc') { + if (aValue < bValue) return -1; + if (aValue > bValue) return 1; + return 0; + } else { + if (aValue > bValue) return -1; + if (aValue < bValue) return 1; + return 0; + } + }); +}; + +export const useRolesColumnsDV = ( + getSortParams: (columnIndex: number) => ThProps['sort'], + t: (key: string) => string, +): DataViewTh[] => { + const columns: DataViewTh[] = [ + { + cell: t('Name'), + props: { + 'aria-label': 'name', + className: 'pf-m-width-20', + sort: getSortParams(0), + style: { minWidth: '150px' }, + }, + }, + { + cell: t('Description'), + props: { + 'aria-label': 'description', + className: 'pf-m-width-30', + sort: getSortParams(1), + style: { minWidth: '200px' }, + }, + }, + { + cell: t('Groups'), + props: { + 'aria-label': 'groups', + className: 'pf-m-width-25', + sort: getSortParams(2), + }, + }, + { + cell: t('Policies'), + props: { + 'aria-label': 'policies', + className: 'pf-m-width-25', + sort: getSortParams(3), + }, + }, + ]; + + return columns; +}; + +const useRolesRowsDV = (roles: Role[], t: (key: string) => string): DataViewTr[] => { + const rows: DataViewTr[] = []; + + roles.forEach((role, index) => { + rows.push([ + { + cell: {role.name}, + id: `name-${index}`, + dataLabel: t('Name'), + }, + { + cell: role.description || '-', + id: `description-${index}`, + dataLabel: t('Description'), + }, + { + cell: + role.groups && role.groups.length > 0 ? ( +
+ {role.groups.map((group, idx) => ( + + {group} + + ))} +
+ ) : ( + '-' + ), + id: `groups-${index}`, + dataLabel: t('Groups'), + }, + { + cell: + role.policies && role.policies.length > 0 ? ( +
+ {role.policies.map((policy, idx) => ( + + {policy} + + ))} +
+ ) : ( + '-' + ), + id: `policies-${index}`, + dataLabel: t('Policies'), + }, + ]); + }); + + return rows; +}; + +export default ProjectRolesTab; diff --git a/src/gitops/components/project/ProjectSyncWindowsTab.tsx b/src/gitops/components/project/ProjectSyncWindowsTab.tsx new file mode 100644 index 000000000..641200cab --- /dev/null +++ b/src/gitops/components/project/ProjectSyncWindowsTab.tsx @@ -0,0 +1,373 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; + +import { Badge, EmptyState, EmptyStateBody, PageSection, Title } 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 { AppProjectKind, SyncWindow } from '../../models/AppProjectModel'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; + +type ProjectSyncWindowsTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: AppProjectKind; +}; + +const ProjectSyncWindowsTab: React.FC = ({ obj }) => { + const { t } = useGitOpsTranslation(); + + const syncWindows = React.useMemo(() => obj?.spec?.syncWindows || [], [obj?.spec?.syncWindows]); + + const columnSortConfig = React.useMemo( + () => + [ + 'kind', + 'schedule', + 'duration', + 'applications', + 'clusters', + 'namespaces', + 'manualSync', + 'timeZone', + ].map((key) => ({ key })), + [], + ); + + const { sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); + const columnsDV = useSyncWindowsColumnsDV(getSortParams, t); + const sortedSyncWindows = React.useMemo(() => { + return sortSyncWindowsData(syncWindows, sortBy, direction); + }, [syncWindows, sortBy, direction]); + + const rows = useSyncWindowsRowsDV(sortedSyncWindows, t); + + if (!obj) return null; + + const empty = ( + + + + + + {t('This AppProject does not have any sync windows configured.')} + + + + + + ); + + return ( + + + {t('Sync Windows')} + + + + ); +}; + +const parseDurationToMinutes = (duration: string): number => { + if (!duration) return 0; + + const hourMatch = duration.match(/(\d+)h/i); + const minuteMatch = duration.match(/(\d+)m/i); + const secondMatch = duration.match(/(\d+)s/i); + + const hours = hourMatch ? parseInt(hourMatch[1], 10) : 0; + const minutes = minuteMatch ? parseInt(minuteMatch[1], 10) : 0; + const seconds = secondMatch ? parseInt(secondMatch[1], 10) : 0; + + return hours * 60 + minutes + seconds / 60; +}; + +const normalizeScheduleForSort = (schedule: string): number => { + if (!schedule) return 0; + + const parts = schedule.trim().split(/\s+/); + + if (parts.length >= 2) { + const minute = parseInt(parts[0], 10) || 0; + const hour = parseInt(parts[1], 10) || 0; + return hour * 60 + minute; + } + + return 0; +}; + +const sortSyncWindowsData = ( + data: SyncWindow[], + sortBy: string | undefined, + direction: 'asc' | 'desc' | undefined, +) => { + if (!sortBy || !direction) return data; + + return [...data].sort((a, b) => { + let aValue: any, bValue: any; + + switch (sortBy) { + case 'kind': + aValue = a.kind || ''; + bValue = b.kind || ''; + break; + case 'schedule': + aValue = normalizeScheduleForSort(a.schedule || ''); + bValue = normalizeScheduleForSort(b.schedule || ''); + if (aValue === 0 && bValue === 0) { + aValue = a.schedule || ''; + bValue = b.schedule || ''; + } + break; + case 'duration': + aValue = parseDurationToMinutes(a.duration || ''); + bValue = parseDurationToMinutes(b.duration || ''); + break; + case 'applications': + const aAppsCount = a.applications?.length || 0; + const bAppsCount = b.applications?.length || 0; + if (aAppsCount !== bAppsCount) { + aValue = aAppsCount; + bValue = bAppsCount; + } else { + aValue = a.applications?.[0] || ''; + bValue = b.applications?.[0] || ''; + } + break; + case 'clusters': + const aClustersCount = a.clusters?.length || 0; + const bClustersCount = b.clusters?.length || 0; + if (aClustersCount !== bClustersCount) { + aValue = aClustersCount; + bValue = bClustersCount; + } else { + aValue = a.clusters?.[0] || ''; + bValue = b.clusters?.[0] || ''; + } + break; + case 'namespaces': + const aNamespacesCount = a.namespaces?.length || 0; + const bNamespacesCount = b.namespaces?.length || 0; + if (aNamespacesCount !== bNamespacesCount) { + aValue = aNamespacesCount; + bValue = bNamespacesCount; + } else { + aValue = a.namespaces?.[0] || ''; + bValue = b.namespaces?.[0] || ''; + } + break; + case 'manualSync': + if (a.manualSync !== undefined) { + aValue = a.manualSync ? 1 : 0; + } else { + aValue = -1; + } + if (b.manualSync !== undefined) { + bValue = b.manualSync ? 1 : 0; + } else { + bValue = -1; + } + break; + case 'timeZone': + aValue = a.timeZone || ''; + bValue = b.timeZone || ''; + break; + default: + return 0; + } + + if (direction === 'asc') { + if (aValue < bValue) return -1; + if (aValue > bValue) return 1; + return 0; + } else { + if (aValue > bValue) return -1; + if (aValue < bValue) return 1; + return 0; + } + }); +}; + +export const useSyncWindowsColumnsDV = ( + getSortParams: (columnIndex: number) => ThProps['sort'], + t: (key: string) => string, +): DataViewTh[] => { + const columns: DataViewTh[] = [ + { + cell: t('Kind'), + props: { + 'aria-label': 'kind', + className: 'pf-m-width-10', + sort: getSortParams(0), + }, + }, + { + cell: t('Schedule'), + props: { + 'aria-label': 'schedule', + className: 'pf-m-width-15', + sort: getSortParams(1), + }, + }, + { + cell: t('Duration'), + props: { + 'aria-label': 'duration', + className: 'pf-m-width-10', + sort: getSortParams(2), + }, + }, + { + cell: t('Applications'), + props: { + 'aria-label': 'applications', + className: 'pf-m-width-15', + sort: getSortParams(3), + }, + }, + { + cell: t('Clusters'), + props: { + 'aria-label': 'clusters', + className: 'pf-m-width-15', + sort: getSortParams(4), + }, + }, + { + cell: t('Namespaces'), + props: { + 'aria-label': 'namespaces', + className: 'pf-m-width-15', + sort: getSortParams(5), + }, + }, + { + cell: t('Manual Sync'), + props: { + 'aria-label': 'manual sync', + className: 'pf-m-width-10', + sort: getSortParams(6), + }, + }, + { + cell: t('Time Zone'), + props: { + 'aria-label': 'time zone', + className: 'pf-m-width-10', + sort: getSortParams(7), + }, + }, + ]; + + return columns; +}; + +const useSyncWindowsRowsDV = ( + syncWindows: SyncWindow[], + t: (key: string) => string, +): DataViewTr[] => { + const rows: DataViewTr[] = []; + + syncWindows.forEach((window, index) => { + rows.push([ + { + cell: ( + + {window.kind || '-'} + + ), + id: `kind-${index}`, + dataLabel: t('Kind'), + }, + { + cell: window.schedule || '-', + id: `schedule-${index}`, + dataLabel: t('Schedule'), + }, + { + cell: window.duration || '-', + id: `duration-${index}`, + dataLabel: t('Duration'), + }, + { + cell: + window.applications && window.applications.length > 0 ? ( +
+ {window.applications.map((app, idx) => ( + + {app} + + ))} +
+ ) : ( + {t('All')} + ), + id: `applications-${index}`, + dataLabel: t('Applications'), + }, + { + cell: + window.clusters && window.clusters.length > 0 ? ( +
+ {window.clusters.map((cluster, idx) => ( + + {cluster} + + ))} +
+ ) : ( + {t('All')} + ), + id: `clusters-${index}`, + dataLabel: t('Clusters'), + }, + { + cell: + window.namespaces && window.namespaces.length > 0 ? ( +
+ {window.namespaces.map((ns, idx) => ( + + {ns} + + ))} +
+ ) : ( + {t('All')} + ), + id: `namespaces-${index}`, + dataLabel: t('Namespaces'), + }, + { + cell: + window.manualSync !== undefined ? ( + + {window.manualSync ? t('Allowed') : t('Denied')} + + ) : ( + '-' + ), + id: `manualSync-${index}`, + dataLabel: t('Manual Sync'), + }, + { + cell: window.timeZone || '-', + id: `timeZone-${index}`, + dataLabel: t('Time Zone'), + }, + ]); + }); + + return rows; +}; + +export default ProjectSyncWindowsTab; diff --git a/src/gitops/components/project/ResourceAllowDenyList.tsx b/src/gitops/components/project/ResourceAllowDenyList.tsx new file mode 100644 index 000000000..3718d19e1 --- /dev/null +++ b/src/gitops/components/project/ResourceAllowDenyList.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; + +import { EmptyState, EmptyStateBody, PageSection } from '@patternfly/react-core'; +import { CubesIcon } from '@patternfly/react-icons'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; + +import { ResourceAllowDeny } from '../../models/AppProjectModel'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; + +interface ResourceAllowDenyListProps { + list?: ResourceAllowDeny[]; +} + +const ResourceAllowDenyList: React.FC = ({ list }) => { + const { t } = useGitOpsTranslation(); + + const resourceList = list || []; + + return ( + + {resourceList.length > 0 ? ( + + + + + + + + + {resourceList.map((resource, index) => ( + + + + + ))} + +
{t('Kind')}{t('Group')}
{resource.kind || '-'}{resource.group || '-'}
+ ) : ( + + {t('This list does not have any resources configured.')} + + )} +
+ ); +}; + +export default ResourceAllowDenyList; diff --git a/src/gitops/components/project/project-list.scss b/src/gitops/components/project/project-list.scss index e69de29bb..26cc7b70f 100644 --- a/src/gitops/components/project/project-list.scss +++ b/src/gitops/components/project/project-list.scss @@ -0,0 +1,22 @@ +// Prevent vertical text wrapping in Name and Namespace columns +.pf-c-table tbody td[data-label='Name'], +.pf-c-table tbody td[data-label='Namespace'] { + white-space: nowrap !important; + overflow: hidden; + text-overflow: ellipsis; + + a, + .co-resource-item { + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + max-width: 100%; + } +} + +// Description column text truncation +.pf-c-table tbody td[data-label='Description'] { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx b/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx index c971b7ff1..9f08a7188 100644 --- a/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx +++ b/src/gitops/components/shared/BaseDetailsSummary/BaseDetailsSummary.tsx @@ -101,7 +101,12 @@ const MetadataLabels: React.FC = ({ kind, labels }) => { ); }; -export const BaseDetailsSummary: React.FC = ({ obj, model, nameLink }) => { +export const BaseDetailsSummary: React.FC = ({ + obj, + model, + nameLink, + showOwner = true, +}) => { const { t } = useGitOpsTranslation(); const [canPatch, canUpdate] = useObjectModifyPermissions(obj, model); const launchLabelsModal = useLabelsModal(obj); @@ -220,14 +225,16 @@ export const BaseDetailsSummary: React.FC = ({ obj, mod > - - - + {showOwner && ( + + + + )} ); @@ -237,6 +244,7 @@ export type BaseDetailsSummaryProps = { obj: K8sResourceKind; model: K8sModel; nameLink?: React.ReactNode; + showOwner?: boolean; }; export default BaseDetailsSummary; diff --git a/src/gitops/components/shared/MetadataLabels/MetadataLabels.tsx b/src/gitops/components/shared/MetadataLabels/MetadataLabels.tsx new file mode 100644 index 000000000..016b8ec02 --- /dev/null +++ b/src/gitops/components/shared/MetadataLabels/MetadataLabels.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import classNames from 'classnames'; + +import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; +import { getSelectorSearchURL, kindForReference } from '@gitops/utils/utils'; +import { K8sResourceKindReference } from '@openshift-console/dynamic-plugin-sdk'; +import { LabelGroup } from '@patternfly/react-core'; +import { Label as PfLabel } from '@patternfly/react-core'; + +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}} + + + ); +}; + +type MetadataLabelsProps = { + kind: K8sResourceKindReference; + labels?: { [key: string]: string }; +}; + +export const MetadataLabels: React.FC = ({ kind, labels }) => { + const { t } = useGitOpsTranslation(); + return labels && Object.keys(labels).length > 0 ? ( + + {Object.keys(labels || {})?.map((key) => { + return ( + + {labels[key] ? `${key}=${labels[key]}` : key} + + ); + })} + + ) : ( + {t('No labels')} + ); +}; + +export default MetadataLabels; diff --git a/src/gitops/components/shared/MetadataLabels/index.ts b/src/gitops/components/shared/MetadataLabels/index.ts new file mode 100644 index 000000000..679cef799 --- /dev/null +++ b/src/gitops/components/shared/MetadataLabels/index.ts @@ -0,0 +1 @@ +export { default, MetadataLabels } from './MetadataLabels'; diff --git a/src/gitops/models/AppProjectModel.ts b/src/gitops/models/AppProjectModel.ts index b7404429f..a70ce1c8f 100644 --- a/src/gitops/models/AppProjectModel.ts +++ b/src/gitops/models/AppProjectModel.ts @@ -57,6 +57,7 @@ export type AppProjectKind = K8sResourceCommon & { namespaceResourceBlacklist?: ResourceAllowDeny[]; roles?: Role[]; syncWindows?: SyncWindow[]; + permitOnlyProjectScopedClusters?: boolean; }; status?: { [key: string]: any }; }; diff --git a/src/gitops/utils/project-utils.ts b/src/gitops/utils/project-utils.ts new file mode 100644 index 000000000..c9838be3c --- /dev/null +++ b/src/gitops/utils/project-utils.ts @@ -0,0 +1,18 @@ +/** + * Utility functions for AppProject components + */ + +/** + * Checks if a string value represents a deny rule (starts with '!') + */ +export const isDenyRule = (value: string | undefined): boolean => { + return value?.startsWith('!') || false; +}; + +/** + * Gets the display value from a string, removing the '!' prefix if present + */ +export const getDisplayValue = (value: string | undefined): string => { + if (!value) return '-'; + return isDenyRule(value) ? value.substring(1) : value; +};