From 6eba5a16eb61c232a1a049634f95b1ab97d4b47e Mon Sep 17 00:00:00 2001 From: Keith Chong Date: Thu, 30 Jul 2026 14:25:18 -0400 Subject: [PATCH 1/5] feat: Add Rollout actions (#8568) Signed-off-by: Keith Chong --- locales/en/plugin__gitops-plugin.json | 14 +- locales/ja/plugin__gitops-plugin.json | 14 +- locales/ko/plugin__gitops-plugin.json | 14 +- locales/zh/plugin__gitops-plugin.json | 14 +- package.json | 19 +- pnpm-lock.yaml | 28 ++ .../components/rollout/RolloutDetailsTab.tsx | 8 +- src/gitops/components/rollout/RolloutList.tsx | 15 +- .../components/rollout/RolloutNavPage.tsx | 6 +- .../rollout/RolloutRevisionsTab.tsx | 6 +- .../components/rollout/RolloutStatus.tsx | 11 +- .../hooks/useRolloutActionsProvider.tsx | 75 ----- .../useRolloutRevisionsActionsProvider.tsx | 120 ++++++++ .../useRolloutRevisionsRSActionsProvider.tsx | 48 +++ .../rollout/revisions/ReplicaSetInfo.ts | 2 + .../rollout/revisions/Revisions.scss | 6 + .../rollout/revisions/Revisions.tsx | 291 +++++++++++++++++- .../components/rollout/services/Rollout.ts | 88 ++++++ .../components/shared/Ticker/Ticker.tsx | 33 ++ .../ActionDropDown/ActionDropDown.tsx | 12 +- src/gitops/utils/components/Icons/Icons.tsx | 2 +- src/gitops/utils/utils.tsx | 43 +++ 22 files changed, 746 insertions(+), 123 deletions(-) create mode 100644 src/gitops/components/rollout/hooks/useRolloutRevisionsActionsProvider.tsx create mode 100644 src/gitops/components/rollout/hooks/useRolloutRevisionsRSActionsProvider.tsx create mode 100644 src/gitops/components/rollout/revisions/Revisions.scss create mode 100644 src/gitops/components/rollout/services/Rollout.ts create mode 100644 src/gitops/components/shared/Ticker/Ticker.tsx diff --git a/locales/en/plugin__gitops-plugin.json b/locales/en/plugin__gitops-plugin.json index ffdbf1a23..de5d3c913 100644 --- a/locales/en/plugin__gitops-plugin.json +++ b/locales/en/plugin__gitops-plugin.json @@ -255,17 +255,26 @@ "No pods": "No pods", "There are no pods associated with the rollout.": "There are no pods associated with the rollout.", "Edit Pod": "Edit Pod", + "Edit Rollout": "Edit Rollout", "Promote": "Promote", "Full Promote": "Full Promote", "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", - "Edit Rollout": "Edit Rollout", + "Rollback": "Rollback", "Age": "Age", "Info": "Info", "Ready containers": "Ready containers", "ready": "ready", "0 Pods": "0 Pods", + "Scaling down in:": "Scaling down in:", + "{{x}} failed with an error.": "{{x}} failed with an error.", + "The current status of the rollout": "The current status of the rollout", + "There is no rollout status. Check that the Rollout Manager is created and is available.": "There is no rollout status. Check that the Rollout Manager is created and is available.", + "Strategy": "Strategy", + "Whether the rollout is using a blue-green or canary strategy": "Whether the rollout is using a blue-green or canary strategy", + "FullPromote": "FullPromote", + "Close": "Close", "Stable": "Stable", "Active": "Active", "Preview": "Preview", @@ -273,9 +282,6 @@ "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", "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.", diff --git a/locales/ja/plugin__gitops-plugin.json b/locales/ja/plugin__gitops-plugin.json index 2430d79c3..445e49764 100644 --- a/locales/ja/plugin__gitops-plugin.json +++ b/locales/ja/plugin__gitops-plugin.json @@ -255,17 +255,26 @@ "No pods": "No pods", "There are no pods associated with the rollout.": "There are no pods associated with the rollout.", "Edit Pod": "Edit Pod", + "Edit Rollout": "Edit Rollout", "Promote": "Promote", "Full Promote": "Full Promote", "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", - "Edit Rollout": "Edit Rollout", + "Rollback": "Rollback", "Age": "Age", "Info": "Info", "Ready containers": "Ready containers", "ready": "ready", "0 Pods": "0 Pods", + "Scaling down in:": "Scaling down in:", + "{{x}} failed with an error.": "{{x}} failed with an error.", + "The current status of the rollout": "The current status of the rollout", + "There is no rollout status. Check that the Rollout Manager is created and is available.": "There is no rollout status. Check that the Rollout Manager is created and is available.", + "Strategy": "Strategy", + "Whether the rollout is using a blue-green or canary strategy": "Whether the rollout is using a blue-green or canary strategy", + "FullPromote": "FullPromote", + "Close": "Close", "Stable": "Stable", "Active": "Active", "Preview": "Preview", @@ -273,9 +282,6 @@ "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", "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.", diff --git a/locales/ko/plugin__gitops-plugin.json b/locales/ko/plugin__gitops-plugin.json index 9b2a17804..f1fffc805 100644 --- a/locales/ko/plugin__gitops-plugin.json +++ b/locales/ko/plugin__gitops-plugin.json @@ -255,17 +255,26 @@ "No pods": "No pods", "There are no pods associated with the rollout.": "There are no pods associated with the rollout.", "Edit Pod": "Edit Pod", + "Edit Rollout": "Edit Rollout", "Promote": "Promote", "Full Promote": "Full Promote", "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", - "Edit Rollout": "Edit Rollout", + "Rollback": "Rollback", "Age": "Age", "Info": "Info", "Ready containers": "Ready containers", "ready": "ready", "0 Pods": "0 Pods", + "Scaling down in:": "Scaling down in:", + "{{x}} failed with an error.": "{{x}} failed with an error.", + "The current status of the rollout": "The current status of the rollout", + "There is no rollout status. Check that the Rollout Manager is created and is available.": "There is no rollout status. Check that the Rollout Manager is created and is available.", + "Strategy": "Strategy", + "Whether the rollout is using a blue-green or canary strategy": "Whether the rollout is using a blue-green or canary strategy", + "FullPromote": "FullPromote", + "Close": "Close", "Stable": "Stable", "Active": "Active", "Preview": "Preview", @@ -273,9 +282,6 @@ "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", "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.", diff --git a/locales/zh/plugin__gitops-plugin.json b/locales/zh/plugin__gitops-plugin.json index 873866551..9266519a8 100644 --- a/locales/zh/plugin__gitops-plugin.json +++ b/locales/zh/plugin__gitops-plugin.json @@ -255,17 +255,26 @@ "No pods": "No pods", "There are no pods associated with the rollout.": "There are no pods associated with the rollout.", "Edit Pod": "Edit Pod", + "Edit Rollout": "Edit Rollout", "Promote": "Promote", "Full Promote": "Full Promote", "Abort": "Abort", "Retry": "Retry", "Restart": "Restart", - "Edit Rollout": "Edit Rollout", + "Rollback": "Rollback", "Age": "Age", "Info": "Info", "Ready containers": "Ready containers", "ready": "ready", "0 Pods": "0 Pods", + "Scaling down in:": "Scaling down in:", + "{{x}} failed with an error.": "{{x}} failed with an error.", + "The current status of the rollout": "The current status of the rollout", + "There is no rollout status. Check that the Rollout Manager is created and is available.": "There is no rollout status. Check that the Rollout Manager is created and is available.", + "Strategy": "Strategy", + "Whether the rollout is using a blue-green or canary strategy": "Whether the rollout is using a blue-green or canary strategy", + "FullPromote": "FullPromote", + "Close": "Close", "Stable": "Stable", "Active": "Active", "Preview": "Preview", @@ -273,9 +282,6 @@ "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", "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.", diff --git a/package.json b/package.json index c7e262ca3..69dc792cb 100644 --- a/package.json +++ b/package.json @@ -22,15 +22,16 @@ }, "devDependencies": { "@dagrejs/dagre": "^1.1.8", + "@jest/types": "^29.0.0", "@openshift-console/dynamic-plugin-sdk": "^4.19.1", "@openshift-console/dynamic-plugin-sdk-internal": "^4.19.1", "@openshift-console/dynamic-plugin-sdk-webpack": "^4.19.1", "@openshift-console/plugin-shared": "^0.0.3", "@patternfly/quickstarts": "^6.4.0", "@patternfly/react-charts": "^8.4.0", + "@patternfly/react-component-groups": "^6.3.0", "@patternfly/react-core": "^6.4.0", "@patternfly/react-data-view": "^6.4.0", - "@patternfly/react-component-groups": "^6.3.0", "@patternfly/react-icons": "^6.4.0", "@patternfly/react-styles": "^6.4.0", "@patternfly/react-table": "^6.4.0", @@ -38,10 +39,9 @@ "@patternfly/react-topology": "^6.2.0", "@svgr/webpack": "^8.1.0", "@types/classnames": "^2.3.1", - "@jest/types": "^29.0.0", - "@types/jest": "^29.0.0", "@types/dagre": "^0.7.53", "@types/git-url-parse": "^9.0.0", + "@types/jest": "^29.0.0", "@types/lodash": "^4.14.0", "@types/node": "^17.0.21", "@types/react": "17.0.91", @@ -67,6 +67,8 @@ "http-server": "0.12.x", "i18next": "^19.8.3", "i18next-parser": "^3.3.0", + "jest": "^29.0.0", + "jest-environment-jsdom": "^29.0.0", "lodash-es": "^4.17.23", "marked": "15.0.12", "mobx-react": "^7.6.0", @@ -76,16 +78,14 @@ "react-dom": "17.0.2", "react-i18next": "^11.7.3", "react-router": "5.3.4", - "semver": "^7.0.0", "sass": "^1.42.1", "sass-loader": "^10.1.1", + "semver": "^7.0.0", "style-loader": "^2.0.0", + "ts-jest": "^29.0.0", "ts-loader": "^9.5.1", "ts-node": "^10.7.0", "tsconfig-paths-webpack-plugin": "^4.1.0", - "jest": "^29.0.0", - "jest-environment-jsdom": "^29.0.0", - "ts-jest": "^29.0.0", "typesafe-actions": "^4.4.2", "typescript": "5.9.3", "webpack": "^5.1.0", @@ -122,12 +122,15 @@ "dagre": "^0.8.5", "git-url-parse": "^13.1.0", "lodash": "^4.18.1", + "moment": "^2.29.4", + "pnpm": "^11.18.0", "react": "17.0.2", "react-helmet": "^6.1.0", "react-redux": "7.2.9", "react-router-dom-v5-compat": "^6.30.0", "react-router-hash-link": "2.4.3", - "redux": "4.0.1" + "redux": "4.0.1", + "rxjs": "^7.8.1" }, "//TODO": [ "For @patternfly/react-icons: find another solution for icons from older react-icons version and remove this dependency." diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67af753fb..eeb0fcf44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,12 @@ importers: lodash: specifier: ^4.18.1 version: 4.18.1 + moment: + specifier: ^2.29.4 + version: 2.30.1 + pnpm: + specifier: ^11.18.0 + version: 11.18.0 react: specifier: 17.0.2 version: 17.0.2 @@ -52,6 +58,9 @@ importers: redux: specifier: 4.0.1 version: 4.0.1 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 devDependencies: '@dagrejs/dagre': specifier: ^1.1.8 @@ -4205,6 +4214,9 @@ packages: mobx@6.13.6: resolution: {integrity: sha512-r19KNV0uBN4b+ER8Z0gA4y+MzDYIQ2SvOmn3fUrqPnWXdQfakd9yfbPBDBF/p5I+bd3N5Rk1fHONIvMay+bJGA==} + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -4464,6 +4476,11 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pnpm@11.18.0: + resolution: {integrity: sha512-M9g8d9qC9J+6g2klxvG4QRgewxMrZwY5vQEvcHX1x89jTF+HAUfBmq50ePrAHfCdiJLogEVIlu3SPumzN1dWPA==} + engines: {node: '>=22.13'} + hasBin: true + point-in-svg-path@1.0.2: resolution: {integrity: sha512-+Smsf7B9e7eRFHIwpN+4rE8inF2APbFWeywPfUgbeh02xdJSkbTz6Pqdt7A36wVCR+CnLbaNkRnBjgOpF5RMVQ==} engines: {node: '>=8.x.x'} @@ -4880,6 +4897,9 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -10725,6 +10745,8 @@ snapshots: mobx@6.13.6: {} + moment@2.30.1: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -10980,6 +11002,8 @@ snapshots: dependencies: find-up: 4.1.0 + pnpm@11.18.0: {} + point-in-svg-path@1.0.2: {} popper.js@1.16.1: {} @@ -11415,6 +11439,10 @@ snapshots: rw@1.3.3: {} + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 diff --git a/src/gitops/components/rollout/RolloutDetailsTab.tsx b/src/gitops/components/rollout/RolloutDetailsTab.tsx index 168c5b69b..c5ab38975 100644 --- a/src/gitops/components/rollout/RolloutDetailsTab.tsx +++ b/src/gitops/components/rollout/RolloutDetailsTab.tsx @@ -119,7 +119,13 @@ const RolloutDetailsTab: React.FC = ({ obj }) => { diff --git a/src/gitops/components/rollout/RolloutList.tsx b/src/gitops/components/rollout/RolloutList.tsx index 4f47f3b05..da54ec184 100644 --- a/src/gitops/components/rollout/RolloutList.tsx +++ b/src/gitops/components/rollout/RolloutList.tsx @@ -5,7 +5,7 @@ import TechPreviewBadge from 'src/plugin/import/badges/TechPreviewBadge'; import { AppProjectKind } from '@gitops/models/AppProjectModel'; import ActionsDropdown from '@gitops/utils/components/ActionDropDown/ActionDropDown'; import { isApplicationRefreshing } from '@gitops/utils/gitops'; -import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; +import { t, useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; import { getSelectorSearchURL, modelToGroupVersionKind, modelToRef } from '@gitops/utils/utils'; import { Action, @@ -386,7 +386,18 @@ export const useRolloutsRowsDV = (rolloutsList, namespace): DataViewTr[] => { : []), { id: 'status', - cell: , + cell: ( + + ), }, { id: 'pods', diff --git a/src/gitops/components/rollout/RolloutNavPage.tsx b/src/gitops/components/rollout/RolloutNavPage.tsx index 53a4434c8..74d9d94e6 100644 --- a/src/gitops/components/rollout/RolloutNavPage.tsx +++ b/src/gitops/components/rollout/RolloutNavPage.tsx @@ -10,6 +10,7 @@ import EventsTab from '../shared/EventsTab/EventsTab'; import ResourceYAMLTab from '../shared/ResourceYAMLTab/ResourceYAMLTab'; import { useRolloutActionsProvider } from './hooks/useRolloutActionsProvider'; +import { useRolloutRevisionsActionsProvider } from './hooks/useRolloutRevisionsActionsProvider'; import { RolloutKind, RolloutModel } from './model/RolloutModel'; import RolloutDetailsTab from './RolloutDetailsTab'; import RolloutPodsTab from './RolloutPodsTab'; @@ -34,7 +35,10 @@ const RolloutNavPage: React.FC = ({ name, namespace, kind }) = namespace, }); - const [actions] = useRolloutActionsProvider(rollout); + const isRevisionsTab = /\/revisions\/?$/.test(location.pathname); + const [actions] = isRevisionsTab + ? useRolloutRevisionsActionsProvider(rollout) + : useRolloutActionsProvider(rollout); const pages = React.useMemo( () => [ diff --git a/src/gitops/components/rollout/RolloutRevisionsTab.tsx b/src/gitops/components/rollout/RolloutRevisionsTab.tsx index 84e65f098..3cce4b03c 100644 --- a/src/gitops/components/rollout/RolloutRevisionsTab.tsx +++ b/src/gitops/components/rollout/RolloutRevisionsTab.tsx @@ -7,7 +7,7 @@ import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; import { resourceAsArray } from '@gitops/utils/utils'; import { K8sResourceCommon, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; import { ErrorState } from '@patternfly/react-component-groups'; -import { Flex, FlexItem, PageSection, PageSectionVariants, Title } from '@patternfly/react-core'; +import { Flex, FlexItem, PageSection, PageSectionVariants } from '@patternfly/react-core'; import { ReplicaSetKind, RolloutKind } from './model/RolloutModel'; import { Revisions } from './revisions/Revisions'; @@ -51,10 +51,6 @@ const RolloutRevisionsTab: React.FC = ({ obj: rollout variant={PageSectionVariants.default} className={classNames('co-m-pane__body', { 'co-m-pane__body--section-heading': true })} > - - {t('Rollout Revisions')} - - {/* eslint-disable-next-line no-nested-ternary */} {loadError || podsloadError ? (
diff --git a/src/gitops/components/rollout/RolloutStatus.tsx b/src/gitops/components/rollout/RolloutStatus.tsx index b20bb6f1d..31e11aa11 100644 --- a/src/gitops/components/rollout/RolloutStatus.tsx +++ b/src/gitops/components/rollout/RolloutStatus.tsx @@ -15,9 +15,14 @@ import { RolloutStatus } from './utils/rollout-utils'; interface RolloutStatusProps { status: RolloutStatus; message?: string; + showPhaseLabel?: boolean; } -export const RolloutStatusFragment: React.FC = ({ status, message }) => { +export const RolloutStatusFragment: React.FC = ({ + status, + message, + showPhaseLabel = true, +}) => { let icon: React.ReactNode; switch (status) { case RolloutStatus.Progressing: { @@ -41,9 +46,9 @@ export const RolloutStatusFragment: React.FC = ({ status, me } return ( - Phase: + {showPhaseLabel && Phase:} {icon} {status}{' '} - {status == RolloutStatus.Degraded && message && ( + {(status == RolloutStatus.Degraded || status === undefined) && message && ( diff --git a/src/gitops/components/rollout/hooks/useRolloutActionsProvider.tsx b/src/gitops/components/rollout/hooks/useRolloutActionsProvider.tsx index adcdd2697..af1955a59 100644 --- a/src/gitops/components/rollout/hooks/useRolloutActionsProvider.tsx +++ b/src/gitops/components/rollout/hooks/useRolloutActionsProvider.tsx @@ -23,81 +23,6 @@ export const useRolloutActionsProvider: UseRolloutActionsProvider = (rollout) => const actions = React.useMemo( () => [ - // { - // id: 'gitops-action-promote', - // // disabled: !isDeploying(rollout), - // label: t('Promote'), - // accessReview: { - // group: RolloutModel.apiGroup, - // verb: 'patch' as K8sVerb, - // resource: RolloutModel.plural, - // namespace: rollout?.metadata?.namespace, - // }, - // cta: () => - // // TODO - Show toast alert if it fails but this is proving more challenging then I thought - // // promoteRollout(rollout, false) - // {}, - // }, - // { - // id: 'gitops-action-promote-full', - // // disabled: !isDeploying(rollout), - // label: t('Full Promote'), - // accessReview: { - // group: RolloutModel.apiGroup, - // verb: 'patch' as K8sVerb, - // resource: RolloutModel.plural, - // namespace: rollout?.metadata?.namespace, - // }, - // cta: () => - // // TODO - Show toast alert if it fails but this is proving more challenging then I thought - // // promoteRollout(rollout, true) - // {}, - // }, - // { - // id: 'gitops-action-abort', - // // disabled: !isDeploying(rollout), - // label: t('Abort'), - // accessReview: { - // group: RolloutModel.apiGroup, - // verb: 'patch' as K8sVerb, - // resource: RolloutModel.plural, - // namespace: rollout?.metadata?.namespace, - // }, - // cta: () => - // // TODO - Show toast alert if it fails but this is proving more challenging then I thought - // // abortRollout(rollout) - // {}, - // }, - // { - // id: 'gitops-action-retry', - // disabled: rollout?.status?.phase !== RolloutStatus.Degraded, - // label: t('Retry'), - // accessReview: { - // group: RolloutModel.apiGroup, - // verb: 'patch' as K8sVerb, - // resource: RolloutModel.plural, - // namespace: rollout?.metadata?.namespace, - // }, - // cta: () => - // // TODO - Show toast alert if it fails but this is proving more challenging then I thought - // // retryRollout(rollout) - // {}, - // }, - // { - // id: 'gitops-action-restart', - // disabled: false, - // label: t('Restart'), - // accessReview: { - // group: RolloutModel.apiGroup, - // verb: 'patch' as K8sVerb, - // resource: RolloutModel.plural, - // namespace: rollout?.metadata?.namespace, - // }, - // cta: () => - // // TODO - Show toast alert if it fails but this is proving more challenging then I thought - // // restartRollout(rollout) - // {}, - // }, { id: 'gitops-action-edit-labels-rollout', disabled: false, diff --git a/src/gitops/components/rollout/hooks/useRolloutRevisionsActionsProvider.tsx b/src/gitops/components/rollout/hooks/useRolloutRevisionsActionsProvider.tsx new file mode 100644 index 000000000..fd39b5ea1 --- /dev/null +++ b/src/gitops/components/rollout/hooks/useRolloutRevisionsActionsProvider.tsx @@ -0,0 +1,120 @@ +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 { useGitOpsTranslation } from '../../../utils/hooks/useGitOpsTranslation'; +import { RolloutKind, RolloutModel } from '../model/RolloutModel'; +import { abortRollout, promoteRollout, restartRollout, retryRollout } from '../services/Rollout'; +import { isDeploying, RolloutStatus } from '../utils/rollout-utils'; + +type UseRolloutRevisionsActionsProvider = ( + rollout: RolloutKind, + onError?: (error: Error | string, action: string) => void, +) => [actions: Action[]]; + +export const useRolloutRevisionsActionsProvider: UseRolloutRevisionsActionsProvider = ( + rollout, + onError, +) => { + const { t } = useGitOpsTranslation(); + const navigate = useNavigate(); + const launchLabelsModal = useLabelsModal(rollout); + const launchAnnotationsModal = useAnnotationsModal(rollout); + const launchDeleteModal = useDeleteModal(rollout); + + const actions = React.useMemo( + () => [ + { + id: 'gitops-action-promote', + disabled: !isDeploying(rollout), + label: t('Promote'), + accessReview: { + group: RolloutModel.apiGroup, + verb: 'patch' as K8sVerb, + resource: RolloutModel.plural, + namespace: rollout?.metadata?.namespace, + }, + cta: () => + void promoteRollout(rollout, false).catch((err: unknown) => { + onError?.(err instanceof Error ? err : String(err), t('Promote')); + }), + }, + { + id: 'gitops-action-promote-full', + disabled: !isDeploying(rollout), + label: t('Full Promote'), + accessReview: { + group: RolloutModel.apiGroup, + verb: 'patch' as K8sVerb, + resource: RolloutModel.plural, + namespace: rollout?.metadata?.namespace, + }, + cta: () => + void promoteRollout(rollout, true).catch((err: unknown) => { + onError?.(err instanceof Error ? err : String(err), t('Full Promote')); + }), + }, + { + id: 'separator-rollout-actions', + label: '', + cta: () => {}, + }, + { + id: 'gitops-action-abort', + disabled: !isDeploying(rollout), + label: t('Abort'), + accessReview: { + group: RolloutModel.apiGroup, + verb: 'patch' as K8sVerb, + resource: RolloutModel.plural, + namespace: rollout?.metadata?.namespace, + }, + cta: () => + void abortRollout(rollout).catch((err: unknown) => { + onError?.(err instanceof Error ? err : String(err), t('Abort')); + }), + }, + { + id: 'gitops-action-retry', + disabled: rollout?.status?.phase !== RolloutStatus.Degraded, + label: t('Retry'), + accessReview: { + group: RolloutModel.apiGroup, + verb: 'patch' as K8sVerb, + resource: RolloutModel.plural, + namespace: rollout?.metadata?.namespace, + }, + cta: () => + void retryRollout(rollout).catch((err: unknown) => { + onError?.(err instanceof Error ? err : String(err), t('Retry')); + }), + }, + { + id: 'gitops-action-restart', + disabled: false, + label: t('Restart'), + accessReview: { + group: RolloutModel.apiGroup, + verb: 'patch' as K8sVerb, + resource: RolloutModel.plural, + namespace: rollout?.metadata?.namespace, + }, + cta: () => { + void restartRollout(rollout).catch((err: unknown) => { + onError?.(err instanceof Error ? err : String(err), t('Restart')); + }); + }, + }, + ], + [rollout, launchLabelsModal, launchAnnotationsModal, launchDeleteModal, navigate, t, onError], + ); + + return [actions]; +}; diff --git a/src/gitops/components/rollout/hooks/useRolloutRevisionsRSActionsProvider.tsx b/src/gitops/components/rollout/hooks/useRolloutRevisionsRSActionsProvider.tsx new file mode 100644 index 000000000..e477de7a3 --- /dev/null +++ b/src/gitops/components/rollout/hooks/useRolloutRevisionsRSActionsProvider.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; + +import { Action, K8sVerb } from '@openshift-console/dynamic-plugin-sdk'; + +import { useGitOpsTranslation } from '../../../utils/hooks/useGitOpsTranslation'; +import { RolloutKind, RolloutModel } from '../model/RolloutModel'; +import { ReplicaSetInfo } from '../revisions/ReplicaSetInfo'; +import { rollbackRollout } from '../services/Rollout'; + +type UseRolloutRevisionsRSActionsProvider = ( + rollout: RolloutKind, + replicaSet: ReplicaSetInfo, + index: number, + onError?: (err: Error | string, action: string) => void, +) => [actions: Action[]]; + +export const useRolloutRevisionsRSActionsProvider: UseRolloutRevisionsRSActionsProvider = ( + rollout, + replicaSet, + index, + onError, +) => { + const { t } = useGitOpsTranslation(); + + const actions = React.useMemo( + () => [ + { + id: 'gitops-action-rollback-' + index, + disabled: index === 0, + label: t('Rollback'), + accessReview: { + group: RolloutModel.apiGroup, + verb: 'patch' as K8sVerb, + resource: RolloutModel.plural, + namespace: rollout?.metadata?.namespace, + }, + cta: () => { + void rollbackRollout(rollout, replicaSet.replicaSet).catch((err: unknown) => { + onError?.(err instanceof Error ? err : String(err), t('Rollback')); + }); + }, + }, + ], + [rollout, replicaSet, index, onError, t], + ); + + return [actions]; +}; diff --git a/src/gitops/components/rollout/revisions/ReplicaSetInfo.ts b/src/gitops/components/rollout/revisions/ReplicaSetInfo.ts index 7f805da3b..65a17ea64 100644 --- a/src/gitops/components/rollout/revisions/ReplicaSetInfo.ts +++ b/src/gitops/components/rollout/revisions/ReplicaSetInfo.ts @@ -50,6 +50,7 @@ export type ReplicaSetInfo = { podTemplateHash: string; analysisRuns: AnalysisRunInfo[]; replicaSet?: K8sResourceCommon; + replicaSetScaleDownDeadline?: string; }; function getReplicaSetStatus( @@ -184,6 +185,7 @@ export const getReplicaSetInfo = async ( podTemplateHash: podTemplateHash, analysisRuns: getAnalysisRunInfo(analysisRuns, podTemplateHash, revision), replicaSet: rs, + replicaSetScaleDownDeadline: rs.metadata?.annotations['scale-down-deadline'], }); }); diff --git a/src/gitops/components/rollout/revisions/Revisions.scss b/src/gitops/components/rollout/revisions/Revisions.scss new file mode 100644 index 000000000..4620ad6cb --- /dev/null +++ b/src/gitops/components/rollout/revisions/Revisions.scss @@ -0,0 +1,6 @@ +.gitops-revisions-table { + tbody > tr:first-child { + --pf-v6-c-table__tr--BorderBlockEndWidth: 1px; + border-top: 1px solid; + } +} diff --git a/src/gitops/components/rollout/revisions/Revisions.tsx b/src/gitops/components/rollout/revisions/Revisions.tsx index c4c4fddfb..22b7d44c0 100644 --- a/src/gitops/components/rollout/revisions/Revisions.tsx +++ b/src/gitops/components/rollout/revisions/Revisions.tsx @@ -1,19 +1,46 @@ import * as React from 'react'; import { Link } from 'react-router-dom-v5-compat'; +import moment from 'moment'; import { getPodStatus } from '@gitops/components/shared/pod-utils'; import { PodKind } from '@gitops/topology/console/types'; +import ActionsDropdown from '@gitops/utils/components/ActionDropDown/ActionDropDown'; import { isApplicationRefreshing } from '@gitops/utils/gitops'; import { t } from '@gitops/utils/hooks/useGitOpsTranslation'; -import { getResourceUrl, modelToGroupVersionKind, resourceAsArray } from '@gitops/utils/utils'; import { + formatDuration, + getResourceUrl, + modelToGroupVersionKind, + resourceAsArray, +} from '@gitops/utils/utils'; +import { + Action, K8sModel, ResourceLink, Selector, useK8sModel, useK8sWatchResource, } from '@openshift-console/dynamic-plugin-sdk'; -import { Label, LabelGroup, Spinner, Tooltip } from '@patternfly/react-core'; +import { + Alert, + AlertActionCloseButton, + AlertGroup, + AlertVariant, + Button, + Divider, + Flex, + FlexItem, + Label, + LabelGroup, + Spinner, + Title, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + // Popover, + Tooltip, +} from '@patternfly/react-core'; import { DataViewTh, DataViewTrTree } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import DataViewTableTree from '@patternfly/react-data-view/dist/esm/DataViewTableTree'; import { FolderIcon, FolderOpenIcon } from '@patternfly/react-icons'; @@ -23,9 +50,15 @@ import { EyeIcon } from '@patternfly/react-icons/dist/esm/icons/eye-icon'; import { MigrationIcon } from '@patternfly/react-icons/dist/esm/icons/migration-icon'; import { RunningIcon } from '@patternfly/react-icons/dist/esm/icons/running-icon'; +import { Ticker } from '../../shared/Ticker/Ticker'; import { AnalysisRunStatusFragment } from '../components/AnalysisRunStatus/AnalysisRunStatus'; +import { useRolloutRevisionsActionsProvider } from '../hooks/useRolloutRevisionsActionsProvider'; +import { useRolloutRevisionsRSActionsProvider } from '../hooks/useRolloutRevisionsRSActionsProvider'; import { AnalysisRunKind } from '../model/AnalysisRunModel'; import { ReplicaSetKind, RolloutKind, RolloutModel } from '../model/RolloutModel'; +import { RolloutStatusFragment } from '../RolloutStatus'; +import { abortRollout, promoteRollout, restartRollout, retryRollout } from '../services/Rollout'; +import { isDeploying, RolloutStatus } from '../utils/rollout-utils'; import { getAnalysisRunSelector, @@ -35,6 +68,8 @@ import { ReplicaSetStatus, } from './ReplicaSetInfo'; +import './Revisions.scss'; + interface RevisionsProps { rollout: RolloutKind; replicaSets: ReplicaSetKind[]; @@ -73,14 +108,68 @@ const getColumnsDV = (): DataViewTh[] => { 'aria-label': 'info', }, }, + { + cell: '', + props: { 'aria-label': 'actions' }, + }, ]; return columns; }; +const RolloutActionsCell: React.FC<{ + app: RolloutKind; + index: number; + onError?: (error: Error | string, action: string) => void; +}> = ({ app, index, onError }) => { + const actionList: [actions: Action[]] = useRolloutRevisionsActionsProvider(app, onError); + return ( +
+ +
+ ); +}; + +const RolloutRevisionRSActionsCell: React.FC<{ + rollout: RolloutKind; + replicaSet: ReplicaSetInfo; + index: number; + onError?: (error: Error | string, action: string) => void; +}> = ({ rollout, replicaSet, index, onError }) => { + const actionList: [actions: Action[]] = useRolloutRevisionsRSActionsProvider( + rollout, + replicaSet, + index, + onError, + ); + return ( +
+ +
+ ); +}; + +type RevisionAlert = { + key: string; + title: string; + message: string; + details?: string; + variant: AlertVariant; + actionLinks?: React.ReactNode; +}; + const getRowsDV = ( replicaSetInfo: ReplicaSetInfo[], replicaSetModel: K8sModel, obj: RolloutKind, + onRevisionError?: (error: Error | string, action: string) => void, ): DataViewTrTree[] => { const rows: DataViewTrTree[] = []; const rsChildren: DataViewTrTree[] = []; @@ -93,6 +182,7 @@ const getRowsDV = ( @@ -102,8 +192,25 @@ const getRowsDV = (
, obj.kind, - obj.status?.phase, + { + cell: ( + <> + { + + } + + ), + }, getAgeInMinutes(obj.metadata?.creationTimestamp) + 'm', + { + cell: <>, + }, + { + cell: , + }, ], }); @@ -172,9 +279,46 @@ const getRowsDV = ( ), }, replicaSet?.replicaSet.kind, - replicaSet?.status, + { + cell: ( +
+
{replicaSet?.status}
+ {replicaSet.replicaSetScaleDownDeadline && ( +
+ + {(now) => { + const time = moment(replicaSet.replicaSetScaleDownDeadline).diff( + now.toDate(), + 'second', + ); + return time <= 0 ? null : ( + + + + ); + }} + +
+ )} +
+ ), + }, getAgeInMinutes(replicaSet?.replicaSet?.metadata?.creationTimestamp) + 'm', getStatusSection(replicaSet.statuses), + { + cell: ( + + ), + }, ], ...{ children: podsChildren }, }); @@ -186,6 +330,7 @@ const getRowsDV = ( export const Revisions: React.FC = ({ rollout, replicaSets, pods }) => { const [replicaSetInfo, setReplicaSetInfo] = React.useState([]); + const [alerts, setAlerts] = React.useState([]); const selector: Selector = React.useMemo( () => getAnalysisRunSelector(resourceAsArray(replicaSets)), @@ -201,6 +346,22 @@ export const Revisions: React.FC = ({ rollout, replicaSets, pods selector: selector, }); + const removeAlert = React.useCallback((key: string) => { + setAlerts((prev) => prev.filter((alert) => alert.key !== key)); + }, []); + + const onRevisionError = React.useCallback((error: Error | string, action: string) => { + setAlerts((prev) => [ + ...prev, + { + key: `${Date.now()}-${prev.length}`, + title: t('{{x}} failed with an error.', { x: action }), + message: error instanceof Error ? error.stack : error, + variant: AlertVariant.danger, + }, + ]); + }, []); + React.useEffect(() => { getReplicaSetInfo( rollout, @@ -212,11 +373,127 @@ export const Revisions: React.FC = ({ rollout, replicaSets, pods }); }, [rollout, replicaSets, analysisRuns, pods]); - const rows = getRowsDV(replicaSetInfo, replicaSetModel, rollout); + const rows = getRowsDV(replicaSetInfo, replicaSetModel, rollout, onRevisionError); return ( <> + + + + + {t('Rollout Revisions')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {alerts.map(({ key, title, message, variant }) => ( + removeAlert(key)} + actionClose={ + removeAlert(key)} /> + } + > + {message} + + ))} + {rollout.metadata && ( { , @@ -267,7 +544,7 @@ const getStatusSection = (statuses: ReplicaSetStatus[]) => { return ( {statuses.includes(ReplicaSetStatus.Stable) && ( -