diff --git a/console-extensions.json b/console-extensions.json index df049e22..b823fec3 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 6251d791..69a80c10 100644 --- a/locales/en/plugin__gitops-plugin.json +++ b/locales/en/plugin__gitops-plugin.json @@ -118,20 +118,70 @@ "Current health status of the ApplicationSet.": "Current health status of the ApplicationSet.", "Generated Apps": "Generated Apps", "Number of applications generated by this ApplicationSet.": "Number of applications generated by this ApplicationSet.", - "applications": "applications", "application": "application", + "applications": "applications", "Generators": "Generators", "Number of generators configured in this ApplicationSet.": "Number of generators configured in this ApplicationSet.", - "generators": "generators", "generator": "generator", - "App Project": "App Project", + "generators": "generators", + "App Project": "AppProject", "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", + "Allowed Sources help": "Git repositories and namespaces that are allowed as sources for applications in this project.", + "Repositories": "Repositories", + "Namespaces": "Namespaces", + "Allowed Destinations": "Allowed Destinations", + "Allowed Destinations help": "Clusters and namespaces where applications in this project are allowed to be deployed.", + "Resource Allow/Deny Lists": "Resource Allow/Deny Lists", + "Resource Allow/Deny Lists help": "Lists of Kubernetes resources that are allowed or denied for applications in this project. Cluster-scoped resources apply to all clusters, while namespace-scoped resources apply to specific namespaces.", + "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", + "Error: Missing required route parameters": "Error: Missing required route parameters", + "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 clusters and namespaces where applications are allowed to be deployed.": "Number of clusters and namespaces where applications are allowed to be deployed.", + "destination": "destination", + "destinations": "destinations", + "Source Repositories": "Source Repositories", + "Number of allowed source repositories for this AppProject.": "Number of allowed source repositories for this AppProject.", + "repository": "repository", + "repositories": "repositories", + "Source Namespaces": "Source Namespaces", + "Number of allowed source namespaces for this AppProject.": "Number of allowed source namespaces for this AppProject.", + "namespace": "namespace", + "namespaces": "namespaces", + "Roles": "Roles", + "Number of roles configured in this AppProject.": "Number of roles configured in this AppProject.", + "role": "role", + "roles": "roles", + "Sync Windows": "Sync Windows", + "Number of sync windows configured in this AppProject.": "Number of sync windows configured in this AppProject.", + "sync window": "sync window", + "sync windows": "sync windows", + "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.", @@ -140,26 +190,39 @@ "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.", - "AppProjects": "AppProjects", + "AppProjects": "ArgoCD 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", + "ArgoCD AppProject": "ArgoCD AppProject", + "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", @@ -171,8 +234,8 @@ "There are no pods associated with the rollout.": "There are no pods associated with the rollout.", "Edit Pod": "Edit Pod", "Promote": "Promote", - "Abort": "Abort", "Full Promote": "Full Promote", + "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", "Edit Rollout": "Edit Rollout", @@ -181,21 +244,25 @@ "Ready containers": "Ready containers", "ready": "ready", "0 Pods": "0 Pods", + "Stable": "Stable", + "Active": "Active", + "Preview": "Preview", + "Canary": "Canary", "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.", - "There was an error retrieving rollouts. Check your connection and reload the page.": "There was an error retrieving rollouts. Check your connection and reload the page.", + "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", "Pods": "Pods", "Selector": "Selector", + "No labels": "No labels", "Rollout Status": "Rollout Status", "Revisions": "Revisions", "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.", @@ -220,7 +287,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.", - "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.", "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.", @@ -228,7 +294,7 @@ "No matching Argo CD ApplicationSets": "No matching Argo CD ApplicationSets", "No Argo CD ApplicationSets": "No Argo CD ApplicationSets", "There was an error retrieving applicationsets. Check your connection and reload the page.": "There was an error retrieving applicationsets. Check your connection and reload the page.", - "ApplicationSets": "Argo CD ApplicationSets", + "ApplicationSets": "ArgoCD ApplicationSets", "Create ApplicationSet": "Create ApplicationSet", "View in Argo CD": "View in Argo CD", "Name must be unique within a namespace.": "Name must be unique within a namespace.", diff --git a/locales/ja/plugin__gitops-plugin.json b/locales/ja/plugin__gitops-plugin.json index d3bbfcff..c5645a69 100644 --- a/locales/ja/plugin__gitops-plugin.json +++ b/locales/ja/plugin__gitops-plugin.json @@ -118,20 +118,70 @@ "Current health status of the ApplicationSet.": "Current health status of the ApplicationSet.", "Generated Apps": "Generated Apps", "Number of applications generated by this ApplicationSet.": "Number of applications generated by this ApplicationSet.", - "applications": "applications", "application": "application", + "applications": "applications", "Generators": "Generators", "Number of generators configured in this ApplicationSet.": "Number of generators configured in this ApplicationSet.", - "generators": "generators", "generator": "generator", - "App Project": "App Project", + "generators": "generators", + "App Project": "AppProject", "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", + "Allowed Sources help": "Git repositories and namespaces that are allowed as sources for applications in this project.", + "Repositories": "Repositories", + "Namespaces": "Namespaces", + "Allowed Destinations": "Allowed Destinations", + "Allowed Destinations help": "Clusters and namespaces where applications in this project are allowed to be deployed.", + "Resource Allow/Deny Lists": "Resource Allow/Deny Lists", + "Resource Allow/Deny Lists help": "Lists of Kubernetes resources that are allowed or denied for applications in this project. Cluster-scoped resources apply to all clusters, while namespace-scoped resources apply to specific namespaces.", + "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", + "Error: Missing required route parameters": "Error: Missing required route parameters", + "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 clusters and namespaces where applications are allowed to be deployed.": "Number of clusters and namespaces where applications are allowed to be deployed.", + "destination": "destination", + "destinations": "destinations", + "Source Repositories": "Source Repositories", + "Number of allowed source repositories for this AppProject.": "Number of allowed source repositories for this AppProject.", + "repository": "repository", + "repositories": "repositories", + "Source Namespaces": "Source Namespaces", + "Number of allowed source namespaces for this AppProject.": "Number of allowed source namespaces for this AppProject.", + "namespace": "namespace", + "namespaces": "namespaces", + "Roles": "Roles", + "Number of roles configured in this AppProject.": "Number of roles configured in this AppProject.", + "role": "role", + "roles": "roles", + "Sync Windows": "Sync Windows", + "Number of sync windows configured in this AppProject.": "Number of sync windows configured in this AppProject.", + "sync window": "sync window", + "sync windows": "sync windows", + "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 +193,36 @@ "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", + "ArgoCD AppProject": "ArgoCD AppProject", + "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", @@ -171,8 +234,8 @@ "There are no pods associated with the rollout.": "There are no pods associated with the rollout.", "Edit Pod": "Edit Pod", "Promote": "Promote", - "Abort": "Abort", "Full Promote": "Full Promote", + "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", "Edit Rollout": "Edit Rollout", @@ -181,21 +244,25 @@ "Ready containers": "Ready containers", "ready": "ready", "0 Pods": "0 Pods", + "Stable": "Stable", + "Active": "Active", + "Preview": "Preview", + "Canary": "Canary", "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.", - "There was an error retrieving rollouts. Check your connection and reload the page.": "There was an error retrieving rollouts. Check your connection and reload the page.", + "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", "Pods": "Pods", "Selector": "Selector", + "No labels": "No labels", "Rollout Status": "Rollout Status", "Revisions": "Revisions", "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.", @@ -220,7 +287,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.", - "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.", "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 be6ccd7c..7c392607 100644 --- a/locales/ko/plugin__gitops-plugin.json +++ b/locales/ko/plugin__gitops-plugin.json @@ -118,20 +118,70 @@ "Current health status of the ApplicationSet.": "Current health status of the ApplicationSet.", "Generated Apps": "Generated Apps", "Number of applications generated by this ApplicationSet.": "Number of applications generated by this ApplicationSet.", - "applications": "applications", "application": "application", + "applications": "applications", "Generators": "Generators", "Number of generators configured in this ApplicationSet.": "Number of generators configured in this ApplicationSet.", - "generators": "generators", "generator": "generator", + "generators": "generators", "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": "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", + "Allowed Sources help": "Git repositories and namespaces that are allowed as sources for applications in this project.", + "Repositories": "Repositories", + "Namespaces": "Namespaces", + "Allowed Destinations": "Allowed Destinations", + "Allowed Destinations help": "Clusters and namespaces where applications in this project are allowed to be deployed.", + "Resource Allow/Deny Lists": "Resource Allow/Deny Lists", + "Resource Allow/Deny Lists help": "Lists of Kubernetes resources that are allowed or denied for applications in this project. Cluster-scoped resources apply to all clusters, while namespace-scoped resources apply to specific namespaces.", + "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", + "Error: Missing required route parameters": "Error: Missing required route parameters", + "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 clusters and namespaces where applications are allowed to be deployed.": "Number of clusters and namespaces where applications are allowed to be deployed.", + "destination": "destination", + "destinations": "destinations", + "Source Repositories": "Source Repositories", + "Number of allowed source repositories for this AppProject.": "Number of allowed source repositories for this AppProject.", + "repository": "repository", + "repositories": "repositories", + "Source Namespaces": "Source Namespaces", + "Number of allowed source namespaces for this AppProject.": "Number of allowed source namespaces for this AppProject.", + "namespace": "namespace", + "namespaces": "namespaces", + "Roles": "Roles", + "Number of roles configured in this AppProject.": "Number of roles configured in this AppProject.", + "role": "role", + "roles": "roles", + "Sync Windows": "Sync Windows", + "Number of sync windows configured in this AppProject.": "Number of sync windows configured in this AppProject.", + "sync window": "sync window", + "sync windows": "sync windows", + "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 +193,36 @@ "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", + "ArgoCD AppProject": "ArgoCD AppProject", + "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", @@ -171,8 +234,8 @@ "There are no pods associated with the rollout.": "There are no pods associated with the rollout.", "Edit Pod": "Edit Pod", "Promote": "Promote", - "Abort": "Abort", "Full Promote": "Full Promote", + "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", "Edit Rollout": "Edit Rollout", @@ -181,21 +244,25 @@ "Ready containers": "Ready containers", "ready": "ready", "0 Pods": "0 Pods", + "Stable": "Stable", + "Active": "Active", + "Preview": "Preview", + "Canary": "Canary", "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.", - "There was an error retrieving rollouts. Check your connection and reload the page.": "There was an error retrieving rollouts. Check your connection and reload the page.", + "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", "Pods": "Pods", "Selector": "Selector", + "No labels": "No labels", "Rollout Status": "Rollout Status", "Revisions": "Revisions", "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.", @@ -220,7 +287,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.", - "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.", "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 32abf300..da5f71c8 100644 --- a/locales/zh/plugin__gitops-plugin.json +++ b/locales/zh/plugin__gitops-plugin.json @@ -118,20 +118,70 @@ "Current health status of the ApplicationSet.": "Current health status of the ApplicationSet.", "Generated Apps": "Generated Apps", "Number of applications generated by this ApplicationSet.": "Number of applications generated by this ApplicationSet.", - "applications": "applications", "application": "application", + "applications": "applications", "Generators": "Generators", "Number of generators configured in this ApplicationSet.": "Number of generators configured in this ApplicationSet.", - "generators": "generators", "generator": "generator", + "generators": "generators", "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": "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", + "Allowed Sources help": "Git repositories and namespaces that are allowed as sources for applications in this project.", + "Repositories": "Repositories", + "Namespaces": "Namespaces", + "Allowed Destinations": "Allowed Destinations", + "Allowed Destinations help": "Clusters and namespaces where applications in this project are allowed to be deployed.", + "Resource Allow/Deny Lists": "Resource Allow/Deny Lists", + "Resource Allow/Deny Lists help": "Lists of Kubernetes resources that are allowed or denied for applications in this project. Cluster-scoped resources apply to all clusters, while namespace-scoped resources apply to specific namespaces.", + "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", + "Error: Missing required route parameters": "Error: Missing required route parameters", + "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 clusters and namespaces where applications are allowed to be deployed.": "Number of clusters and namespaces where applications are allowed to be deployed.", + "destination": "destination", + "destinations": "destinations", + "Source Repositories": "Source Repositories", + "Number of allowed source repositories for this AppProject.": "Number of allowed source repositories for this AppProject.", + "repository": "repository", + "repositories": "repositories", + "Source Namespaces": "Source Namespaces", + "Number of allowed source namespaces for this AppProject.": "Number of allowed source namespaces for this AppProject.", + "namespace": "namespace", + "namespaces": "namespaces", + "Roles": "Roles", + "Number of roles configured in this AppProject.": "Number of roles configured in this AppProject.", + "role": "role", + "roles": "roles", + "Sync Windows": "Sync Windows", + "Number of sync windows configured in this AppProject.": "Number of sync windows configured in this AppProject.", + "sync window": "sync window", + "sync windows": "sync windows", + "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 +193,36 @@ "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", + "ArgoCD AppProject": "ArgoCD AppProject", + "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", @@ -171,8 +234,8 @@ "There are no pods associated with the rollout.": "There are no pods associated with the rollout.", "Edit Pod": "Edit Pod", "Promote": "Promote", - "Abort": "Abort", "Full Promote": "Full Promote", + "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", "Edit Rollout": "Edit Rollout", @@ -181,21 +244,25 @@ "Ready containers": "Ready containers", "ready": "ready", "0 Pods": "0 Pods", + "Stable": "Stable", + "Active": "Active", + "Preview": "Preview", + "Canary": "Canary", "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.", - "There was an error retrieving rollouts. Check your connection and reload the page.": "There was an error retrieving rollouts. Check your connection and reload the page.", + "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", "Pods": "Pods", "Selector": "Selector", + "No labels": "No labels", "Rollout Status": "Rollout Status", "Revisions": "Revisions", "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.", @@ -220,7 +287,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.", - "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.", "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 b2d6c4e5..0a75e668 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/appset/AppSetDetailsTab.tsx b/src/gitops/components/appset/AppSetDetailsTab.tsx index 585c5e3a..95ab3e8e 100644 --- a/src/gitops/components/appset/AppSetDetailsTab.tsx +++ b/src/gitops/components/appset/AppSetDetailsTab.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; +import { Link, useLocation } from 'react-router-dom-v5-compat'; import { ResourceLink, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; -import { Badge, DescriptionList, Flex, FlexItem, PageSection, Title } from '@patternfly/react-core'; +import { DescriptionList, Flex, FlexItem, PageSection, Title } from '@patternfly/react-core'; import { ApplicationKind, ApplicationModel } from '../../models/ApplicationModel'; import { ApplicationSetKind, ApplicationSetModel } from '../../models/ApplicationSetModel'; @@ -20,10 +21,32 @@ type AppSetDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & obj?: ApplicationSetKind; }; -const AppSetDetailsTab: React.FC = ({ obj }) => { +const AppSetDetailsTab: React.FC = ({ obj, match }) => { const { t } = useGitOpsTranslation(); + const location = useLocation(); const namespace = obj?.metadata?.namespace; + const getTabUrl = (tab: string) => { + const knownTabs = new Set(['yaml', 'generators', 'applications', 'events']); + + let baseUrl = match?.url; + + if (baseUrl) { + const segments = baseUrl.split('/'); + const lastSegment = segments.at(-1) || ''; + if (knownTabs.has(lastSegment)) { + baseUrl = segments.slice(0, -1).join('/'); + } + return `${baseUrl}/${tab}`; + } + + const currentPath = location.pathname; + const segments = currentPath.split('/'); + const lastSegment = segments.at(-1) || ''; + const isOnTab = knownTabs.has(lastSegment); + return isOnTab ? `../${tab}` : tab; + }; + // Get applications to count generated apps const [applications] = useK8sWatchResource({ groupVersionKind: { @@ -79,19 +102,19 @@ const AppSetDetailsTab: React.FC = ({ obj }) => { title={t('Generated Apps')} help={t('Number of applications generated by this ApplicationSet.')} > - + {generatedAppsCount}{' '} - {generatedAppsCount !== 1 ? t('applications') : t('application')} - + {generatedAppsCount === 1 ? t('application') : t('applications')} + - - {totalGenerators} {totalGenerators !== 1 ? t('generators') : t('generator')} - + + {totalGenerators} {totalGenerators === 1 ? t('generator') : t('generators')} + = ({ 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 00000000..73ed4944 --- /dev/null +++ b/src/gitops/components/project/ProjectAllowDenyTab.tsx @@ -0,0 +1,263 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; + +import { useK8sModel } from '@openshift-console/dynamic-plugin-sdk'; +import { + Badge, + Card, + CardBody, + CardHeader, + Flex, + FlexItem, + Grid, + GridItem, + List, + ListItem, + PageSection, + PageSectionVariants, + Panel, + Popover, + Title, +} from '@patternfly/react-core'; +import { QuestionCircleIcon } from '@patternfly/react-icons'; + +import { AppProjectKind } from '../../models/AppProjectModel'; +import { ArgoServer, getArgoServerForProject } from '../../utils/gitops'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import { getDisplayValue, isDenyRule } from '../../utils/project-utils'; +import { ArgoCDLink } from '../shared/ArgoCDLink/ArgoCDLink'; + +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(); + const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' }); + const [argoServer, setArgoServer] = React.useState({ host: '', protocol: '' }); + + React.useEffect(() => { + if (!obj || !model) return; + (async () => { + try { + const server = await getArgoServerForProject(model, obj); + setArgoServer(server); + } catch (err) { + console.warn('Error while fetching Argo CD Server url:', err); + } + })(); + }, [model, obj]); + + if (!obj) return null; + + const spec = obj.spec || {}; + const projectName = obj.metadata?.name; + const argoCDUrl = argoServer.host + ? `${argoServer.protocol}://${argoServer.host}/settings/projects/${projectName}?tab=summary` + : ''; + + return ( + <> + + + + + + + {t('Allowed Sources')} + + + + {t('Allowed Sources')}} + bodyContent={
{t('Allowed Sources help')}
} + > + +
+
+
+
+ {argoCDUrl && ( + + + + )} +
+ + + + + + {t('Repositories')} + + {renderStringArray(spec.sourceRepos, t)} + + + + + + {t('Namespaces')} + + {renderStringArray(spec.sourceNamespaces, t)} + + + + +
+ + + + + + {t('Allowed Destinations')} + + + + {t('Allowed Destinations')}} + bodyContent={
{t('Allowed Destinations help')}
} + > + +
+
+
+ + + + + + + + + + + +
+ + + + + + {t('Resource Allow/Deny Lists')} + + + + {t('Resource Allow/Deny Lists')}} + bodyContent={
{t('Resource Allow/Deny Lists help')}
} + > + +
+
+
+ + + + + + {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 00000000..2b8a32f8 --- /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 00000000..ee5e956a --- /dev/null +++ b/src/gitops/components/project/ProjectDetailsPage.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom-v5-compat'; + +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; + +import ProjectNavPage from './ProjectNavPage'; + +const ProjectDetailsPage: React.FC = () => { + const { t } = useGitOpsTranslation(); + const { name, ns } = useParams<{ name?: string; ns?: string }>(); + + if (!name || !ns) { + return
{t('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 00000000..dd644bf4 --- /dev/null +++ b/src/gitops/components/project/ProjectDetailsTab.tsx @@ -0,0 +1,216 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; +import { Link, useLocation } from 'react-router-dom-v5-compat'; +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, match }) => { + const { t } = useGitOpsTranslation(); + const location = useLocation(); + const namespace = obj?.metadata?.namespace; + + const getTabUrl = (tab: string) => { + const knownTabs = new Set([ + 'yaml', + 'allowdeny', + 'applications', + 'roles', + 'syncWindows', + 'events', + ]); + + let baseUrl = match?.url; + + if (baseUrl) { + const segments = baseUrl.split('/'); + const lastSegment = segments.at(-1) || ''; + if (knownTabs.has(lastSegment)) { + baseUrl = segments.slice(0, -1).join('/'); + } + return `${baseUrl}/${tab}`; + } + + const currentPath = location.pathname; + const segments = currentPath.split('/'); + const lastSegment = segments.at(-1) || ''; + const isOnTab = knownTabs.has(lastSegment); + return isOnTab ? `../${tab}` : tab; + }; + + 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('application') : t('applications')} + + + + + + {destinationsCount}{' '} + {destinationsCount === 1 ? t('destination') : t('destinations')} + + + + + {sourceReposCount} {sourceReposCount === 1 ? t('repository') : t('repositories')} + + + + {sourceNamespacesCount}{' '} + {sourceNamespacesCount === 1 ? t('namespace') : t('namespaces')} + + + + + {rolesCount} {rolesCount === 1 ? t('role') : t('roles')} + + + + + + {syncWindowsCount}{' '} + {syncWindowsCount === 1 ? t('sync window') : t('sync windows')} + + + + {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 5ace36b9..280869a6 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'; @@ -40,6 +32,7 @@ import { useShowOperandsInAllNamespaces, } from '../shared/AllNamespaces'; import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; +import { MetadataLabels } from '../shared/MetadataLabels/MetadataLabels'; import { useProjectActionsProvider } from './hooks/useProjectActionsProvider'; @@ -332,8 +325,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 @@ -342,8 +336,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' }, }, }, ] @@ -352,7 +347,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), }, }, @@ -360,7 +355,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), }, }, @@ -375,7 +370,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), }, }, @@ -388,53 +383,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 00000000..4ad08e45 --- /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 00000000..71c5f285 --- /dev/null +++ b/src/gitops/components/project/ProjectRolesTab.tsx @@ -0,0 +1,263 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; + +import { useK8sModel } from '@openshift-console/dynamic-plugin-sdk'; +import { + Badge, + EmptyState, + EmptyStateBody, + Flex, + FlexItem, + 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 { ArgoServer, getArgoServerForProject } from '../../utils/gitops'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import { ArgoCDLink } from '../shared/ArgoCDLink/ArgoCDLink'; +import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; + +type ProjectRolesTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: AppProjectKind; +}; + +const ProjectRolesTab: React.FC = ({ obj }) => { + const { t } = useGitOpsTranslation(); + const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' }); + const [argoServer, setArgoServer] = React.useState({ host: '', protocol: '' }); + + React.useEffect(() => { + if (!obj || !model) return; + (async () => { + try { + const server = await getArgoServerForProject(model, obj); + setArgoServer(server); + } catch (err) { + console.warn('Error while fetching Argo CD Server url:', err); + } + })(); + }, [model, obj]); + + 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.')} + + + + + + ); + + const projectName = obj?.metadata?.name; + const argoCDUrl = argoServer.host + ? `${argoServer.protocol}://${argoServer.host}/settings/projects/${projectName}?tab=roles` + : ''; + + return ( + + + + + {t('Roles')} + + + {argoCDUrl && ( + + + + )} + + + + ); +}; + +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 00000000..23daa1bc --- /dev/null +++ b/src/gitops/components/project/ProjectSyncWindowsTab.tsx @@ -0,0 +1,415 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; + +import { useK8sModel } from '@openshift-console/dynamic-plugin-sdk'; +import { + Badge, + EmptyState, + EmptyStateBody, + Flex, + FlexItem, + 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 { ArgoServer, getArgoServerForProject } from '../../utils/gitops'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import { ArgoCDLink } from '../shared/ArgoCDLink/ArgoCDLink'; +import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; + +type ProjectSyncWindowsTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: AppProjectKind; +}; + +const ProjectSyncWindowsTab: React.FC = ({ obj }) => { + const { t } = useGitOpsTranslation(); + const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' }); + const [argoServer, setArgoServer] = React.useState({ host: '', protocol: '' }); + + React.useEffect(() => { + if (!obj || !model) return; + (async () => { + try { + const server = await getArgoServerForProject(model, obj); + setArgoServer(server); + } catch (err) { + console.warn('Error while fetching Argo CD Server url:', err); + } + })(); + }, [model, obj]); + + 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.')} + + + + + + ); + + const projectName = obj?.metadata?.name; + const argoCDUrl = argoServer.host + ? `${argoServer.protocol}://${argoServer.host}/settings/projects/${projectName}?tab=windows` + : ''; + + return ( + + + + + {t('Sync Windows')} + + + {argoCDUrl && ( + + + + )} + + + + ); +}; + +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 00000000..3718d19e --- /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 e69de29b..26cc7b70 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 c971b7ff..9f08a718 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 00000000..016b8ec0 --- /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 00000000..679cef79 --- /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 b7404429..a70ce1c8 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/gitops.ts b/src/gitops/utils/gitops.ts index 513da9fb..fcb3adb3 100644 --- a/src/gitops/utils/gitops.ts +++ b/src/gitops/utils/gitops.ts @@ -88,7 +88,52 @@ export const getArgoServer = async (model, app: ApplicationKind): Promise } }, +): Promise => { + const info: ArgoServer = { + host: '', + protocol: '', + }; + + const ns = (): string => { + if (project.metadata?.labels && project.metadata.labels[labelControllerNamespaceKey]) + return project.metadata.labels[labelControllerNamespaceKey]; + else return project.metadata?.namespace || ''; + }; + + try { + const [argoServerURL] = await k8sListItems({ + model: model, + queryParams: { + ns: ns(), + labelSelector: { + matchLabels: { + 'app.kubernetes.io/part-of': 'argocd', + }, + }, + }, + }); + info.protocol = 'https'; + if (argoServerURL && argoServerURL['spec']?.['host']) { + info.host = argoServerURL['spec']['host']; + } else { + const appsIndex = location.host.indexOf('.apps'); + if (appsIndex !== -1) { + info.host = 'argocd-server-' + ns() + location.host.substring(appsIndex); + } } return info; } catch (e) { diff --git a/src/gitops/utils/project-utils.ts b/src/gitops/utils/project-utils.ts new file mode 100644 index 00000000..c9838be3 --- /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; +};