From c56043f1002d3f70a179b138f2c5987ec3e2b528 Mon Sep 17 00:00:00 2001 From: dkarpele Date: Tue, 19 May 2026 18:59:20 +0200 Subject: [PATCH 1/4] feat: Image Updater in the console Signed-off-by: dkarpele --- console-extensions.json | 110 +++- locales/en/plugin__gitops-plugin.json | 53 +- locales/ja/plugin__gitops-plugin.json | 53 +- locales/ko/plugin__gitops-plugin.json | 53 +- locales/zh/plugin__gitops-plugin.json | 53 +- plugin-metadata.ts | 2 + .../imageupdater/ImageUpdaterDetailsPage.tsx | 19 + .../imageupdater/ImageUpdaterDetailsTab.tsx | 126 +++++ .../imageupdater/ImageUpdaterList.tsx | 476 ++++++++++++++++++ .../imageupdater/ImageUpdaterListTab.tsx | 25 + .../imageupdater/ImageUpdaterNavPage.tsx | 90 ++++ .../ImageUpdaterRecentUpdatesTab.tsx | 198 ++++++++ .../hooks/useImageUpdaterActionsProvider.tsx | 94 ++++ .../imageupdater/imageupdater-list.scss | 22 + src/gitops/models/ImageUpdaterModel.ts | 49 ++ src/gitops/templates/imageupdater-yaml.ts | 12 + src/gitops/templates/index.ts | 1 + 17 files changed, 1410 insertions(+), 26 deletions(-) create mode 100644 src/gitops/components/imageupdater/ImageUpdaterDetailsPage.tsx create mode 100644 src/gitops/components/imageupdater/ImageUpdaterDetailsTab.tsx create mode 100644 src/gitops/components/imageupdater/ImageUpdaterList.tsx create mode 100644 src/gitops/components/imageupdater/ImageUpdaterListTab.tsx create mode 100644 src/gitops/components/imageupdater/ImageUpdaterNavPage.tsx create mode 100644 src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx create mode 100644 src/gitops/components/imageupdater/hooks/useImageUpdaterActionsProvider.tsx create mode 100644 src/gitops/components/imageupdater/imageupdater-list.scss create mode 100644 src/gitops/models/ImageUpdaterModel.ts create mode 100644 src/gitops/templates/imageupdater-yaml.ts diff --git a/console-extensions.json b/console-extensions.json index e3da32f3a..41522f4f6 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -61,7 +61,7 @@ "properties": { "id": "gitops-rollouts-topology-side-bar-tab-details", "label": "Details", - "priority": "1000", + "priority": "1000", "insertBefore": "topology-side-bar-tab-resource" }, "flags": { @@ -84,7 +84,7 @@ "id": "gitops-rollouts-topology-sidebar-details-tab-section", "tab": "gitops-rollouts-topology-side-bar-tab-details", "provider": { "$codeRef": "topology.useRolloutSideBarDetails" }, - "insertBefore": "gitops-rollouts-topology-sidebar-resource-tab-section" + "insertBefore": "gitops-rollouts-topology-sidebar-resource-tab-section" }, "flags": { "required": ["ARGO_ROLLOUT", "GITOPS_ENABLE_TOPOLOGY"] @@ -267,6 +267,23 @@ "required": ["ARGO_ROLLOUT"] } }, + { + "type": "console.model-metadata", + "properties": { + "model": { + "group": "argocd-image-updater.argoproj.io", + "kind": "ImageUpdater", + "version": "v1alpha1" + }, + "color": "#E9654B", + "label": "Argo CD Image Updater", + "labelPlural": "Argo CD Image Updaters", + "abbr": "IU" + }, + "flags": { + "required": ["IMAGEUPDATER"] + } + }, { "type": "console.flag/model", "properties": { @@ -289,6 +306,17 @@ } } }, + { + "type": "console.flag/model", + "properties": { + "flag": "IMAGEUPDATER", + "model": { + "group": "argocd-image-updater.argoproj.io", + "kind": "ImageUpdater", + "version": "v1alpha1" + } + } + }, { "type": "console.navigation/resource-ns", "properties": { @@ -340,6 +368,29 @@ "insertAfter": "appproject" } }, + { + "type": "console.navigation/resource-ns", + "properties": { + "id": "imageupdater", + "name": "ImageUpdaters", + "perspective": "admin", + "section": "gitops-navigation-section", + "model": { + "group": "argocd-image-updater.argoproj.io", + "kind": "ImageUpdater", + "version": "v1alpha1" + } + } + }, + { + "type": "console.navigation/separator", + "properties": { + "perspective": "admin", + "section": "gitops-navigation-section", + "id": "argocd-separator", + "insertAfter": "imageupdater" + } + }, { "type": "console.navigation/resource-ns", "properties": { @@ -426,6 +477,24 @@ } } }, + { + "type": "console.page/resource/list", + "flags": { + "required": [ + "IMAGEUPDATER" + ] + }, + "properties": { + "model": { + "group": "argocd-image-updater.argoproj.io", + "kind": "ImageUpdater", + "version": "v1alpha1" + }, + "component": { + "$codeRef": "ImageUpdaterList" + } + } + }, { "type": "console.page/resource/details", "flags": { @@ -538,6 +607,25 @@ } } }, + { + "type": "console.yaml-template", + "flags": { + "required": [ + "IMAGEUPDATER" + ] + }, + "properties": { + "name": "default", + "model": { + "group": "argocd-image-updater.argoproj.io", + "kind": "ImageUpdater", + "version": "v1alpha1" + }, + "template": { + "$codeRef": "yamlTemplates.defaultImageUpdaterYamlTemplate" + } + } + }, { "type": "console.page/resource/details", "flags": { @@ -574,6 +662,24 @@ } } }, + { + "type": "console.page/resource/details", + "flags": { + "required": [ + "IMAGEUPDATER" + ] + }, + "properties": { + "model": { + "group": "argocd-image-updater.argoproj.io", + "kind": "ImageUpdater", + "version": "v1alpha1" + }, + "component": { + "$codeRef": "ImageUpdaterDetailsPage" + } + } + }, { "type": "console.page/resource/search", "properties": { diff --git a/locales/en/plugin__gitops-plugin.json b/locales/en/plugin__gitops-plugin.json index 66e92206d..ffdbf1a23 100644 --- a/locales/en/plugin__gitops-plugin.json +++ b/locales/en/plugin__gitops-plugin.json @@ -105,6 +105,53 @@ "Progressive Sync Flow View": "Progressive Sync Flow View", "Expand or collapse all progressive sync step groups": "Expand or collapse all progressive sync step groups", "No Applications In This Step": "No Applications In This Step", + "Edit ImageUpdater": "Edit ImageUpdater", + "Delete ImageUpdater": "Delete ImageUpdater", + "Error: Missing required route parameters": "Error: Missing required route parameters", + "ImageUpdater details": "ImageUpdater details", + "Ready": "Ready", + "Whether the last reconciliation completed without errors.": "Whether the last reconciliation completed without errors.", + "True": "True", + "False": "False", + "Applications Matched": "Applications Matched", + "Number of applications matched by this ImageUpdater.": "Number of applications matched by this ImageUpdater.", + "Images Managed": "Images Managed", + "Number of images eligible for update checking.": "Number of images eligible for update checking.", + "Last Checked At": "Last Checked At", + "When the controller last checked for image updates.": "When the controller last checked for image updates.", + "Last Updated At": "Last Updated At", + "When the controller last performed an image update.": "When the controller last performed an image update.", + "Observed Generation": "Observed Generation", + "The generation of the resource that was last reconciled.": "The generation of the resource that was last reconciled.", + "Conditions": "Conditions", + "No ImageUpdaters match the search filter": "No ImageUpdaters match the search filter", + "Try removing the filter or searching for a different term to see more ImageUpdaters.": "Try removing the filter or searching for a different term to see more ImageUpdaters.", + "There are no ImageUpdaters in this namespace.": "There are no ImageUpdaters in this namespace.", + "There are no ImageUpdaters in all namespaces.": "There are no ImageUpdaters in all namespaces.", + "No matching ImageUpdaters": "No matching ImageUpdaters", + "No ImageUpdaters": "No ImageUpdaters", + "Unable to load data": "Unable to load data", + "There was an error retrieving ImageUpdaters. Check your connection and reload the page.": "There was an error retrieving ImageUpdaters. Check your connection and reload the page.", + "ImageUpdaters": "ImageUpdaters", + "This list page is under tech preview, but not necessarily the resources it represents": "This list page is under tech preview, but not necessarily the resources it represents", + "Create ImageUpdater": "Create ImageUpdater", + "Search by name...": "Search by name...", + "Apps": "Apps", + "Images": "Images", + "Last Checked": "Last Checked", + "Has Apps": "Has Apps", + "No Apps": "No Apps", + "Not Ready": "Not Ready", + "Recent Updates": "Recent Updates", + "ArgoCD ImageUpdater": "ArgoCD ImageUpdater", + "There was an error retrieving the ImageUpdater. Check your connection and reload the page.": "There was an error retrieving the ImageUpdater. Check your connection and reload the page.", + "No recent updates": "No recent updates", + "No image updates have been recorded in the most recent reconciliation cycle.": "No image updates have been recorded in the most recent reconciliation cycle.", + "Alias": "Alias", + "Image": "Image", + "New Version": "New Version", + "Apps Updated": "Apps Updated", + "Updated At": "Updated At", "Server": "Server", "Deny": "Deny", "Allow": "Allow", @@ -124,7 +171,6 @@ "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.", @@ -156,19 +202,15 @@ "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.", "There are no Argo CD App Projects in all projects.": "There are no Argo CD App Projects in all projects.", "No matching Argo CD App Projects": "No matching Argo CD App Projects", "No Argo CD App Projects": "No Argo CD App Projects", - "Unable to load data": "Unable to load data", "There was an error retrieving App Projects. Check your connection and reload the page.": "There was an error retrieving App Projects. Check your connection and reload the page.", "AppProjects": "AppProjects", - "This list page is under tech preview, but not necessarily the resources it represents": "This list page is under tech preview, but not necessarily the resources it represents", "Create AppProject": "Create AppProject", - "Search by name...": "Search by name...", "Labels": "Labels", "Last Updated": "Last Updated", "Has Description": "Has Description", @@ -205,7 +247,6 @@ "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", "Owner": "Owner", "Memory": "Memory", diff --git a/locales/ja/plugin__gitops-plugin.json b/locales/ja/plugin__gitops-plugin.json index f0e3da81a..2430d79c3 100644 --- a/locales/ja/plugin__gitops-plugin.json +++ b/locales/ja/plugin__gitops-plugin.json @@ -105,6 +105,53 @@ "Progressive Sync Flow View": "Progressive Sync Flow View", "Expand or collapse all progressive sync step groups": "Expand or collapse all progressive sync step groups", "No Applications In This Step": "No Applications In This Step", + "Edit ImageUpdater": "Edit ImageUpdater", + "Delete ImageUpdater": "Delete ImageUpdater", + "Error: Missing required route parameters": "Error: Missing required route parameters", + "ImageUpdater details": "ImageUpdater details", + "Ready": "Ready", + "Whether the last reconciliation completed without errors.": "Whether the last reconciliation completed without errors.", + "True": "True", + "False": "False", + "Applications Matched": "Applications Matched", + "Number of applications matched by this ImageUpdater.": "Number of applications matched by this ImageUpdater.", + "Images Managed": "Images Managed", + "Number of images eligible for update checking.": "Number of images eligible for update checking.", + "Last Checked At": "Last Checked At", + "When the controller last checked for image updates.": "When the controller last checked for image updates.", + "Last Updated At": "Last Updated At", + "When the controller last performed an image update.": "When the controller last performed an image update.", + "Observed Generation": "Observed Generation", + "The generation of the resource that was last reconciled.": "The generation of the resource that was last reconciled.", + "Conditions": "Conditions", + "No ImageUpdaters match the search filter": "No ImageUpdaters match the search filter", + "Try removing the filter or searching for a different term to see more ImageUpdaters.": "Try removing the filter or searching for a different term to see more ImageUpdaters.", + "There are no ImageUpdaters in this namespace.": "There are no ImageUpdaters in this namespace.", + "There are no ImageUpdaters in all namespaces.": "There are no ImageUpdaters in all namespaces.", + "No matching ImageUpdaters": "No matching ImageUpdaters", + "No ImageUpdaters": "No ImageUpdaters", + "Unable to load data": "Unable to load data", + "There was an error retrieving ImageUpdaters. Check your connection and reload the page.": "There was an error retrieving ImageUpdaters. Check your connection and reload the page.", + "ImageUpdaters": "ImageUpdaters", + "This list page is under tech preview, but not necessarily the resources it represents": "This list page is under tech preview, but not necessarily the resources it represents", + "Create ImageUpdater": "Create ImageUpdater", + "Search by name...": "Search by name...", + "Apps": "Apps", + "Images": "Images", + "Last Checked": "Last Checked", + "Has Apps": "Has Apps", + "No Apps": "No Apps", + "Not Ready": "Not Ready", + "Recent Updates": "Recent Updates", + "ArgoCD ImageUpdater": "ArgoCD ImageUpdater", + "There was an error retrieving the ImageUpdater. Check your connection and reload the page.": "There was an error retrieving the ImageUpdater. Check your connection and reload the page.", + "No recent updates": "No recent updates", + "No image updates have been recorded in the most recent reconciliation cycle.": "No image updates have been recorded in the most recent reconciliation cycle.", + "Alias": "Alias", + "Image": "Image", + "New Version": "New Version", + "Apps Updated": "Apps Updated", + "Updated At": "Updated At", "Server": "Server", "Deny": "Deny", "Allow": "Allow", @@ -124,7 +171,6 @@ "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.", @@ -156,19 +202,15 @@ "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.", "There are no Argo CD App Projects in all projects.": "There are no Argo CD App Projects in all projects.", "No matching Argo CD App Projects": "No matching Argo CD App Projects", "No Argo CD App Projects": "No Argo CD App Projects", - "Unable to load data": "Unable to load data", "There was an error retrieving App Projects. Check your connection and reload the page.": "There was an error retrieving App Projects. Check your connection and reload the page.", "AppProjects": "AppProjects", - "This list page is under tech preview, but not necessarily the resources it represents": "This list page is under tech preview, but not necessarily the resources it represents", "Create AppProject": "Create AppProject", - "Search by name...": "Search by name...", "Labels": "Labels", "Last Updated": "Last Updated", "Has Description": "Has Description", @@ -205,7 +247,6 @@ "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", "Owner": "Owner", "Memory": "Memory", diff --git a/locales/ko/plugin__gitops-plugin.json b/locales/ko/plugin__gitops-plugin.json index 42aec3dd9..9b2a17804 100644 --- a/locales/ko/plugin__gitops-plugin.json +++ b/locales/ko/plugin__gitops-plugin.json @@ -105,6 +105,53 @@ "Progressive Sync Flow View": "Progressive Sync Flow View", "Expand or collapse all progressive sync step groups": "Expand or collapse all progressive sync step groups", "No Applications In This Step": "No Applications In This Step", + "Edit ImageUpdater": "Edit ImageUpdater", + "Delete ImageUpdater": "Delete ImageUpdater", + "Error: Missing required route parameters": "Error: Missing required route parameters", + "ImageUpdater details": "ImageUpdater details", + "Ready": "Ready", + "Whether the last reconciliation completed without errors.": "Whether the last reconciliation completed without errors.", + "True": "True", + "False": "False", + "Applications Matched": "Applications Matched", + "Number of applications matched by this ImageUpdater.": "Number of applications matched by this ImageUpdater.", + "Images Managed": "Images Managed", + "Number of images eligible for update checking.": "Number of images eligible for update checking.", + "Last Checked At": "Last Checked At", + "When the controller last checked for image updates.": "When the controller last checked for image updates.", + "Last Updated At": "Last Updated At", + "When the controller last performed an image update.": "When the controller last performed an image update.", + "Observed Generation": "Observed Generation", + "The generation of the resource that was last reconciled.": "The generation of the resource that was last reconciled.", + "Conditions": "Conditions", + "No ImageUpdaters match the search filter": "No ImageUpdaters match the search filter", + "Try removing the filter or searching for a different term to see more ImageUpdaters.": "Try removing the filter or searching for a different term to see more ImageUpdaters.", + "There are no ImageUpdaters in this namespace.": "There are no ImageUpdaters in this namespace.", + "There are no ImageUpdaters in all namespaces.": "There are no ImageUpdaters in all namespaces.", + "No matching ImageUpdaters": "No matching ImageUpdaters", + "No ImageUpdaters": "No ImageUpdaters", + "Unable to load data": "Unable to load data", + "There was an error retrieving ImageUpdaters. Check your connection and reload the page.": "There was an error retrieving ImageUpdaters. Check your connection and reload the page.", + "ImageUpdaters": "ImageUpdaters", + "This list page is under tech preview, but not necessarily the resources it represents": "This list page is under tech preview, but not necessarily the resources it represents", + "Create ImageUpdater": "Create ImageUpdater", + "Search by name...": "Search by name...", + "Apps": "Apps", + "Images": "Images", + "Last Checked": "Last Checked", + "Has Apps": "Has Apps", + "No Apps": "No Apps", + "Not Ready": "Not Ready", + "Recent Updates": "Recent Updates", + "ArgoCD ImageUpdater": "ArgoCD ImageUpdater", + "There was an error retrieving the ImageUpdater. Check your connection and reload the page.": "There was an error retrieving the ImageUpdater. Check your connection and reload the page.", + "No recent updates": "No recent updates", + "No image updates have been recorded in the most recent reconciliation cycle.": "No image updates have been recorded in the most recent reconciliation cycle.", + "Alias": "Alias", + "Image": "Image", + "New Version": "New Version", + "Apps Updated": "Apps Updated", + "Updated At": "Updated At", "Server": "Server", "Deny": "Deny", "Allow": "Allow", @@ -124,7 +171,6 @@ "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.", @@ -156,19 +202,15 @@ "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.", "There are no Argo CD App Projects in all projects.": "There are no Argo CD App Projects in all projects.", "No matching Argo CD App Projects": "No matching Argo CD App Projects", "No Argo CD App Projects": "No Argo CD App Projects", - "Unable to load data": "Unable to load data", "There was an error retrieving App Projects. Check your connection and reload the page.": "There was an error retrieving App Projects. Check your connection and reload the page.", "AppProjects": "AppProjects", - "This list page is under tech preview, but not necessarily the resources it represents": "This list page is under tech preview, but not necessarily the resources it represents", "Create AppProject": "Create AppProject", - "Search by name...": "Search by name...", "Labels": "Labels", "Last Updated": "Last Updated", "Has Description": "Has Description", @@ -205,7 +247,6 @@ "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", "Owner": "Owner", "Memory": "Memory", diff --git a/locales/zh/plugin__gitops-plugin.json b/locales/zh/plugin__gitops-plugin.json index 4858cce3e..873866551 100644 --- a/locales/zh/plugin__gitops-plugin.json +++ b/locales/zh/plugin__gitops-plugin.json @@ -105,6 +105,53 @@ "Progressive Sync Flow View": "Progressive Sync Flow View", "Expand or collapse all progressive sync step groups": "Expand or collapse all progressive sync step groups", "No Applications In This Step": "No Applications In This Step", + "Edit ImageUpdater": "Edit ImageUpdater", + "Delete ImageUpdater": "Delete ImageUpdater", + "Error: Missing required route parameters": "Error: Missing required route parameters", + "ImageUpdater details": "ImageUpdater details", + "Ready": "Ready", + "Whether the last reconciliation completed without errors.": "Whether the last reconciliation completed without errors.", + "True": "True", + "False": "False", + "Applications Matched": "Applications Matched", + "Number of applications matched by this ImageUpdater.": "Number of applications matched by this ImageUpdater.", + "Images Managed": "Images Managed", + "Number of images eligible for update checking.": "Number of images eligible for update checking.", + "Last Checked At": "Last Checked At", + "When the controller last checked for image updates.": "When the controller last checked for image updates.", + "Last Updated At": "Last Updated At", + "When the controller last performed an image update.": "When the controller last performed an image update.", + "Observed Generation": "Observed Generation", + "The generation of the resource that was last reconciled.": "The generation of the resource that was last reconciled.", + "Conditions": "Conditions", + "No ImageUpdaters match the search filter": "No ImageUpdaters match the search filter", + "Try removing the filter or searching for a different term to see more ImageUpdaters.": "Try removing the filter or searching for a different term to see more ImageUpdaters.", + "There are no ImageUpdaters in this namespace.": "There are no ImageUpdaters in this namespace.", + "There are no ImageUpdaters in all namespaces.": "There are no ImageUpdaters in all namespaces.", + "No matching ImageUpdaters": "No matching ImageUpdaters", + "No ImageUpdaters": "No ImageUpdaters", + "Unable to load data": "Unable to load data", + "There was an error retrieving ImageUpdaters. Check your connection and reload the page.": "There was an error retrieving ImageUpdaters. Check your connection and reload the page.", + "ImageUpdaters": "ImageUpdaters", + "This list page is under tech preview, but not necessarily the resources it represents": "This list page is under tech preview, but not necessarily the resources it represents", + "Create ImageUpdater": "Create ImageUpdater", + "Search by name...": "Search by name...", + "Apps": "Apps", + "Images": "Images", + "Last Checked": "Last Checked", + "Has Apps": "Has Apps", + "No Apps": "No Apps", + "Not Ready": "Not Ready", + "Recent Updates": "Recent Updates", + "ArgoCD ImageUpdater": "ArgoCD ImageUpdater", + "There was an error retrieving the ImageUpdater. Check your connection and reload the page.": "There was an error retrieving the ImageUpdater. Check your connection and reload the page.", + "No recent updates": "No recent updates", + "No image updates have been recorded in the most recent reconciliation cycle.": "No image updates have been recorded in the most recent reconciliation cycle.", + "Alias": "Alias", + "Image": "Image", + "New Version": "New Version", + "Apps Updated": "Apps Updated", + "Updated At": "Updated At", "Server": "Server", "Deny": "Deny", "Allow": "Allow", @@ -124,7 +171,6 @@ "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.", @@ -156,19 +202,15 @@ "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.", "There are no Argo CD App Projects in all projects.": "There are no Argo CD App Projects in all projects.", "No matching Argo CD App Projects": "No matching Argo CD App Projects", "No Argo CD App Projects": "No Argo CD App Projects", - "Unable to load data": "Unable to load data", "There was an error retrieving App Projects. Check your connection and reload the page.": "There was an error retrieving App Projects. Check your connection and reload the page.", "AppProjects": "AppProjects", - "This list page is under tech preview, but not necessarily the resources it represents": "This list page is under tech preview, but not necessarily the resources it represents", "Create AppProject": "Create AppProject", - "Search by name...": "Search by name...", "Labels": "Labels", "Last Updated": "Last Updated", "Has Description": "Has Description", @@ -205,7 +247,6 @@ "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", "Owner": "Owner", "Memory": "Memory", diff --git a/plugin-metadata.ts b/plugin-metadata.ts index 2700cf932..a89296f45 100644 --- a/plugin-metadata.ts +++ b/plugin-metadata.ts @@ -19,6 +19,8 @@ const metadata: ConsolePluginBuildMetadata = { ApplicationSetDetailsPage: "./gitops/components/appset/ApplicationSetDetailsPage.tsx", ProjectList: "./gitops/components/project/ProjectListTab.tsx", ProjectDetailsPage: "./gitops/components/project/ProjectDetailsPage.tsx", + ImageUpdaterList: "./gitops/components/imageupdater/ImageUpdaterListTab.tsx", + ImageUpdaterDetailsPage: "./gitops/components/imageupdater/ImageUpdaterDetailsPage.tsx", yamlTemplates: "./gitops/templates/index.ts" } }; diff --git a/src/gitops/components/imageupdater/ImageUpdaterDetailsPage.tsx b/src/gitops/components/imageupdater/ImageUpdaterDetailsPage.tsx new file mode 100644 index 000000000..add077f65 --- /dev/null +++ b/src/gitops/components/imageupdater/ImageUpdaterDetailsPage.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 ImageUpdaterNavPage from './ImageUpdaterNavPage'; + +const ImageUpdaterDetailsPage: 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 ImageUpdaterDetailsPage; diff --git a/src/gitops/components/imageupdater/ImageUpdaterDetailsTab.tsx b/src/gitops/components/imageupdater/ImageUpdaterDetailsTab.tsx new file mode 100644 index 000000000..03472a5d7 --- /dev/null +++ b/src/gitops/components/imageupdater/ImageUpdaterDetailsTab.tsx @@ -0,0 +1,126 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; +import classNames from 'classnames'; + +import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { + DescriptionList, + Flex, + FlexItem, + PageSection, + PageSectionVariants, + Title, +} from '@patternfly/react-core'; + +import { ImageUpdaterKind, ImageUpdaterModel } from '../../models/ImageUpdaterModel'; +import { Conditions } from '../../utils/components/Conditions/Conditions'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import BaseDetailsSummary, { + DetailsDescriptionGroup, +} from '../shared/BaseDetailsSummary/BaseDetailsSummary'; + +type ImageUpdaterDetailsTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: ImageUpdaterKind; +}; + +const ImageUpdaterDetailsTab: React.FC = ({ obj }) => { + const { t } = useGitOpsTranslation(); + + if (!obj) return null; + + const status = obj.status || {}; + + return ( + <> + + + {t('ImageUpdater details')} + + + + + + + + + + + + + {(() => { + const readyCondition = status.conditions?.find((c) => c.type === 'Ready'); + if (!readyCondition) return '-'; + return readyCondition.status === 'True' ? t('True') : t('False'); + })()} + + + + {status.applicationsMatched != null ? String(status.applicationsMatched) : '-'} + + + + {status.imagesManaged != null ? String(status.imagesManaged) : '-'} + + + + {status.lastCheckedAt ? ( + + ) : ( + '-' + )} + + + + {status.lastUpdatedAt ? ( + + ) : ( + '-' + )} + + + + {status.observedGeneration != null ? String(status.observedGeneration) : '-'} + + + + + + + + {status.conditions && status.conditions.length > 0 && ( + + + {t('Conditions')} + + + + )} + + ); +}; + +export default ImageUpdaterDetailsTab; diff --git a/src/gitops/components/imageupdater/ImageUpdaterList.tsx b/src/gitops/components/imageupdater/ImageUpdaterList.tsx new file mode 100644 index 000000000..b8d59b4ca --- /dev/null +++ b/src/gitops/components/imageupdater/ImageUpdaterList.tsx @@ -0,0 +1,476 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom-v5-compat'; +import TechPreviewBadge from 'src/plugin/import/badges/TechPreviewBadge'; + +import ActionsDropdown from '@gitops/utils/components/ActionDropDown/ActionDropDown'; +import { modelToGroupVersionKind } from '@gitops/utils/utils'; +import { + Action, + K8sResourceCommon, + ListPageBody, + ListPageCreate, + ListPageFilter, + ListPageHeader, + RowFilter, + Timestamp, + useK8sWatchResource, + useListPageFilter, +} from '@openshift-console/dynamic-plugin-sdk'; +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { ErrorState } from '@patternfly/react-component-groups'; +import { EmptyState, EmptyStateBody } 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 { ImageUpdaterKind, ImageUpdaterModel, imageUpdaterModelRef } from '../../models/ImageUpdaterModel'; +import { + ShowOperandsInAllNamespacesRadioGroup, + useShowOperandsInAllNamespaces, +} from '../shared/AllNamespaces'; +import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; + +import { useImageUpdaterActionsProvider } from './hooks/useImageUpdaterActionsProvider'; + +import './imageupdater-list.scss'; + +type ImageUpdaterListTabProps = { + namespace?: string; + hideNameLabelFilters?: boolean; + showTitle?: boolean; +}; + +const ImageUpdaterList: React.FC = ({ + namespace, + hideNameLabelFilters, + showTitle, +}) => { + const location = useLocation(); + const [showOperandsInAllNamespaces] = useShowOperandsInAllNamespaces(); + const listAllNamespaces = + location.pathname?.includes('openshift-gitops-operator') && showOperandsInAllNamespaces; + if (listAllNamespaces) { + namespace = null; + } + const [imageUpdaters, loaded, loadError] = useK8sWatchResource({ + isList: true, + groupVersionKind: { + group: 'argocd-image-updater.argoproj.io', + kind: 'ImageUpdater', + version: 'v1alpha1', + }, + namespaced: !listAllNamespaces, + namespace, + }); + + const columnSortConfig = React.useMemo(() => { + return [ + 'name', + ...(!listAllNamespaces || !namespace || namespace === '' ? ['namespace'] : []), + 'apps', + 'images', + 'last-checked', + 'ready', + 'actions', + ].map((key) => ({ key })); + }, [listAllNamespaces, namespace]); + + const { searchParams, sortBy, direction, getSortParams } = + useGitOpsDataViewSort(columnSortConfig); + + // Get search query from URL parameters + const searchQuery = searchParams.get('q') || ''; + + const { t } = useTranslation('plugin__gitops-plugin'); + + const columnsDV = useColumnsDV(namespace, getSortParams); + const sortedItems = React.useMemo(() => { + return sortData(imageUpdaters as ImageUpdaterKind[], sortBy, direction); + }, [imageUpdaters, sortBy, direction]); + + const filters = getFilters(t); + const [data, filteredData, onFilterChange] = useListPageFilter(sortedItems, filters); + + const filteredBySearch = React.useMemo(() => { + if (!searchQuery) return filteredData; + + return filteredData.filter((item) => { + const name = item.metadata?.name || ''; + return name.toLowerCase().includes(searchQuery.toLowerCase()); + }); + }, [filteredData, searchQuery]); + + const rows = useImageUpdaterRowsDV(filteredBySearch as ImageUpdaterKind[], namespace); + + const hasItems = React.useMemo(() => { + return sortedItems.length > 0; + }, [sortedItems]); + + const getEmptyStateBody = () => { + if (searchQuery) { + return ( + <> + {t('No ImageUpdaters match the search filter')}{' '} + "{searchQuery}". +
+ {t('Try removing the filter or searching for a different term to see more ImageUpdaters.')} + + ); + } + return namespace + ? t('There are no ImageUpdaters in this namespace.') + : t('There are no ImageUpdaters in all namespaces.'); + }; + + const empty = ( + + + + + {getEmptyStateBody()} + + + + + ); + const error = loadError && ( + + + + + + + + ); + const isEmptyState = !loadError && rows.length === 0; + + return ( +
+ {showTitle == undefined && ( + + ) + } + helpText={ + location.pathname?.includes('openshift-gitops-operator') ? ( + + ) : null + } + hideFavoriteButton={false} + > + + {t('Create ImageUpdater')} + + + )} + + {!hideNameLabelFilters && hasItems && ( + + )} + + +
+ ); +}; + +export const sortData = ( + data: ImageUpdaterKind[], + sortBy: string | undefined, + direction: 'asc' | 'desc' | undefined, +) => { + if (!(sortBy && direction)) return data || []; + if (!data) return []; + + return [...data].sort((a, b) => { + let aValue: any, bValue: any; + + switch (sortBy) { + case 'name': + aValue = a.metadata?.name || ''; + bValue = b.metadata?.name || ''; + break; + case 'namespace': + aValue = a.metadata?.namespace || ''; + bValue = b.metadata?.namespace || ''; + break; + case 'apps': + aValue = a.status?.applicationsMatched ?? -1; + bValue = b.status?.applicationsMatched ?? -1; + break; + case 'images': + aValue = a.status?.imagesManaged ?? -1; + bValue = b.status?.imagesManaged ?? -1; + break; + case 'last-checked': + aValue = a.status?.lastCheckedAt || ''; + bValue = b.status?.lastCheckedAt || ''; + break; + case 'ready': + aValue = a.status?.conditions?.find((c) => c.type === 'Ready')?.status || ''; + bValue = b.status?.conditions?.find((c) => c.type === 'Ready')?.status || ''; + break; + default: + return 0; + } + + if (direction === 'asc') { + // eslint-disable-next-line no-nested-ternary + return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; + } else { + // eslint-disable-next-line no-nested-ternary + return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; + } + }); +}; + +export const useColumnsDV = ( + namespace: string | undefined, + getSortParams: (columnIndex: number) => ThProps['sort'], +): DataViewTh[] => { + const showNamespace = !namespace || namespace === ''; + const i: number = showNamespace ? 1 : 0; + const { t } = useTranslation('plugin__gitops-plugin'); + const columns: DataViewTh[] = [ + { + cell: t('Name'), + props: { + 'aria-label': 'name', + className: 'pf-m-width-20', + sort: getSortParams(0), + style: { minWidth: '200px' }, + }, + }, + ...(showNamespace + ? [ + { + cell: t('Namespace'), + props: { + 'aria-label': 'namespace', + className: 'pf-m-width-15', + sort: getSortParams(1), + style: { minWidth: '150px' }, + }, + }, + ] + : []), + { + cell: t('Apps'), + props: { + 'aria-label': 'apps', + className: 'pf-m-width-10', + sort: getSortParams(1 + i), + }, + }, + { + cell: t('Images'), + props: { + 'aria-label': 'images', + className: 'pf-m-width-10', + sort: getSortParams(2 + i), + }, + }, + { + cell: t('Last Checked'), + props: { + 'aria-label': 'last checked', + className: 'pf-m-width-15', + sort: getSortParams(3 + i), + }, + }, + { + cell: t('Ready'), + props: { + 'aria-label': 'ready', + className: 'pf-m-width-10', + sort: getSortParams(4 + i), + }, + }, + { + cell: '', + props: { 'aria-label': 'actions' }, + }, + ]; + + return columns; +}; + +export const useImageUpdaterRowsDV = ( + imageUpdaterList: ImageUpdaterKind[], + namespace: string | undefined, +): DataViewTr[] => { + const rows: DataViewTr[] = []; + if (imageUpdaterList == undefined || imageUpdaterList.length == 0) { + return rows; + } + const showNamespace = !namespace || namespace === ''; + imageUpdaterList.forEach((obj, index) => { + const readyCondition = obj.status?.conditions?.find((c) => c.type === 'Ready'); + const isReady = readyCondition?.status === 'True'; + + rows.push([ + { + cell: ( +
+ +
+ ), + id: 'name', + dataLabel: 'Name', + }, + ...(showNamespace + ? [ + { + cell: , + id: obj.metadata.namespace, + dataLabel: 'Namespace', + }, + ] + : []), + { + id: 'apps', + cell: obj.status?.applicationsMatched != null ? String(obj.status.applicationsMatched) : '-', + dataLabel: 'Apps', + }, + { + id: 'images', + cell: obj.status?.imagesManaged != null ? String(obj.status.imagesManaged) : '-', + dataLabel: 'Images', + }, + { + id: 'last-checked', + cell: obj.status?.lastCheckedAt ? ( +
+ +
+ ) : ( + '-' + ), + dataLabel: 'Last Checked', + }, + { + id: 'ready', + cell: readyCondition ? String(isReady) : '-', + dataLabel: 'Ready', + }, + { + id: 'actions-' + index, + cell: , + props: { style: { paddingTop: 8, paddingRight: 0, paddingLeft: 0, width: 10 } }, + }, + ]); + }); + return rows; +}; + +const ImageUpdaterActionsCell: React.FC<{ + imageUpdater: ImageUpdaterKind; +}> = ({ imageUpdater }) => { + const actionList: Action[] = useImageUpdaterActionsProvider(imageUpdater); + return ( +
+ +
+ ); +}; + +const getFilters = (t: (key: string) => string): RowFilter[] => [ + { + filterGroupName: t('Apps'), + type: 'apps-status', + reducer: (item) => { + const apps = (item as ImageUpdaterKind).status?.applicationsMatched; + return apps > 0 ? 'has-apps' : 'no-apps'; + }, + filter: (input, item) => { + if (input.selected?.length) { + const apps = (item as ImageUpdaterKind).status?.applicationsMatched; + const hasApps = apps > 0; + if (input.selected.includes('has-apps')) { + return hasApps; + } + if (input.selected.includes('no-apps')) { + return !hasApps; + } + } + return true; + }, + items: [ + { id: 'has-apps', title: t('Has Apps') }, + { id: 'no-apps', title: t('No Apps') }, + ], + }, + { + filterGroupName: t('Ready'), + type: 'ready-status', + reducer: (item) => { + const readyCondition = (item as ImageUpdaterKind).status?.conditions?.find( + (c) => c.type === 'Ready', + ); + return readyCondition?.status === 'True' ? 'ready' : 'not-ready'; + }, + filter: (input, item) => { + if (input.selected?.length) { + const readyCondition = (item as ImageUpdaterKind).status?.conditions?.find( + (c) => c.type === 'Ready', + ); + const isReady = readyCondition?.status === 'True'; + if (input.selected.includes('ready')) { + return isReady; + } + if (input.selected.includes('not-ready')) { + return !isReady; + } + } + return true; + }, + items: [ + { id: 'ready', title: t('Ready') }, + { id: 'not-ready', title: t('Not Ready') }, + ], + }, +]; + +export default ImageUpdaterList; diff --git a/src/gitops/components/imageupdater/ImageUpdaterListTab.tsx b/src/gitops/components/imageupdater/ImageUpdaterListTab.tsx new file mode 100644 index 000000000..090ae4d2e --- /dev/null +++ b/src/gitops/components/imageupdater/ImageUpdaterListTab.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import ImageUpdaterList from './ImageUpdaterList'; + +type ImageUpdaterListTabProps = { + namespace?: string; + hideNameLabelFilters?: boolean; + showTitle?: boolean; +}; + +const ImageUpdaterListTab: React.FC = ({ + namespace, + hideNameLabelFilters, + showTitle, +}) => { + return ( + + ); +}; + +export default ImageUpdaterListTab; diff --git a/src/gitops/components/imageupdater/ImageUpdaterNavPage.tsx b/src/gitops/components/imageupdater/ImageUpdaterNavPage.tsx new file mode 100644 index 000000000..8d3b49b12 --- /dev/null +++ b/src/gitops/components/imageupdater/ImageUpdaterNavPage.tsx @@ -0,0 +1,90 @@ +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 { ImageUpdaterKind, ImageUpdaterModel } from '../../models/ImageUpdaterModel'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import DetailsPageHeader from '../shared/DetailsPageHeader/DetailsPageHeader'; +import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; + +import { useImageUpdaterActionsProvider } from './hooks/useImageUpdaterActionsProvider'; +import ImageUpdaterDetailsTab from "@gitops/components/imageupdater/ImageUpdaterDetailsTab"; +import ImageUpdaterRecentUpdatesTab from "@gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab"; + +type ImageUpdaterPageProps = { + name: string; + namespace: string; + kind: string; +}; + +const ImageUpdaterNavPage: React.FC = ({ name, namespace, kind }) => { + const { t } = useGitOpsTranslation(); + const [imageUpdater, loaded, loadError] = useK8sWatchResource({ + groupVersionKind: { + group: 'argocd-image-updater.argoproj.io', + kind: 'ImageUpdater', + version: 'v1alpha1', + }, + kind, + name, + namespace, + }); + + const actions = useImageUpdaterActionsProvider(imageUpdater); + + const pages = React.useMemo( + () => [ + { + href: '', + name: t('Details'), + component: ImageUpdaterDetailsTab, + }, + { + href: 'recent-updates', + name: t('Recent Updates'), + component: ImageUpdaterRecentUpdatesTab, + }, + { + href: 'yaml', + name: t('YAML'), + component: ResourceYAMLTab, + }, + ], + [t], + ); + + return ( + <> + + {/* eslint-disable-next-line no-nested-ternary */} + {loaded && !loadError ? ( +
+ +
+ ) : loadError ? ( + + ) : ( + + + + )} + + ); +}; + +export default ImageUpdaterNavPage; diff --git a/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx b/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx new file mode 100644 index 000000000..3488e4d18 --- /dev/null +++ b/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx @@ -0,0 +1,198 @@ +import * as React from 'react'; +import { RouteComponentProps } from 'react-router'; +import { useTranslation } from 'react-i18next'; + +import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { EmptyState, EmptyStateBody, PageSection, PageSectionVariants, Title } from '@patternfly/react-core'; +import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/DataViewTable'; +import { CubesIcon } from '@patternfly/react-icons'; +import { Tbody, Td, ThProps, Tr } from '@patternfly/react-table'; + +import { ImageUpdaterKind, ImageUpdaterRecentUpdate } from '../../models/ImageUpdaterModel'; +import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; +import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; + +type ImageUpdaterRecentUpdatesTabProps = RouteComponentProps<{ ns: string; name: string }> & { + obj?: ImageUpdaterKind; +}; + +const ImageUpdaterRecentUpdatesTab: React.FC = ({ obj }) => { + const { t } = useGitOpsTranslation(); + + const recentUpdates: ImageUpdaterRecentUpdate[] = obj?.status?.recentUpdates || []; + + const columnSortConfig = React.useMemo( + () => + ['alias', 'image', 'new-version', 'apps-updated', 'updated-at', 'message'].map((key) => ({ + key, + })), + [], + ); + + const { sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); + + const columnsDV = useColumnsDV(getSortParams); + + const sortedUpdates = React.useMemo( + () => sortData(recentUpdates, sortBy, direction), + [recentUpdates, sortBy, direction], + ); + + const rows = useRowsDV(sortedUpdates); + + const empty = ( + + + + + + {t('No image updates have been recorded in the most recent reconciliation cycle.')} + + + + + + ); + + if (!obj) return null; + + return ( +
+ + + {t('Recent Updates')} + + + +
+ ); +}; + +const useRowsDV = (updates: ImageUpdaterRecentUpdate[]): DataViewTr[] => { + const rows: DataViewTr[] = []; + + updates.forEach((update) => { + rows.push([ + { + cell: update.alias || '-', + id: 'alias', + dataLabel: 'Alias', + }, + { + cell: update.image || '-', + id: 'image', + dataLabel: 'Image', + }, + { + cell: update.newVersion || '-', + id: 'new-version', + dataLabel: 'New Version', + }, + { + cell: update.applicationsUpdated != null ? String(update.applicationsUpdated) : '-', + id: 'apps-updated', + dataLabel: 'Apps Updated', + }, + { + cell: update.updatedAt ? : '-', + id: 'updated-at', + dataLabel: 'Updated At', + }, + { + cell: update.message || '-', + id: 'message', + dataLabel: 'Message', + }, + ]); + }); + + return rows; +}; + +const useColumnsDV = (getSortParams: (columnIndex: number) => ThProps['sort']): DataViewTh[] => { + const { t } = useTranslation('plugin__gitops-plugin'); + + return [ + { + cell: t('Alias'), + props: { 'aria-label': 'alias', sort: getSortParams(0) }, + }, + { + cell: t('Image'), + props: { 'aria-label': 'image', className: 'pf-m-width-20', sort: getSortParams(1) }, + }, + { + cell: t('New Version'), + props: { 'aria-label': 'new version', sort: getSortParams(2) }, + }, + { + cell: t('Apps Updated'), + props: { 'aria-label': 'apps updated', sort: getSortParams(3) }, + }, + { + cell: t('Updated At'), + props: { 'aria-label': 'updated at', className: 'pf-m-width-15', sort: getSortParams(4) }, + }, + { + cell: t('Message'), + props: { 'aria-label': 'message', className: 'pf-m-width-30', sort: getSortParams(5) }, + }, + ]; +}; + +const sortData = ( + data: ImageUpdaterRecentUpdate[], + sortBy: string | undefined, + direction: 'asc' | 'desc' | undefined, +): ImageUpdaterRecentUpdate[] => { + if (!sortBy || !direction) return data; + + return [...data].sort((a, b) => { + let aValue: any, bValue: any; + + switch (sortBy) { + case 'alias': + aValue = a.alias || ''; + bValue = b.alias || ''; + break; + case 'image': + aValue = a.image || ''; + bValue = b.image || ''; + break; + case 'new-version': + aValue = a.newVersion || ''; + bValue = b.newVersion || ''; + break; + case 'apps-updated': + aValue = a.applicationsUpdated ?? -1; + bValue = b.applicationsUpdated ?? -1; + break; + case 'updated-at': + aValue = a.updatedAt || ''; + bValue = b.updatedAt || ''; + break; + case 'message': + aValue = a.message || ''; + bValue = b.message || ''; + break; + default: + return 0; + } + + if (direction === 'asc') { + return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; + } else { + return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; + } + }); +}; + +export default ImageUpdaterRecentUpdatesTab; diff --git a/src/gitops/components/imageupdater/hooks/useImageUpdaterActionsProvider.tsx b/src/gitops/components/imageupdater/hooks/useImageUpdaterActionsProvider.tsx new file mode 100644 index 000000000..914e11f36 --- /dev/null +++ b/src/gitops/components/imageupdater/hooks/useImageUpdaterActionsProvider.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { + Action, + K8sVerb, + useAnnotationsModal, + useDeleteModal, + useLabelsModal, +} from '@openshift-console/dynamic-plugin-sdk'; + +import { + ImageUpdaterKind, + ImageUpdaterModel, + imageUpdaterModelRef, +} from '../../../models/ImageUpdaterModel'; +import { useGitOpsTranslation } from '../../../utils/hooks/useGitOpsTranslation'; + +type UseImageUpdaterActionsProvider = (imageUpdater: ImageUpdaterKind) => Action[]; + +export const useImageUpdaterActionsProvider: UseImageUpdaterActionsProvider = (imageUpdater) => { + const { t } = useGitOpsTranslation(); + const navigate = useNavigate(); + const launchLabelsModal = useLabelsModal(imageUpdater); + const launchAnnotationsModal = useAnnotationsModal(imageUpdater); + const launchDeleteModal = useDeleteModal(imageUpdater); + + const actions = React.useMemo( + () => [ + { + id: 'gitops-action-edit-labels-imageupdater', + disabled: false, + label: t('Edit labels'), + accessReview: { + group: ImageUpdaterModel.apiGroup, + verb: 'patch' as K8sVerb, + resource: ImageUpdaterModel.plural, + namespace: imageUpdater?.metadata?.namespace, + }, + cta: () => { + launchLabelsModal(); + }, + }, + { + id: 'gitops-action-edit-annotations-imageupdater', + disabled: false, + label: t('Edit annotations'), + accessReview: { + group: ImageUpdaterModel.apiGroup, + verb: 'patch' as K8sVerb, + resource: ImageUpdaterModel.plural, + namespace: imageUpdater?.metadata?.namespace, + }, + cta: () => { + launchAnnotationsModal(); + }, + }, + { + id: 'gitops-action-edit-imageupdater', + disabled: !imageUpdater?.metadata?.namespace || !imageUpdater?.metadata?.name, + label: t('Edit ImageUpdater'), + accessReview: { + group: ImageUpdaterModel.apiGroup, + verb: 'update' as K8sVerb, + resource: ImageUpdaterModel.plural, + namespace: imageUpdater?.metadata?.namespace, + }, + cta: () => { + if (!imageUpdater?.metadata?.namespace || !imageUpdater?.metadata?.name) { + return; + } + navigate( + `/k8s/ns/${imageUpdater?.metadata?.namespace}/${imageUpdaterModelRef}/${imageUpdater?.metadata?.name}/yaml`, + ); + }, + }, + { + id: 'gitops-action-delete-imageupdater', + disabled: false, + label: t('Delete ImageUpdater'), + accessReview: { + group: ImageUpdaterModel.apiGroup, + verb: 'delete' as K8sVerb, + resource: ImageUpdaterModel.plural, + namespace: imageUpdater?.metadata?.namespace, + }, + cta: () => launchDeleteModal(), + }, + ], + [imageUpdater, launchLabelsModal, launchAnnotationsModal, launchDeleteModal, navigate, t], + ); + + return actions; +}; diff --git a/src/gitops/components/imageupdater/imageupdater-list.scss b/src/gitops/components/imageupdater/imageupdater-list.scss new file mode 100644 index 000000000..54c8b2278 --- /dev/null +++ b/src/gitops/components/imageupdater/imageupdater-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/models/ImageUpdaterModel.ts b/src/gitops/models/ImageUpdaterModel.ts new file mode 100644 index 000000000..981e88012 --- /dev/null +++ b/src/gitops/models/ImageUpdaterModel.ts @@ -0,0 +1,49 @@ +import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; +import { K8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/api/common-types'; + +import { modelToRef } from 'src/gitops/utils/utils'; + +export type ImageUpdaterCondition = { + lastTransitionTime: string; + message: string; + observedGeneration?: number; + reason: string; + status: string; + type: string; +}; + +export type ImageUpdaterRecentUpdate = { + alias: string; + applicationsUpdated: number; + image: string; + message: string; + newVersion: string; + updatedAt: string; +}; + +export type ImageUpdaterKind = K8sResourceCommon & { + status?: { + applicationsMatched?: number; + imagesManaged?: number; + lastCheckedAt?: string; + lastUpdatedAt?: string; + observedGeneration?: number; + conditions?: ImageUpdaterCondition[]; + recentUpdates?: ImageUpdaterRecentUpdate[]; + }; +}; + +export const ImageUpdaterModel: K8sModel = { + label: 'ImageUpdater', + labelPlural: 'ImageUpdaters', + apiVersion: 'v1alpha1', + apiGroup: 'argocd-image-updater.argoproj.io', + plural: 'imageupdaters', + abbr: 'imageupdater', + namespaced: true, + kind: 'ImageUpdater', + id: 'imageupdater', + crd: true, +}; + +export const imageUpdaterModelRef = modelToRef(ImageUpdaterModel); diff --git a/src/gitops/templates/imageupdater-yaml.ts b/src/gitops/templates/imageupdater-yaml.ts new file mode 100644 index 000000000..a4ffe628d --- /dev/null +++ b/src/gitops/templates/imageupdater-yaml.ts @@ -0,0 +1,12 @@ +export const defaultImageUpdaterYamlTemplate = ` +apiVersion: argocd-image-updater.argoproj.io/v1alpha1 +kind: ImageUpdater +metadata: + name: my-image-updater +spec: + applicationRefs: + - namePattern: "app-001" + images: + - alias: "test" + imageName: "test:1.2.3" +`; diff --git a/src/gitops/templates/index.ts b/src/gitops/templates/index.ts index 5cd825806..83c8aebca 100644 --- a/src/gitops/templates/index.ts +++ b/src/gitops/templates/index.ts @@ -1,4 +1,5 @@ export * from './application-yaml'; export * from './applicationset-yaml'; export * from './appproject-yaml'; +export * from './imageupdater-yaml'; export * from './rollout-yaml'; From 2fc86de24484fb8e96d9f130fe195d3311d25e7a Mon Sep 17 00:00:00 2001 From: dkarpele Date: Wed, 27 May 2026 21:25:00 +0200 Subject: [PATCH 2/4] fix(image-updater): address code review comments Signed-off-by: dkarpele --- console-extensions.json | 10 ++ .../imageupdater/ImageUpdaterDetailsTab.tsx | 16 +- .../imageupdater/ImageUpdaterList.tsx | 164 ++++++++---------- .../imageupdater/ImageUpdaterNavPage.tsx | 12 +- .../ImageUpdaterRecentUpdatesTab.tsx | 20 +-- .../imageupdater/imageupdater-list.scss | 15 ++ src/gitops/models/ImageUpdaterModel.ts | 2 +- 7 files changed, 122 insertions(+), 117 deletions(-) diff --git a/console-extensions.json b/console-extensions.json index 41522f4f6..6ea199247 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -380,6 +380,11 @@ "kind": "ImageUpdater", "version": "v1alpha1" } + }, + "flags": { + "required": [ + "IMAGEUPDATER" + ] } }, { @@ -389,6 +394,11 @@ "section": "gitops-navigation-section", "id": "argocd-separator", "insertAfter": "imageupdater" + }, + "flags": { + "required": [ + "IMAGEUPDATER" + ] } }, { diff --git a/src/gitops/components/imageupdater/ImageUpdaterDetailsTab.tsx b/src/gitops/components/imageupdater/ImageUpdaterDetailsTab.tsx index 03472a5d7..e0fc6fb80 100644 --- a/src/gitops/components/imageupdater/ImageUpdaterDetailsTab.tsx +++ b/src/gitops/components/imageupdater/ImageUpdaterDetailsTab.tsx @@ -29,6 +29,10 @@ const ImageUpdaterDetailsTab: React.FC = ({ obj }) if (!obj) return null; const status = obj.status || {}; + const readyCondition = status.conditions?.find((c) => c.type === 'Ready'); + const readyLabel = readyCondition + ? readyCondition.status === 'True' ? t('True') : t('False') + : '-'; return ( <> @@ -56,25 +60,21 @@ const ImageUpdaterDetailsTab: React.FC = ({ obj }) title={t('Ready')} help={t('Whether the last reconciliation completed without errors.')} > - {(() => { - const readyCondition = status.conditions?.find((c) => c.type === 'Ready'); - if (!readyCondition) return '-'; - return readyCondition.status === 'True' ? t('True') : t('False'); - })()} + {readyLabel} - {status.applicationsMatched != null ? String(status.applicationsMatched) : '-'} + {String(status.applicationsMatched ?? '-')} - {status.imagesManaged != null ? String(status.imagesManaged) : '-'} + {String(status.imagesManaged ?? '-')} = ({ obj }) title={t('Observed Generation')} help={t('The generation of the resource that was last reconciled.')} > - {status.observedGeneration != null ? String(status.observedGeneration) : '-'} + {String(status.observedGeneration ?? '-')} diff --git a/src/gitops/components/imageupdater/ImageUpdaterList.tsx b/src/gitops/components/imageupdater/ImageUpdaterList.tsx index b8d59b4ca..7b2d56659 100644 --- a/src/gitops/components/imageupdater/ImageUpdaterList.tsx +++ b/src/gitops/components/imageupdater/ImageUpdaterList.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom-v5-compat'; +import {useTranslation} from 'react-i18next'; +import {useLocation} from 'react-router-dom-v5-compat'; import TechPreviewBadge from 'src/plugin/import/badges/TechPreviewBadge'; import ActionsDropdown from '@gitops/utils/components/ActionDropDown/ActionDropDown'; -import { modelToGroupVersionKind } from '@gitops/utils/utils'; +import {modelToGroupVersionKind} from '@gitops/utils/utils'; import { Action, K8sResourceCommon, @@ -12,27 +12,23 @@ import { ListPageCreate, ListPageFilter, ListPageHeader, + ResourceLink, RowFilter, Timestamp, useK8sWatchResource, useListPageFilter, } from '@openshift-console/dynamic-plugin-sdk'; -import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; -import { ErrorState } from '@patternfly/react-component-groups'; -import { EmptyState, EmptyStateBody } 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 {ErrorState} from '@patternfly/react-component-groups'; +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 {Tbody, Td, ThProps, Tr} from '@patternfly/react-table'; -import { ImageUpdaterKind, ImageUpdaterModel, imageUpdaterModelRef } from '../../models/ImageUpdaterModel'; -import { - ShowOperandsInAllNamespacesRadioGroup, - useShowOperandsInAllNamespaces, -} from '../shared/AllNamespaces'; -import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; +import {ImageUpdaterKind, ImageUpdaterModel, imageUpdaterModelRef} from '../../models/ImageUpdaterModel'; +import {ShowOperandsInAllNamespacesRadioGroup, useShowOperandsInAllNamespaces,} from '../shared/AllNamespaces'; +import {GitOpsDataViewTable, useGitOpsDataViewSort} from '../shared/DataView'; -import { useImageUpdaterActionsProvider } from './hooks/useImageUpdaterActionsProvider'; +import {useImageUpdaterActionsProvider} from './hooks/useImageUpdaterActionsProvider'; import './imageupdater-list.scss'; @@ -51,41 +47,35 @@ const ImageUpdaterList: React.FC = ({ const [showOperandsInAllNamespaces] = useShowOperandsInAllNamespaces(); const listAllNamespaces = location.pathname?.includes('openshift-gitops-operator') && showOperandsInAllNamespaces; - if (listAllNamespaces) { - namespace = null; - } + const effectiveNamespace = listAllNamespaces ? null : namespace; const [imageUpdaters, loaded, loadError] = useK8sWatchResource({ isList: true, - groupVersionKind: { - group: 'argocd-image-updater.argoproj.io', - kind: 'ImageUpdater', - version: 'v1alpha1', - }, + groupVersionKind: modelToGroupVersionKind(ImageUpdaterModel), namespaced: !listAllNamespaces, - namespace, + namespace: effectiveNamespace, }); const columnSortConfig = React.useMemo(() => { return [ 'name', - ...(!listAllNamespaces || !namespace || namespace === '' ? ['namespace'] : []), + ...(!listAllNamespaces || !effectiveNamespace || effectiveNamespace === '' ? ['namespace'] : []), 'apps', 'images', 'last-checked', 'ready', 'actions', - ].map((key) => ({ key })); - }, [listAllNamespaces, namespace]); + ].map((key) => ({key})); + }, [listAllNamespaces, effectiveNamespace]); - const { searchParams, sortBy, direction, getSortParams } = + const {searchParams, sortBy, direction, getSortParams} = useGitOpsDataViewSort(columnSortConfig); // Get search query from URL parameters const searchQuery = searchParams.get('q') || ''; - const { t } = useTranslation('plugin__gitops-plugin'); + const {t} = useTranslation('plugin__gitops-plugin'); - const columnsDV = useColumnsDV(namespace, getSortParams); + const columnsDV = useColumnsDV(effectiveNamespace, getSortParams); const sortedItems = React.useMemo(() => { return sortData(imageUpdaters as ImageUpdaterKind[], sortBy, direction); }, [imageUpdaters, sortBy, direction]); @@ -102,7 +92,7 @@ const ImageUpdaterList: React.FC = ({ }); }, [filteredData, searchQuery]); - const rows = useImageUpdaterRowsDV(filteredBySearch as ImageUpdaterKind[], namespace); + const rows = useImageUpdaterRowsDV(filteredBySearch as ImageUpdaterKind[], effectiveNamespace); const hasItems = React.useMemo(() => { return sortedItems.length > 0; @@ -114,12 +104,12 @@ const ImageUpdaterList: React.FC = ({ <> {t('No ImageUpdaters match the search filter')}{' '} "{searchQuery}". -
+
{t('Try removing the filter or searching for a different term to see more ImageUpdaters.')} ); } - return namespace + return effectiveNamespace ? t('There are no ImageUpdaters in this namespace.') : t('There are no ImageUpdaters in all namespaces.'); }; @@ -173,7 +163,7 @@ const ImageUpdaterList: React.FC = ({ } helpText={ location.pathname?.includes('openshift-gitops-operator') ? ( - + ) : null } hideFavoriteButton={false} @@ -258,12 +248,12 @@ export const sortData = ( }; export const useColumnsDV = ( - namespace: string | undefined, + namespace: string | null | undefined, getSortParams: (columnIndex: number) => ThProps['sort'], ): DataViewTh[] => { const showNamespace = !namespace || namespace === ''; const i: number = showNamespace ? 1 : 0; - const { t } = useTranslation('plugin__gitops-plugin'); + const {t} = useTranslation('plugin__gitops-plugin'); const columns: DataViewTh[] = [ { cell: t('Name'), @@ -271,21 +261,21 @@ export const useColumnsDV = ( 'aria-label': 'name', className: 'pf-m-width-20', sort: getSortParams(0), - style: { minWidth: '200px' }, + style: {minWidth: '200px'}, }, }, ...(showNamespace ? [ - { - cell: t('Namespace'), - props: { - 'aria-label': 'namespace', - className: 'pf-m-width-15', - sort: getSortParams(1), - style: { minWidth: '150px' }, - }, + { + cell: t('Namespace'), + props: { + 'aria-label': 'namespace', + className: 'pf-m-width-15', + sort: getSortParams(1), + style: {minWidth: '150px'}, }, - ] + }, + ] : []), { cell: t('Apps'), @@ -321,7 +311,7 @@ export const useColumnsDV = ( }, { cell: '', - props: { 'aria-label': 'actions' }, + props: {'aria-label': 'actions'}, }, ]; @@ -330,10 +320,10 @@ export const useColumnsDV = ( export const useImageUpdaterRowsDV = ( imageUpdaterList: ImageUpdaterKind[], - namespace: string | undefined, + namespace: string | null | undefined, ): DataViewTr[] => { const rows: DataViewTr[] = []; - if (imageUpdaterList == undefined || imageUpdaterList.length == 0) { + if (imageUpdaterList === undefined || imageUpdaterList.length === 0) { return rows; } const showNamespace = !namespace || namespace === ''; @@ -358,12 +348,12 @@ export const useImageUpdaterRowsDV = ( }, ...(showNamespace ? [ - { - cell: , - id: obj.metadata.namespace, - dataLabel: 'Namespace', - }, - ] + { + cell: , + id: obj.metadata.namespace, + dataLabel: 'Namespace', + }, + ] : []), { id: 'apps', @@ -378,8 +368,8 @@ export const useImageUpdaterRowsDV = ( { id: 'last-checked', cell: obj.status?.lastCheckedAt ? ( -
- +
+
) : ( '-' @@ -393,8 +383,8 @@ export const useImageUpdaterRowsDV = ( }, { id: 'actions-' + index, - cell: , - props: { style: { paddingTop: 8, paddingRight: 0, paddingLeft: 0, width: 10 } }, + cell: , + props: {className: 'gitops-imageupdater-list__actions-cell'}, }, ]); }); @@ -403,10 +393,10 @@ export const useImageUpdaterRowsDV = ( const ImageUpdaterActionsCell: React.FC<{ imageUpdater: ImageUpdaterKind; -}> = ({ imageUpdater }) => { +}> = ({imageUpdater}) => { const actionList: Action[] = useImageUpdaterActionsProvider(imageUpdater); return ( -
+
string): RowFilter[] => [ return apps > 0 ? 'has-apps' : 'no-apps'; }, filter: (input, item) => { - if (input.selected?.length) { - const apps = (item as ImageUpdaterKind).status?.applicationsMatched; - const hasApps = apps > 0; - if (input.selected.includes('has-apps')) { - return hasApps; - } - if (input.selected.includes('no-apps')) { - return !hasApps; - } - } - return true; + if (!input.selected?.length) return true; + const apps = (item as ImageUpdaterKind).status?.applicationsMatched; + const hasApps = apps > 0; + return ( + (input.selected.includes('has-apps') && hasApps) || + (input.selected.includes('no-apps') && !hasApps) + ); }, items: [ - { id: 'has-apps', title: t('Has Apps') }, - { id: 'no-apps', title: t('No Apps') }, + {id: 'has-apps', title: t('Has Apps')}, + {id: 'no-apps', title: t('No Apps')}, ], }, { @@ -452,23 +438,19 @@ const getFilters = (t: (key: string) => string): RowFilter[] => [ return readyCondition?.status === 'True' ? 'ready' : 'not-ready'; }, filter: (input, item) => { - if (input.selected?.length) { - const readyCondition = (item as ImageUpdaterKind).status?.conditions?.find( - (c) => c.type === 'Ready', - ); - const isReady = readyCondition?.status === 'True'; - if (input.selected.includes('ready')) { - return isReady; - } - if (input.selected.includes('not-ready')) { - return !isReady; - } - } - return true; + if (!input.selected?.length) return true; + const readyCondition = (item as ImageUpdaterKind).status?.conditions?.find( + (c) => c.type === 'Ready', + ); + const isReady = readyCondition?.status === 'True'; + return ( + (input.selected.includes('ready') && isReady) || + (input.selected.includes('not-ready') && !isReady) + ); }, items: [ - { id: 'ready', title: t('Ready') }, - { id: 'not-ready', title: t('Not Ready') }, + {id: 'ready', title: t('Ready')}, + {id: 'not-ready', title: t('Not Ready')}, ], }, ]; diff --git a/src/gitops/components/imageupdater/ImageUpdaterNavPage.tsx b/src/gitops/components/imageupdater/ImageUpdaterNavPage.tsx index 8d3b49b12..b5b194101 100644 --- a/src/gitops/components/imageupdater/ImageUpdaterNavPage.tsx +++ b/src/gitops/components/imageupdater/ImageUpdaterNavPage.tsx @@ -4,14 +4,16 @@ import { HorizontalNav, useK8sWatchResource } from '@openshift-console/dynamic-p import { ErrorState } from '@patternfly/react-component-groups'; import { Bullseye, Spinner } from '@patternfly/react-core'; +import {modelToGroupVersionKind} from '@gitops/utils/utils'; + import { ImageUpdaterKind, ImageUpdaterModel } from '../../models/ImageUpdaterModel'; import { useGitOpsTranslation } from '../../utils/hooks/useGitOpsTranslation'; import DetailsPageHeader from '../shared/DetailsPageHeader/DetailsPageHeader'; import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; import { useImageUpdaterActionsProvider } from './hooks/useImageUpdaterActionsProvider'; -import ImageUpdaterDetailsTab from "@gitops/components/imageupdater/ImageUpdaterDetailsTab"; -import ImageUpdaterRecentUpdatesTab from "@gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab"; +import ImageUpdaterDetailsTab from './ImageUpdaterDetailsTab'; +import ImageUpdaterRecentUpdatesTab from './ImageUpdaterRecentUpdatesTab'; type ImageUpdaterPageProps = { name: string; @@ -22,11 +24,7 @@ type ImageUpdaterPageProps = { const ImageUpdaterNavPage: React.FC = ({ name, namespace, kind }) => { const { t } = useGitOpsTranslation(); const [imageUpdater, loaded, loadError] = useK8sWatchResource({ - groupVersionKind: { - group: 'argocd-image-updater.argoproj.io', - kind: 'ImageUpdater', - version: 'v1alpha1', - }, + groupVersionKind: modelToGroupVersionKind(ImageUpdaterModel), kind, name, namespace, diff --git a/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx b/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx index 3488e4d18..2780fed08 100644 --- a/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx +++ b/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; -import { useTranslation } from 'react-i18next'; import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; import { EmptyState, EmptyStateBody, PageSection, PageSectionVariants, Title } from '@patternfly/react-core'; @@ -19,8 +18,6 @@ type ImageUpdaterRecentUpdatesTabProps = RouteComponentProps<{ ns: string; name: const ImageUpdaterRecentUpdatesTab: React.FC = ({ obj }) => { const { t } = useGitOpsTranslation(); - const recentUpdates: ImageUpdaterRecentUpdate[] = obj?.status?.recentUpdates || []; - const columnSortConfig = React.useMemo( () => ['alias', 'image', 'new-version', 'apps-updated', 'updated-at', 'message'].map((key) => ({ @@ -31,7 +28,9 @@ const ImageUpdaterRecentUpdatesTab: React.FC const { sortBy, direction, getSortParams } = useGitOpsDataViewSort(columnSortConfig); - const columnsDV = useColumnsDV(getSortParams); + const columnsDV = useColumnsDV(getSortParams, t); + + const recentUpdates: ImageUpdaterRecentUpdate[] = obj?.status?.recentUpdates || []; const sortedUpdates = React.useMemo( () => sortData(recentUpdates, sortBy, direction), @@ -40,6 +39,8 @@ const ImageUpdaterRecentUpdatesTab: React.FC const rows = useRowsDV(sortedUpdates); + if (!obj) return null; + const empty = ( @@ -54,8 +55,6 @@ const ImageUpdaterRecentUpdatesTab: React.FC ); - if (!obj) return null; - return (
{ return rows; }; -const useColumnsDV = (getSortParams: (columnIndex: number) => ThProps['sort']): DataViewTh[] => { - const { t } = useTranslation('plugin__gitops-plugin'); - +const useColumnsDV = ( + getSortParams: (columnIndex: number) => ThProps['sort'], + t: (key: string) => string, +): DataViewTh[] => { return [ { cell: t('Alias'), @@ -156,7 +156,7 @@ const sortData = ( if (!sortBy || !direction) return data; return [...data].sort((a, b) => { - let aValue: any, bValue: any; + let aValue: string | number, bValue: string | number; switch (sortBy) { case 'alias': diff --git a/src/gitops/components/imageupdater/imageupdater-list.scss b/src/gitops/components/imageupdater/imageupdater-list.scss index 54c8b2278..c4544608c 100644 --- a/src/gitops/components/imageupdater/imageupdater-list.scss +++ b/src/gitops/components/imageupdater/imageupdater-list.scss @@ -20,3 +20,18 @@ text-overflow: ellipsis; white-space: nowrap; } + +.gitops-imageupdater-list__timestamp { + white-space: nowrap; +} + +.gitops-imageupdater-list__actions { + text-align: right; +} + +.gitops-imageupdater-list__actions-cell { + padding-top: 8px; + padding-right: 0; + padding-left: 0; + width: 10px; +} diff --git a/src/gitops/models/ImageUpdaterModel.ts b/src/gitops/models/ImageUpdaterModel.ts index 981e88012..292147933 100644 --- a/src/gitops/models/ImageUpdaterModel.ts +++ b/src/gitops/models/ImageUpdaterModel.ts @@ -39,7 +39,7 @@ export const ImageUpdaterModel: K8sModel = { apiVersion: 'v1alpha1', apiGroup: 'argocd-image-updater.argoproj.io', plural: 'imageupdaters', - abbr: 'imageupdater', + abbr: 'IU', namespaced: true, kind: 'ImageUpdater', id: 'imageupdater', From 6b5dd46fca5368017f34e5c4abefc764cd32cb02 Mon Sep 17 00:00:00 2001 From: dkarpele Date: Wed, 27 May 2026 22:03:29 +0200 Subject: [PATCH 3/4] fix(image-updater): address coderabbit review comments Signed-off-by: dkarpele --- console-extensions.json | 2 +- src/gitops/components/imageupdater/ImageUpdaterList.tsx | 7 ++++--- .../imageupdater/ImageUpdaterRecentUpdatesTab.tsx | 4 ++-- src/gitops/components/imageupdater/imageupdater-list.scss | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/console-extensions.json b/console-extensions.json index 6ea199247..006bfa3e1 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -392,7 +392,7 @@ "properties": { "perspective": "admin", "section": "gitops-navigation-section", - "id": "argocd-separator", + "id": "argocd-separator-imageupdater", "insertAfter": "imageupdater" }, "flags": { diff --git a/src/gitops/components/imageupdater/ImageUpdaterList.tsx b/src/gitops/components/imageupdater/ImageUpdaterList.tsx index 7b2d56659..1e898c1b3 100644 --- a/src/gitops/components/imageupdater/ImageUpdaterList.tsx +++ b/src/gitops/components/imageupdater/ImageUpdaterList.tsx @@ -55,17 +55,18 @@ const ImageUpdaterList: React.FC = ({ namespace: effectiveNamespace, }); + const showNamespaceColumn = !effectiveNamespace || effectiveNamespace === ''; const columnSortConfig = React.useMemo(() => { return [ 'name', - ...(!listAllNamespaces || !effectiveNamespace || effectiveNamespace === '' ? ['namespace'] : []), + ...(showNamespaceColumn ? ['namespace'] : []), 'apps', 'images', 'last-checked', 'ready', 'actions', - ].map((key) => ({key})); - }, [listAllNamespaces, effectiveNamespace]); + ].map((key) => ({ key })); + }, [showNamespaceColumn]); const {searchParams, sortBy, direction, getSortParams} = useGitOpsDataViewSort(columnSortConfig); diff --git a/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx b/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx index 2780fed08..aee14ba65 100644 --- a/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx +++ b/src/gitops/components/imageupdater/ImageUpdaterRecentUpdatesTab.tsx @@ -16,6 +16,8 @@ type ImageUpdaterRecentUpdatesTabProps = RouteComponentProps<{ ns: string; name: }; const ImageUpdaterRecentUpdatesTab: React.FC = ({ obj }) => { + if (!obj) return null; + const { t } = useGitOpsTranslation(); const columnSortConfig = React.useMemo( @@ -39,8 +41,6 @@ const ImageUpdaterRecentUpdatesTab: React.FC const rows = useRowsDV(sortedUpdates); - if (!obj) return null; - const empty = ( diff --git a/src/gitops/components/imageupdater/imageupdater-list.scss b/src/gitops/components/imageupdater/imageupdater-list.scss index c4544608c..aaae535e5 100644 --- a/src/gitops/components/imageupdater/imageupdater-list.scss +++ b/src/gitops/components/imageupdater/imageupdater-list.scss @@ -1,6 +1,6 @@ // 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'] { +.gitops-imageupdater-list .pf-c-table tbody td[data-label='Name'], +.gitops-imageupdater-list .pf-c-table tbody td[data-label='Namespace'] { white-space: nowrap !important; overflow: hidden; text-overflow: ellipsis; @@ -15,7 +15,7 @@ } // Description column text truncation -.pf-c-table tbody td[data-label='Description'] { +.gitops-imageupdater-list .pf-c-table tbody td[data-label='Description'] { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; From 46df71edf3b86e52c0a1761e9afb04a19b1f90f6 Mon Sep 17 00:00:00 2001 From: dkarpele Date: Thu, 28 May 2026 21:18:49 +0200 Subject: [PATCH 4/4] fix(image-updater): remove separator after ImageUpdaters Signed-off-by: dkarpele --- console-extensions.json | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/console-extensions.json b/console-extensions.json index 006bfa3e1..873985fbd 100644 --- a/console-extensions.json +++ b/console-extensions.json @@ -387,20 +387,6 @@ ] } }, - { - "type": "console.navigation/separator", - "properties": { - "perspective": "admin", - "section": "gitops-navigation-section", - "id": "argocd-separator-imageupdater", - "insertAfter": "imageupdater" - }, - "flags": { - "required": [ - "IMAGEUPDATER" - ] - } - }, { "type": "console.navigation/resource-ns", "properties": {