Repository: openshift/console
Pull request #14842: CONSOLE-3414: Replace `Loading` with PF `Spinner`
Author: login: logonoff
Labels: name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: lgtm
description: Indicates that a PR is ready to be merged.
name: component/shared
description: Related to console-shared
name: component/core
description: Related to console core functionality
name: docs-approved
description: Signifies that Docs has signed off on this PR
name: qe-approved
description: Signifies that QE has signed off on this PR
name: px-approved
description: Signifies that Product Support has signed off on this PR
name: jira/valid-reference
description: Indicates that this PR references a valid Jira ticket of any type.
Created: 2025-03-06T15:20:28Z | Merged: 2025-03-07T11:29:34Z

https://issues.redhat.com/browse/CONSOLE-3414

before:

https://github.com/user-attachments/assets/a066dfb6-6a79-4c37-9f18-d59c764be8ca


after:

https://github.com/user-attachments/assets/2783cca2-7347-46f7-b53c-84081d0193f3


tech debt, adding labels
/label docs-approved
/label px-approved

code review:
/assign @rhamilto 

qe review:
/assign @yapei 


Diff:
diff --git a/frontend/packages/console-shared/src/components/loading/Loading.tsx b/frontend/packages/console-shared/src/components/loading/Loading.tsx
index ce899413e7e..8d17d88782e 100644
--- a/frontend/packages/console-shared/src/components/loading/Loading.tsx
+++ b/frontend/packages/console-shared/src/components/loading/Loading.tsx
@@ -1,18 +1,21 @@
 import * as React from 'react';
-import * as classNames from 'classnames';
+import { Spinner } from '@patternfly/react-core';
+import classNames from 'classnames';
 
-export const Loading: React.FCC<LoadingProps> = ({ className }) => (
-  <div
-    className={classNames('co-m-loader co-an-fade-in-out', className)}
+export const Loading: React.FC<LoadingProps> = ({ className, isInline }) => (
+  <Spinner
+    aria-live="polite"
+    aria-busy="true"
+    className={classNames('co-m-loader', { 'co-m-loader--inline': isInline }, className)}
     data-test="loading-indicator"
-  >
-    <div className="co-m-loader-dot__one" />
-    <div className="co-m-loader-dot__two" />
-    <div className="co-m-loader-dot__three" />
-  </div>
+    isInline={isInline}
+    size="lg"
+  />
 );
+
 Loading.displayName = 'Loading';
 
 type LoadingProps = {
   className?: string;
+  isInline?: boolean;
 };
diff --git a/frontend/packages/console-shared/src/components/loading/LoadingInline.tsx b/frontend/packages/console-shared/src/components/loading/LoadingInline.tsx
index 230dd511a9d..ba13780fbc8 100644
--- a/frontend/packages/console-shared/src/components/loading/LoadingInline.tsx
+++ b/frontend/packages/console-shared/src/components/loading/LoadingInline.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
 import { Loading } from './Loading';
 
-export const LoadingInline: React.FCC = () => <Loading className="co-m-loader--inline" />;
+export const LoadingInline: React.FCC = () => <Loading isInline />;
 LoadingInline.displayName = 'LoadingInline';
diff --git a/frontend/public/style/_common.scss b/frontend/public/style/_common.scss
index 351d619fefc..2cdae280194 100644
--- a/frontend/public/style/_common.scss
+++ b/frontend/public/style/_common.scss
@@ -436,34 +436,6 @@ dl.co-inline {
   white-space: nowrap;
 }
 
-.co-an-fade-in-out {
-  transition-duration: compact(0.2s, false, false, false, false, false, false, false, false, false);
-  transition-property: compact(
-    opacity,
-    false,
-    false,
-    false,
-    false,
-    false,
-    false,
-    false,
-    false,
-    false
-  );
-  transition-timing-function: compact(
-    linear,
-    false,
-    false,
-    false,
-    false,
-    false,
-    false,
-    false,
-    false,
-    false
-  );
-}
-
 .co-m-radio-desc {
   margin-left: 20px;
 }
@@ -522,43 +494,6 @@ dl.co-inline {
   min-width: 18px;
 }
 
-.co-m-loader-dot__one,
-.co-m-loader-dot__two,
-.co-m-loader-dot__three {
-  animation-delay: 0;
-  animation-direction: normal;
-  animation-duration: 1s;
-  animation-fill-mode: forwards;
-  animation-iteration-count: infinite;
-  animation-name: bouncedelay;
-  animation-play-state: running;
-  animation-timing-function: ease-in-out;
-  background: #419eda;
-  border-radius: 100%;
-  display: inline-block;
-  height: 6px;
-  width: 6px;
-}
-
-.co-m-loader-dot__one {
-  animation-delay: -0.32s;
-}
-
-.co-m-loader-dot__two {
-  animation-delay: -0.16s;
-}
-
-@keyframes bouncedelay {
-  0%,
-  80%,
-  100% {
-    transform: scale(0.25, 0.25);
-  }
-  40% {
-    transform: scale(1, 1);
-  }
-}
-
 .no-margin {
   margin: 0 !important;
 }
@@ -567,12 +502,6 @@ dl.co-inline {
   align-self: center !important;
 }
 
-.co-m-loader--inline {
-  display: inline-block;
-  margin: 0;
-  position: static;
-}
-
 .co-m-timeout-error {
   margin-bottom: 30px;
   margin-top: 20px;


Reviews:
user: login: logonoff
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T15:25:41Z
user: login: rhamilto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T15:34:28Z
user: login: rhamilto
body: Noticed a couple things after tagging it, so I untagged.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T15:47:04Z
user: login: logonoff
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T15:54:10Z
user: login: logonoff
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:02:55Z
user: login: rhamilto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T16:15:41Z
user: login: rhamilto
body: /lgtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T20:18:28Z

Review comments:
diff_hunk: @@ -1,18 +1,18 @@
 import * as React from 'react';
-import * as classNames from 'classnames';
+import { Spinner } from '@patternfly/react-core';
+import classNames from 'classnames';
 
-export const Loading: React.FCC<LoadingProps> = ({ className }) => (
+export const Loading: React.FCC<LoadingProps> = ({ className, isInline }) => (
   <div
-    className={classNames('co-m-loader co-an-fade-in-out', className)}
+    className={classNames('co-m-loader', { 'co-m-loader--inline': isInline }, className)}
path: frontend/packages/console-shared/src/components/loading/Loading.tsx
user: login: logonoff
body: kept this class as it's still used for testing
created_at: 2025-03-06T15:25:41Z
author_association: MEMBER
diff_hunk: @@ -522,32 +494,6 @@ dl.co-inline {
   min-width: 18px;
 }
 
-.co-m-loader-dot__one,
-.co-m-loader-dot__two,
-.co-m-loader-dot__three {
-  animation-delay: 0;
-  animation-direction: normal;
-  animation-duration: 1s;
-  animation-fill-mode: forwards;
-  animation-iteration-count: infinite;
-  animation-name: bouncedelay;
-  animation-play-state: running;
-  animation-timing-function: ease-in-out;
-  background: #419eda;
-  border-radius: 100%;
-  display: inline-block;
-  height: 6px;
-  width: 6px;
-}
-
-.co-m-loader-dot__one {
-  animation-delay: -0.32s;
-}
-
-.co-m-loader-dot__two {
-  animation-delay: -0.16s;
-}
-
 @keyframes bouncedelay {
path: frontend/public/style/_common.scss
user: login: rhamilto
body: I believe this can also be removed.
created_at: 2025-03-06T15:34:27Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,18 @@
 import * as React from 'react';
-import * as classNames from 'classnames';
+import { Spinner } from '@patternfly/react-core';
+import classNames from 'classnames';
 
-export const Loading: React.FCC<LoadingProps> = ({ className }) => (
+export const Loading: React.FCC<LoadingProps> = ({ className, isInline }) => (
path: frontend/packages/console-shared/src/components/loading/Loading.tsx
user: login: rhamilto
body: ```suggestion
export const Loading: React.FC<LoadingProps> = ({ className, isInline }) => (
```
created_at: 2025-03-06T15:45:21Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,5 @@
 import * as React from 'react';
 import { Loading } from './Loading';
 
-export const LoadingInline: React.FCC = () => <Loading className="co-m-loader--inline" />;
+export const LoadingInline: React.FCC = () => <Loading isInline />;
path: frontend/packages/console-shared/src/components/loading/LoadingInline.tsx
user: login: rhamilto
body: ```suggestion
export const LoadingInline: React.FC = () => <Loading isInline />;
```
created_at: 2025-03-06T15:45:31Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,18 @@
 import * as React from 'react';
-import * as classNames from 'classnames';
+import { Spinner } from '@patternfly/react-core';
+import classNames from 'classnames';
 
-export const Loading: React.FCC<LoadingProps> = ({ className }) => (
+export const Loading: React.FCC<LoadingProps> = ({ className, isInline }) => (
   <div
-    className={classNames('co-m-loader co-an-fade-in-out', className)}
+    className={classNames('co-m-loader', { 'co-m-loader--inline': isInline }, className)}
     data-test="loading-indicator"
   >
-    <div className="co-m-loader-dot__one" />
-    <div className="co-m-loader-dot__two" />
-    <div className="co-m-loader-dot__three" />
+    <Spinner aria-label="Loading" size="lg" isInline={isInline} />
path: frontend/packages/console-shared/src/components/loading/Loading.tsx
user: login: rhamilto
body: Probably should i18n the `aria-label` text.
created_at: 2025-03-06T15:46:30Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,5 @@
 import * as React from 'react';
 import { Loading } from './Loading';
 
-export const LoadingInline: React.FCC = () => <Loading className="co-m-loader--inline" />;
+export const LoadingInline: React.FCC = () => <Loading isInline />;
path: frontend/packages/console-shared/src/components/loading/LoadingInline.tsx
user: login: logonoff
body: FCC still looks appropriate as it's a custom type @TheRealJon wrote for `React.FC` that don't have children
created_at: 2025-03-06T15:54:10Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,18 @@
 import * as React from 'react';
-import * as classNames from 'classnames';
+import { Spinner } from '@patternfly/react-core';
+import classNames from 'classnames';
 
-export const Loading: React.FCC<LoadingProps> = ({ className }) => (
+export const Loading: React.FCC<LoadingProps> = ({ className, isInline }) => (
   <div
-    className={classNames('co-m-loader co-an-fade-in-out', className)}
+    className={classNames('co-m-loader', { 'co-m-loader--inline': isInline }, className)}
     data-test="loading-indicator"
   >
-    <div className="co-m-loader-dot__one" />
-    <div className="co-m-loader-dot__two" />
-    <div className="co-m-loader-dot__three" />
+    <Spinner aria-label="Loading" size="lg" isInline={isInline} />
path: frontend/packages/console-shared/src/components/loading/Loading.tsx
user: login: logonoff
body: Looks like React doesn't like it when I use a React hook inside of a Suspense loading component, so I've used `i18next.t` here instead
created_at: 2025-03-06T16:02:55Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,5 @@
 import * as React from 'react';
 import { Loading } from './Loading';
 
-export const LoadingInline: React.FCC = () => <Loading className="co-m-loader--inline" />;
+export const LoadingInline: React.FCC = () => <Loading isInline />;
path: frontend/packages/console-shared/src/components/loading/LoadingInline.tsx
user: login: rhamilto
body: Ah.  I should have investigated that.  I thought it was a typo!
created_at: 2025-03-06T16:15:41Z
author_association: MEMBER

PR comments:
user: login: openshift-ci-robot
created_at: 2025-03-06T15:20:33Z
body: @logonoff: This pull request references [CONSOLE-3414](https://issues.redhat.com//browse/CONSOLE-3414) which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.19.0" version, but no target version was set.

<details>

In response to [this](https://github.com/openshift/console/pull/14842):

>https://issues.redhat.com/browse/CONSOLE-3414
>
>before:
>
>https://github.com/user-attachments/assets/a066dfb6-6a79-4c37-9f18-d59c764be8ca
>
>
>after:
>
>https://github.com/user-attachments/assets/2783cca2-7347-46f7-b53c-84081d0193f3
>
>
>tech debt, adding labels
>/label docs-approved
>/label px-approved
>
>code review:
>/assign @rhamilto 
>
>qe review:
>/assign @yapei 


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: rhamilto
created_at: 2025-03-06T15:26:32Z
body: We have some instances of the inline variant where this change could be problematic.  For example, https://github.com/openshift/console/blob/3ed458dfe7b2e489fb2b1c644f2d6831c37e755f/frontend/packages/console-app/src/components/nodes/NodeLogs.tsx#L123
author_association: MEMBER
user: login: rhamilto
created_at: 2025-03-06T15:30:22Z
body: > We have some instances of the inline variant where this change could be problematic. For example,
> 
> https://github.com/openshift/console/blob/3ed458dfe7b2e489fb2b1c644f2d6831c37e755f/frontend/packages/console-app/src/components/nodes/NodeLogs.tsx#L123

Never mind.  I see that PatternFly accounts for this with the `isInline` prop.
author_association: MEMBER
user: login: rhamilto
created_at: 2025-03-06T15:44:38Z
body: /approve
/lgtm
author_association: MEMBER
user: login: rhamilto
created_at: 2025-03-06T15:45:10Z
body: /lgtm cancel
author_association: MEMBER
user: login: rhamilto
created_at: 2025-03-06T16:21:11Z
body: /lgtm
author_association: MEMBER
user: login: logonoff
created_at: 2025-03-06T19:37:27Z
body: Mind taking another look @rhamilto? 

I had to remove the `arla-iabel` entirely as e2e didn't like it. I used an alternate strategy for screen readers I found in this blog post: https://dockyard.com/blog/2020/03/02/accessible-loading-indicatorswith-no-extra-elements
author_association: MEMBER
user: login: openshift-ci[bot]
created_at: 2025-03-06T20:20:13Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/openshift/console/pull/14842#" title="Author self-approved">logonoff</a>*, *<a href="https://github.com/openshift/console/pull/14842#pullrequestreview-2665525722" title="LGTM">rhamilto</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=openshift%2Fconsole).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[frontend/OWNERS](https://github.com/openshift/console/blob/main/frontend/OWNERS)~~ [rhamilto]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: XiyunZhao
created_at: 2025-03-07T06:42:27Z
body: The changes have been verified without any issues. The loading icon has been updated from three dots to a loading circle, and this change is reflected in both Light and Dark modes, as well as from both the Administrator and Developer perspectives
/label qe-approved
author_association: CONTRIBUTOR
user: login: openshift-ci-robot
created_at: 2025-03-07T06:42:32Z
body: @logonoff: This pull request references [CONSOLE-3414](https://issues.redhat.com//browse/CONSOLE-3414) which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.19.0" version, but no target version was set.

<details>

In response to [this](https://github.com/openshift/console/pull/14842):

>https://issues.redhat.com/browse/CONSOLE-3414
>
>before:
>
>https://github.com/user-attachments/assets/a066dfb6-6a79-4c37-9f18-d59c764be8ca
>
>
>after:
>
>https://github.com/user-attachments/assets/2783cca2-7347-46f7-b53c-84081d0193f3
>
>
>tech debt, adding labels
>/label docs-approved
>/label px-approved
>
>code review:
>/assign @rhamilto 
>
>qe review:
>/assign @yapei 


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Fconsole).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-03-07T11:24:42Z
body: @logonoff: all tests **passed!**


[Full PR test history](https://prow.ci.openshift.org/pr-history?org=openshift&repo=console&pr=14842). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:logonoff).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: CONTRIBUTOR
user: login: openshift-bot
created_at: 2025-03-07T15:34:38Z
body: **[ART PR BUILD NOTIFIER]**

Distgit: openshift-enterprise-console
This PR has been included in build [openshift-enterprise-console-container-v4.19.0-202503071410.p0.g5a1fe5e.assembly.stream.el9](https://brewweb.engineering.redhat.com/brew/buildinfo?buildID=3544163).
All builds following this will include this PR.
author_association: CONTRIBUTOR