Repository: red-hat-storage/odf-console
Pull request #1907: Adds support for SC and VSC distrbution to clients
Author: login: bipuladh
Labels: name: approved
name: lgtm
Created: 2025-03-17T06:07:37Z | Merged: 2025-03-18T11:34:22Z

Screenshots: 
![Screenshot 2025-03-17 at 11 14 31 AM](https://github.com/user-attachments/assets/6ac942eb-7d12-400c-b9b6-4ec7333a35be)
![Screenshot 2025-03-17 at 11 33 54 AM](https://github.com/user-attachments/assets/130b6aa8-dd1a-47f2-8ef3-5e3a091c2cbe)



Diff:
diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json
index 9e9799663..ea1b7fcb7 100644
--- a/locales/en/plugin__odf-console.json
+++ b/locales/en/plugin__odf-console.json
@@ -1614,6 +1614,7 @@
   "Preserves any previous object versions. Changes will be applied to newly created objects.": "Preserves any previous object versions. Changes will be applied to newly created objects.",
   "Enable": "Enable",
   "Suspend": "Suspend",
+  "Manage distribution of Storage clients": "Manage distribution of Storage clients",
   "Full deployment": "Full deployment",
   "MultiCloud Object Gateway": "MultiCloud Object Gateway",
   "Provider Mode": "Provider Mode",
@@ -1747,7 +1748,6 @@
   "Infrastructures": "Infrastructures",
   "Subscriptions": "Subscriptions",
   "Project": "Project",
-  "Suspended": "Suspended",
   "Composable table": "Composable table",
   "Selectable table": "Selectable table",
   "Select all": "Select all",
@@ -1802,5 +1802,6 @@
   "Cannot change resource name (original: \"{{name}}\", updated: \"{{newName}}\").": "Cannot change resource name (original: \"{{name}}\", updated: \"{{newName}}\").",
   "Cannot change resource namespace (original: \"{{namespace}}\", updated: \"{{newNamespace}}\").": "Cannot change resource namespace (original: \"{{namespace}}\", updated: \"{{newNamespace}}\").",
   "Cannot change resource kind (original: \"{{original}}\", updated: \"{{updated}}\").": "Cannot change resource kind (original: \"{{original}}\", updated: \"{{updated}}\").",
-  "Cannot change API group (original: \"{{apiGroup}}\", updated: \"{{newAPIGroup}}\").": "Cannot change API group (original: \"{{apiGroup}}\", updated: \"{{newAPIGroup}}\")."
+  "Cannot change API group (original: \"{{apiGroup}}\", updated: \"{{newAPIGroup}}\").": "Cannot change API group (original: \"{{apiGroup}}\", updated: \"{{newAPIGroup}}\").",
+  "Suspended": "Suspended"
 }
diff --git a/packages/odf/components/ResourceDistribution/utils.ts b/packages/odf/components/ResourceDistribution/utils.ts
index 04e73c746..45aacc7de 100644
--- a/packages/odf/components/ResourceDistribution/utils.ts
+++ b/packages/odf/components/ResourceDistribution/utils.ts
@@ -8,14 +8,14 @@ export const generatePatchForDistributionOfResources = (
 ): Patch[] => {
   const patches: Patch[] = [];
   const currentlySelectedStorageClasses =
-    storageConsumer.spec.storageClasses.map((sc) => sc.name);
+    storageConsumer.spec?.storageClasses?.map((sc) => sc.name) || [];
   const currentlySelectedVolumeSnapshotClasses =
-    storageConsumer.spec.volumeSnapshotClasses.map((vsc) => vsc.name);
+    storageConsumer.spec?.volumeSnapshotClasses?.map((vsc) => vsc.name) || [];
   const isStorageClassesChanged = storageClassNames.every((res) =>
     currentlySelectedStorageClasses.includes(res)
   );
-  const isVolumeSnapshotClassesChanged = volumeSnapshotClassNames.every((res) =>
-    currentlySelectedVolumeSnapshotClasses.includes(res)
+  const isVolumeSnapshotClassesChanged = volumeSnapshotClassNames?.every(
+    (res) => currentlySelectedVolumeSnapshotClasses?.includes(res)
   );
 
   if (!isStorageClassesChanged) {
@@ -26,7 +26,7 @@ export const generatePatchForDistributionOfResources = (
       value: storageClassNames.map((name) => ({ name })),
     });
   }
-  if (!storageConsumer.spec.storageClasses.length) {
+  if (!storageConsumer.spec?.storageClasses?.length) {
     patches.push({
       op: 'add',
       path: '/spec/storageClasses/-',
@@ -41,7 +41,7 @@ export const generatePatchForDistributionOfResources = (
       value: volumeSnapshotClassNames.map((name) => ({ name })),
     });
   }
-  if (!storageConsumer.spec.storageClasses.length) {
+  if (!storageConsumer.spec?.storageClasses?.length) {
     patches.push({
       op: 'add',
       path: '/spec/volumeSnapshotClasses/-',
diff --git a/packages/odf/components/odf-dashboard/status-card/utils.ts b/packages/odf/components/odf-dashboard/status-card/utils.ts
index 6c6d90698..c942815fb 100644
--- a/packages/odf/components/odf-dashboard/status-card/utils.ts
+++ b/packages/odf/components/odf-dashboard/status-card/utils.ts
@@ -5,11 +5,11 @@ import { TFunction } from 'react-i18next';
 
 const getHealthAndTotalClientCounts = (clients: StorageConsumerKind[]) => {
   const connectedClients = clients.filter(
-    (client) => client.status.state === StorageConsumerState.Ready
+    (client) => client.status?.state === StorageConsumerState.Ready
   );
 
   const healthyClients = connectedClients.filter(
-    (client) => getTimeDifferenceInSeconds(client.status.lastHeartbeat) < 120
+    (client) => getTimeDifferenceInSeconds(client.status?.lastHeartbeat) < 120
   );
   const healthyClientsCount = healthyClients.length;
   const totalClientsCount = clients.length;
diff --git a/packages/odf/components/storage-consumers/client-list.tsx b/packages/odf/components/storage-consumers/client-list.tsx
index 9ec1fd7b2..04b63feaf 100644
--- a/packages/odf/components/storage-consumers/client-list.tsx
+++ b/packages/odf/components/storage-consumers/client-list.tsx
@@ -58,6 +58,18 @@ import { StorageQuotaUtilizationProgress } from './QuotaUtilizationProgress';
 import { RotateKeysModal } from './rotate-keys-modal';
 import './client-list.scss';
 
+export const getClusterName = (client: StorageConsumerKind) => {
+  const clusterName = client.status?.client?.clusterName;
+  const clientClusterId = client.status?.client?.clusterId;
+  let name = '';
+  if (!clusterName && !clientClusterId) {
+    name = '-';
+  } else {
+    name = `${clusterName || '-'} (${clientClusterId || '-'})`;
+  }
+  return name;
+};
+
 const StorageQuotaPopoverContent: React.FC = () => {
   const { t } = useCustomTranslation();
 
@@ -240,7 +252,7 @@ const getDataFoundationVersion = (obj: StorageConsumerKind) =>
 type LastHeartBeatProps = {
   heartbeat: string;
 };
-const LastHeartBeat: React.FC<LastHeartBeatProps> = ({ heartbeat }) => {
+export const LastHeartBeat: React.FC<LastHeartBeatProps> = ({ heartbeat }) => {
   const { t } = useCustomTranslation();
   const difference = getTimeDifferenceInSeconds(heartbeat);
   const Component = (() => {
@@ -360,9 +372,7 @@ const StorageClientRow: React.FC<
             data = obj?.status?.client?.name || '-';
             break;
           case 'clusterName':
-            data = `${obj?.status?.client?.clusterName || '-'} (${
-              clientClusterId || '-'
-            })`;
+            data = getClusterName(obj);
             break;
           case 'storageQuota':
             data = humanizedStorageQuota;
diff --git a/packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherModal.tsx b/packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherModal.tsx
new file mode 100644
index 000000000..83cd47a26
--- /dev/null
+++ b/packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherModal.tsx
@@ -0,0 +1,168 @@
+import * as React from 'react';
+import { StorageConsumerModel } from '@odf/core/models';
+import {
+  getName,
+  ModalFooter,
+  RowComponentType,
+  SelectableTable,
+  StorageClassModel,
+  StorageClassResourceKind,
+  StorageConsumerKind,
+  useCustomTranslation,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import { CommonModalProps } from '@odf/shared/modals';
+import { sortRows } from '@odf/shared/utils';
+import {
+  K8sModel,
+  k8sPatch,
+  ListPageFilter,
+  useK8sWatchResource,
+  useListPageFilter,
+  WatchK8sResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { Button, Flex, FlexItem, Modal } from '@patternfly/react-core';
+import { Td } from '@patternfly/react-table';
+import { generatePatchForDistributionOfResources } from '../../components/ResourceDistribution/utils';
+import {
+  getClusterName,
+  LastHeartBeat,
+} from '../../components/storage-consumers/client-list';
+import './storageClientAttacherComponent.scss';
+
+const RowComponent: React.FC<RowComponentType<StorageConsumerKind>> = ({
+  row: client,
+}) => {
+  const name = getName(client);
+  const clusterName = getClusterName(client);
+  return (
+    <>
+      <Td>{name}</Td>
+      <Td>{clusterName}</Td>
+      <Td>
+        <LastHeartBeat heartbeat={client.status?.lastHeartbeat} />{' '}
+      </Td>
+    </>
+  );
+};
+
+const storageConsumerResource: WatchK8sResource = {
+  isList: true,
+  groupVersionKind: {
+    group: StorageConsumerModel.apiGroup,
+    version: StorageConsumerModel.apiVersion,
+    kind: StorageConsumerModel.kind,
+  },
+};
+
+type StorageClientAttacherModalProps = {
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind;
+  resourceModel: K8sModel;
+};
+
+export const StorageClientAttacherModal: ModalComponent<
+  CommonModalProps<StorageClientAttacherModalProps>
+> = ({ closeModal, isOpen, extraProps: { resource, resourceModel } }) => {
+  const resourceName = getName(resource);
+  const [inProgress, setProgress] = React.useState(false);
+  const [error, setError] = React.useState('');
+
+  const [selectedResources, setSelectedResources] = React.useState<
+    StorageConsumerKind[]
+  >([]);
+  const { t } = useCustomTranslation();
+  const [storageConsumers, loaded] = useK8sWatchResource<StorageConsumerKind[]>(
+    storageConsumerResource
+  );
+  const [unfilteredData, filteredData, onFilterChange] =
+    useListPageFilter(storageConsumers);
+
+  const onSubmit = React.useCallback(() => {
+    const promises = [];
+    setProgress(true);
+    const storageClassNames =
+      resourceModel.kind === StorageClassModel.kind ? [resourceName] : [];
+    const volumeSnapshotClassNames =
+      resourceModel.kind === VolumeSnapshotClassModel.kind
+        ? [resourceName]
+        : [];
+    selectedResources.forEach((storageConsumer) => {
+      const patch = generatePatchForDistributionOfResources(
+        storageConsumer,
+        storageClassNames,
+        volumeSnapshotClassNames
+      );
+      promises.push(
+        k8sPatch({
+          model: StorageConsumerModel,
+          resource: storageConsumer,
+          data: patch,
+        })
+      );
+    });
+    Promise.all(promises)
+      .then(() => {
+        setProgress(false);
+        closeModal();
+      })
+      .catch((err) => {
+        setProgress(false);
+        setError(err?.message);
+      });
+  }, [closeModal, resourceModel.kind, resourceName, selectedResources]);
+
+  const columns = [
+    {
+      columnName: t('Name'),
+      sortFunction: (a, b, c) => sortRows(a, b, c, 'metadata.name'),
+    },
+    {
+      columnName: t('Cluster name'),
+      sortFunction: (a, b, c) => sortRows(a, b, c, 'status.client.clusterName'),
+    },
+    {
+      columnName: t('Last heartbeat'),
+      sortFunction: (a, b, c) => sortRows(a, b, c, 'status.lastHeartbeat'),
+    },
+  ];
+  return (
+    <Modal
+      isOpen={isOpen}
+      onClose={closeModal}
+      title={t('Manage distribution of Storage clients')}
+      variant="medium"
+    >
+      <ListPageFilter
+        data={unfilteredData}
+        loaded={loaded}
+        onFilterChange={onFilterChange}
+        hideColumnManagement={true}
+      />
+      <SelectableTable<StorageConsumerKind>
+        rows={filteredData}
+        columns={columns}
+        RowComponent={RowComponent}
+        selectedRows={selectedResources}
+        setSelectedRows={setSelectedResources}
+        loaded={loaded}
+        loadError={error}
+      />
+      <ModalFooter inProgress={inProgress} errorMessage={error}>
+        <Flex direction={{ default: 'row' }}>
+          <FlexItem>
+            <Button key="Cancel" variant="secondary" onClick={closeModal}>
+              {t('Cancel')}
+            </Button>
+          </FlexItem>
+          <FlexItem>
+            <Button key="Confirm" variant="primary" onClick={onSubmit}>
+              {t('Save changes')}
+            </Button>
+          </FlexItem>
+        </Flex>
+      </ModalFooter>
+    </Modal>
+  );
+};
diff --git a/packages/odf/modals/StorageClientDistributionModal/storageClientAttacherComponent.scss b/packages/odf/modals/StorageClientDistributionModal/storageClientAttacherComponent.scss
new file mode 100644
index 000000000..d45664e02
--- /dev/null
+++ b/packages/odf/modals/StorageClientDistributionModal/storageClientAttacherComponent.scss
@@ -0,0 +1,6 @@
+.odf-storage-class-distribute {
+    margin: 0.5rem 0 0.5rem 0;
+    &__checkbox {
+        margin: 0.5rem 0 0.5rem 0;
+    }
+}
diff --git a/packages/odf/modals/StorageClientDistributionModal/useStorageClientAttacherAction.ts b/packages/odf/modals/StorageClientDistributionModal/useStorageClientAttacherAction.ts
new file mode 100644
index 000000000..f5fce02d2
--- /dev/null
+++ b/packages/odf/modals/StorageClientDistributionModal/useStorageClientAttacherAction.ts
@@ -0,0 +1,78 @@
+import { useMemo } from 'react';
+import {
+  getDriver,
+  getProvisioner,
+  StorageClassModel,
+  StorageClassResourceKind,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import { isCephDriver, isCephProvisioner } from '@odf/shared/utils';
+import {
+  Action,
+  K8sModel,
+  useModal,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { LaunchModal } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { StorageClientAttacherModal } from './StorageClientAttacherModal';
+
+const isStorageClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is StorageClassResourceKind =>
+  resource.kind === StorageClassModel.kind;
+
+const isVolumeSnapshotClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is VolumeSnapshotClassKind =>
+  resource.kind === VolumeSnapshotClassModel.kind;
+
+export const useStorageClientAttacherAction = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+) => {
+  const launchModal = useModal();
+  const actions = useMemo(() => {
+    const items = [];
+    if (
+      isStorageClass(resource) &&
+      isCephProvisioner(getProvisioner(resource))
+    ) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          StorageClassModel,
+          launchModal
+        )
+      );
+    }
+    if (isVolumeSnapshotClass(resource) && isCephDriver(getDriver(resource))) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          VolumeSnapshotClassModel,
+          launchModal
+        )
+      );
+    }
+    return [items, true, undefined];
+  }, [resource, launchModal]);
+  return actions;
+};
+
+const AttachResourceToStorageClientModal = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind,
+  resourceModel: K8sModel,
+  launchModal: LaunchModal
+): Action => {
+  return {
+    id: 'attach-resources',
+    label: 'Distribute to storage clients',
+    insertBefore: 'edit-labels',
+    cta: () => {
+      launchModal(StorageClientAttacherModal, {
+        extraProps: { resource, resourceModel },
+        isOpen: true,
+        closeModal: null,
+      });
+    },
+  };
+};
diff --git a/packages/shared/src/selectors/index.ts b/packages/shared/src/selectors/index.ts
index 5729c893d..b7c2a13dd 100644
--- a/packages/shared/src/selectors/index.ts
+++ b/packages/shared/src/selectors/index.ts
@@ -1,3 +1,4 @@
 export * from './k8s';
 export * from './Node';
 export * from './storageClasses';
+export * from './volumeSnapshots';
diff --git a/packages/shared/src/selectors/volumeSnapshots.ts b/packages/shared/src/selectors/volumeSnapshots.ts
new file mode 100644
index 000000000..8b58ee0f0
--- /dev/null
+++ b/packages/shared/src/selectors/volumeSnapshots.ts
@@ -0,0 +1,4 @@
+import { VolumeSnapshotClassKind } from '../types';
+
+export const getDriver = (resource: VolumeSnapshotClassKind) =>
+  resource?.driver;
diff --git a/packages/shared/src/utils/storage.ts b/packages/shared/src/utils/storage.ts
index 7ccaadbe5..b003fc746 100644
--- a/packages/shared/src/utils/storage.ts
+++ b/packages/shared/src/utils/storage.ts
@@ -33,3 +33,6 @@ export const isCephProvisioner = (scProvisioner: string): boolean => {
     _.endsWith(scProvisioner, provisioner)
   );
 };
+
+export const isCephDriver = (driver: string): boolean =>
+  _.endsWith(driver, 'ceph.com');
diff --git a/plugins/odf/console-extensions.json b/plugins/odf/console-extensions.json
index b93dbc161..f27d6a3f4 100644
--- a/plugins/odf/console-extensions.json
+++ b/plugins/odf/console-extensions.json
@@ -751,5 +751,31 @@
       "exact": false,
       "component": { "$codeRef": "attachStorage.default" }
     }
+  },
+  {
+    "type": "console.action/resource-provider",
+    "properties": {
+      "model": {
+        "version": "v1",
+        "kind": "StorageClass",
+        "group": "storage.k8s.io"
+      },
+      "provider": {
+        "$codeRef": "storageClientAttacherAction.useStorageClientAttacherAction"
+      }
+    }
+  },
+  {
+    "type": "console.action/resource-provider",
+    "properties": {
+      "model": {
+        "version": "v1",
+        "kind": "VolumeSnapshotClass",
+        "group": "snapshot.storage.k8s.io"
+      },
+      "provider": {
+        "$codeRef": "storageClientAttacherAction.useStorageClientAttacherAction"
+      }
+    }
   }
 ]
diff --git a/plugins/odf/console-plugin.json b/plugins/odf/console-plugin.json
index 69631c06c..170a1f7ce 100644
--- a/plugins/odf/console-plugin.json
+++ b/plugins/odf/console-plugin.json
@@ -45,6 +45,7 @@
     "s3BucketOverview": "@odf/core/components/s3-browser/bucket-overview/BucketOverview",
     "s3BucketList": "@odf/core/components/s3-browser/buckets-list-page/bucketsListPage",
     "s3BucketLifecycleRule": "@odf/core/components/s3-browser/create-or-edit-lifecycle-rules/CreateOrEditLifecycleRule",
-    "attachStorage": "@odf/core/components/attach-storage-storagesystem/attach-storage"
+    "attachStorage": "@odf/core/components/attach-storage-storagesystem/attach-storage",
+    "storageClientAttacherAction": "@odf/core/modals/StorageClientDistributionModal/useStorageClientAttacherAction"
   }
 }


Reviews:
user: login: vbnrh
body: The Modal looks extremely stretched across the screen for some reason. Maybe its an ultrawide monitor ?

Would prefer if you could set some limits to the width on large screens.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-17T13:27:10Z
user: login: bipuladh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T13:45:08Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T15:42:05Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T15:42:17Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T15:44:55Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T15:45:35Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T15:47:46Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T15:48:43Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T15:57:53Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T15:59:20Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T16:03:12Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T16:04:22Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T16:05:02Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T16:07:09Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T16:07:51Z
user: login: SanjalKatiyar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T16:09:13Z
user: login: bipuladh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:30:51Z
user: login: bipuladh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:38:20Z
user: login: bipuladh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:38:35Z
user: login: bipuladh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:39:29Z
user: login: bipuladh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:44:06Z
user: login: bipuladh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:44:11Z

Review comments:
diff_hunk: @@ -0,0 +1,30 @@
+import * as React from 'react';
+
+const useInterceptSubmit = () => {
+  const callbackRef = React.useRef(null);
+
+  React.useEffect(() => {
+    const handleClick = () => {
+      if (callbackRef.current) {
+        callbackRef.current(); // Execute the stored callback
+      }
+    };
+
+    const button = document.getElementById('save-changes');
path: packages/shared/src/hooks/useMouseEventListener.ts
user: login: vbnrh
body: Is this ID added anywhere ? I cannot see it on the button where the hook is being used
created_at: 2025-03-17T13:00:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,173 @@
+import * as React from 'react';
+import { StorageConsumerModel } from '@odf/core/models';
+import {
+  getName,
+  ModalFooter,
+  RowComponentType,
+  SelectableTable,
+  StorageClassModel,
+  StorageClassResourceKind,
+  StorageConsumerKind,
+  useCustomTranslation,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import useInterceptSubmit from '@odf/shared/hooks/useMouseEventListener';
+import { CommonModalProps } from '@odf/shared/modals';
+import { sortRows } from '@odf/shared/utils';
+import {
+  K8sModel,
+  k8sPatch,
+  ListPageFilter,
+  useK8sWatchResource,
+  useListPageFilter,
+  WatchK8sResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { Button, Flex, FlexItem, Modal } from '@patternfly/react-core';
+import { Td } from '@patternfly/react-table';
+import { generatePatchForDistributionOfResources } from '../../components/ResourceDistribution/utils';
+import { LastHeartBeat } from '../../components/storage-consumers/client-list';
+import './storageClientAttacherComponent.scss';
+
+const RowComponent: React.FC<RowComponentType<StorageConsumerKind>> = ({
+  row: client,
+}) => {
+  const name = getName(client);
+  const clientClusterId = client?.status?.client?.clusterId;
+  const clusterName = `${client?.status?.client?.clusterName || '-'} (${
+    clientClusterId || '-'
+  })`;
+  return (
+    <>
+      <Td>{name}</Td>
+      <Td>{clusterName}</Td>
+      <Td>
+        <LastHeartBeat heartbeat={client?.status?.lastHeartbeat} />{' '}
+      </Td>
+    </>
+  );
+};
+
+const storageConsumerResource: WatchK8sResource = {
+  isList: true,
+  groupVersionKind: {
+    group: StorageConsumerModel.apiGroup,
+    version: StorageConsumerModel.apiVersion,
+    kind: StorageConsumerModel.kind,
+  },
+};
+
+type StorageClientAttacherModalProps = {
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind;
+  resourceModel: K8sModel;
+};
+
+export const StorageClientAttacherModal: ModalComponent<
+  CommonModalProps<StorageClientAttacherModalProps>
+> = ({ closeModal, isOpen, extraProps: { resource, resourceModel } }) => {
+  const setCallback = useInterceptSubmit();
+  const resourceName = getName(resource);
+  const [inProgress, setProgress] = React.useState(false);
+  const [error, setError] = React.useState('');
+
+  const [selectedResources, setSelectedResources] = React.useState<
+    StorageConsumerKind[]
+  >([]);
+  const { t } = useCustomTranslation();
+  const [storageConsumers, loaded] = useK8sWatchResource<StorageConsumerKind[]>(
+    storageConsumerResource
+  );
+  const [unfilteredData, filteredData, onFilterChange] =
+    useListPageFilter(storageConsumers);
+
+  const onSubmit = React.useCallback(() => {
+    const promises = [];
+    setProgress(true);
+    const storageClassNames =
+      resourceModel.kind === StorageClassModel.kind ? [resourceName] : [];
+    const volumeSnapshotClassNames =
+      resourceModel.kind === VolumeSnapshotClassModel.kind
+        ? [resourceName]
+        : [];
+    selectedResources.forEach((storageConsumer) => {
+      const patch = generatePatchForDistributionOfResources(
+        storageConsumer,
+        storageClassNames,
+        volumeSnapshotClassNames
+      );
+      promises.push(
+        k8sPatch({
+          model: StorageConsumerModel,
+          resource: storageConsumer,
+          data: patch,
+        })
+      );
+    });
+    Promise.all(promises)
+      .then(() => {
+        setProgress(false);
+        closeModal();
+      })
+      .catch((err) => {
+        setProgress(false);
+        setError(err?.message);
+      });
+  }, [closeModal, resourceModel.kind, resourceName, selectedResources]);
+
+  React.useEffect(() => {
+    setCallback(onSubmit);
path: packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherComponent.tsx
user: login: vbnrh
body: What value does this provide over just using the onSubmit directly on click? If i am not wrong onSubmit already is memoized with useCallback
created_at: 2025-03-17T13:14:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,173 @@
+import * as React from 'react';
+import { StorageConsumerModel } from '@odf/core/models';
+import {
+  getName,
+  ModalFooter,
+  RowComponentType,
+  SelectableTable,
+  StorageClassModel,
+  StorageClassResourceKind,
+  StorageConsumerKind,
+  useCustomTranslation,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import useInterceptSubmit from '@odf/shared/hooks/useMouseEventListener';
+import { CommonModalProps } from '@odf/shared/modals';
+import { sortRows } from '@odf/shared/utils';
+import {
+  K8sModel,
+  k8sPatch,
+  ListPageFilter,
+  useK8sWatchResource,
+  useListPageFilter,
+  WatchK8sResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { Button, Flex, FlexItem, Modal } from '@patternfly/react-core';
+import { Td } from '@patternfly/react-table';
+import { generatePatchForDistributionOfResources } from '../../components/ResourceDistribution/utils';
+import { LastHeartBeat } from '../../components/storage-consumers/client-list';
+import './storageClientAttacherComponent.scss';
+
+const RowComponent: React.FC<RowComponentType<StorageConsumerKind>> = ({
+  row: client,
+}) => {
+  const name = getName(client);
+  const clientClusterId = client?.status?.client?.clusterId;
+  const clusterName = `${client?.status?.client?.clusterName || '-'} (${
+    clientClusterId || '-'
+  })`;
path: packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherComponent.tsx
user: login: vbnrh
body: ```suggestion
const clientClusterId = client?.status?.client?.clusterId || '-';
const clusterName = client?.status?.client?.clusterName 
  ? `${client.status.client.clusterName} (${clientClusterId})`
  : '-';
```
Cleaner 
created_at: 2025-03-17T13:24:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,173 @@
+import * as React from 'react';
+import { StorageConsumerModel } from '@odf/core/models';
+import {
+  getName,
+  ModalFooter,
+  RowComponentType,
+  SelectableTable,
+  StorageClassModel,
+  StorageClassResourceKind,
+  StorageConsumerKind,
+  useCustomTranslation,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import useInterceptSubmit from '@odf/shared/hooks/useMouseEventListener';
+import { CommonModalProps } from '@odf/shared/modals';
+import { sortRows } from '@odf/shared/utils';
+import {
+  K8sModel,
+  k8sPatch,
+  ListPageFilter,
+  useK8sWatchResource,
+  useListPageFilter,
+  WatchK8sResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { Button, Flex, FlexItem, Modal } from '@patternfly/react-core';
+import { Td } from '@patternfly/react-table';
+import { generatePatchForDistributionOfResources } from '../../components/ResourceDistribution/utils';
+import { LastHeartBeat } from '../../components/storage-consumers/client-list';
+import './storageClientAttacherComponent.scss';
+
+const RowComponent: React.FC<RowComponentType<StorageConsumerKind>> = ({
+  row: client,
+}) => {
+  const name = getName(client);
+  const clientClusterId = client?.status?.client?.clusterId;
+  const clusterName = `${client?.status?.client?.clusterName || '-'} (${
+    clientClusterId || '-'
+  })`;
path: packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherComponent.tsx
user: login: vbnrh
body: Having a single '-' should be enough to indicate a nil val
created_at: 2025-03-17T13:24:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,30 @@
+import * as React from 'react';
+
+const useInterceptSubmit = () => {
+  const callbackRef = React.useRef(null);
+
+  React.useEffect(() => {
+    const handleClick = () => {
+      if (callbackRef.current) {
+        callbackRef.current(); // Execute the stored callback
+      }
+    };
+
+    const button = document.getElementById('save-changes');
path: packages/shared/src/hooks/useMouseEventListener.ts
user: login: bipuladh
body: Thanks for pointing out. This hook is not used anymore
created_at: 2025-03-17T13:45:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,11 +5,11 @@ import { TFunction } from 'react-i18next';
 
 const getHealthAndTotalClientCounts = (clients: StorageConsumerKind[]) => {
   const connectedClients = clients.filter(
-    (client) => client.status.state === StorageConsumerState.Ready
+    (client) => client?.status?.state === StorageConsumerState.Ready
path: packages/odf/components/odf-dashboard/status-card/utils.ts
user: login: SanjalKatiyar
body: nits:
```suggestion
    (client) => client.status?.state === StorageConsumerState.Ready
```
created_at: 2025-03-17T15:42:04Z
author_association: COLLABORATOR
diff_hunk: @@ -5,11 +5,11 @@ import { TFunction } from 'react-i18next';
 
 const getHealthAndTotalClientCounts = (clients: StorageConsumerKind[]) => {
   const connectedClients = clients.filter(
-    (client) => client.status.state === StorageConsumerState.Ready
+    (client) => client?.status?.state === StorageConsumerState.Ready
   );
 
   const healthyClients = connectedClients.filter(
-    (client) => getTimeDifferenceInSeconds(client.status.lastHeartbeat) < 120
+    (client) => getTimeDifferenceInSeconds(client?.status?.lastHeartbeat) < 120
path: packages/odf/components/odf-dashboard/status-card/utils.ts
user: login: SanjalKatiyar
body: nits:
```suggestion
    (client) => getTimeDifferenceInSeconds(client.status?.lastHeartbeat) < 120
```
created_at: 2025-03-17T15:42:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,173 @@
+import * as React from 'react';
+import { StorageConsumerModel } from '@odf/core/models';
+import {
+  getName,
+  ModalFooter,
+  RowComponentType,
+  SelectableTable,
+  StorageClassModel,
+  StorageClassResourceKind,
+  StorageConsumerKind,
+  useCustomTranslation,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import useInterceptSubmit from '@odf/shared/hooks/useMouseEventListener';
+import { CommonModalProps } from '@odf/shared/modals';
+import { sortRows } from '@odf/shared/utils';
+import {
+  K8sModel,
+  k8sPatch,
+  ListPageFilter,
+  useK8sWatchResource,
+  useListPageFilter,
+  WatchK8sResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { Button, Flex, FlexItem, Modal } from '@patternfly/react-core';
+import { Td } from '@patternfly/react-table';
+import { generatePatchForDistributionOfResources } from '../../components/ResourceDistribution/utils';
+import { LastHeartBeat } from '../../components/storage-consumers/client-list';
+import './storageClientAttacherComponent.scss';
+
+const RowComponent: React.FC<RowComponentType<StorageConsumerKind>> = ({
+  row: client,
+}) => {
+  const name = getName(client);
+  const clientClusterId = client?.status?.client?.clusterId;
+  const clusterName = `${client?.status?.client?.clusterName || '-'} (${
+    clientClusterId || '-'
+  })`;
path: packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherComponent.tsx
user: login: SanjalKatiyar
body: nits (I guess `client` itself can never be undefined or null):
```suggestion
  const clientClusterId = client.status?.client?.clusterId;
  const clusterName = `${client.status?.client?.clusterName || '-'} (${
    clientClusterId || '-'
  })`;
```
created_at: 2025-03-17T15:44:54Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,173 @@
+import * as React from 'react';
+import { StorageConsumerModel } from '@odf/core/models';
+import {
+  getName,
+  ModalFooter,
+  RowComponentType,
+  SelectableTable,
+  StorageClassModel,
+  StorageClassResourceKind,
+  StorageConsumerKind,
+  useCustomTranslation,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import useInterceptSubmit from '@odf/shared/hooks/useMouseEventListener';
+import { CommonModalProps } from '@odf/shared/modals';
+import { sortRows } from '@odf/shared/utils';
+import {
+  K8sModel,
+  k8sPatch,
+  ListPageFilter,
+  useK8sWatchResource,
+  useListPageFilter,
+  WatchK8sResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { Button, Flex, FlexItem, Modal } from '@patternfly/react-core';
+import { Td } from '@patternfly/react-table';
+import { generatePatchForDistributionOfResources } from '../../components/ResourceDistribution/utils';
+import { LastHeartBeat } from '../../components/storage-consumers/client-list';
+import './storageClientAttacherComponent.scss';
+
+const RowComponent: React.FC<RowComponentType<StorageConsumerKind>> = ({
+  row: client,
+}) => {
+  const name = getName(client);
+  const clientClusterId = client?.status?.client?.clusterId;
+  const clusterName = `${client?.status?.client?.clusterName || '-'} (${
+    clientClusterId || '-'
+  })`;
+  return (
+    <>
+      <Td>{name}</Td>
+      <Td>{clusterName}</Td>
+      <Td>
+        <LastHeartBeat heartbeat={client?.status?.lastHeartbeat} />{' '}
path: packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherComponent.tsx
user: login: SanjalKatiyar
body: nits:
```suggestion
        <LastHeartBeat heartbeat={client.status?.lastHeartbeat} />{' '}
```
created_at: 2025-03-17T15:45:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,173 @@
+import * as React from 'react';
+import { StorageConsumerModel } from '@odf/core/models';
+import {
+  getName,
+  ModalFooter,
+  RowComponentType,
+  SelectableTable,
+  StorageClassModel,
+  StorageClassResourceKind,
+  StorageConsumerKind,
+  useCustomTranslation,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import useInterceptSubmit from '@odf/shared/hooks/useMouseEventListener';
+import { CommonModalProps } from '@odf/shared/modals';
+import { sortRows } from '@odf/shared/utils';
+import {
+  K8sModel,
+  k8sPatch,
+  ListPageFilter,
+  useK8sWatchResource,
+  useListPageFilter,
+  WatchK8sResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { Button, Flex, FlexItem, Modal } from '@patternfly/react-core';
+import { Td } from '@patternfly/react-table';
+import { generatePatchForDistributionOfResources } from '../../components/ResourceDistribution/utils';
+import { LastHeartBeat } from '../../components/storage-consumers/client-list';
+import './storageClientAttacherComponent.scss';
+
+const RowComponent: React.FC<RowComponentType<StorageConsumerKind>> = ({
+  row: client,
+}) => {
+  const name = getName(client);
+  const clientClusterId = client?.status?.client?.clusterId;
+  const clusterName = `${client?.status?.client?.clusterName || '-'} (${
+    clientClusterId || '-'
+  })`;
path: packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherComponent.tsx
user: login: SanjalKatiyar
body: nits:
this could be a util function, we are displaying it in same format in `odf/components/storage-consumers/client-list.tsx` file too.
created_at: 2025-03-17T15:47:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,30 @@
+import * as React from 'react';
+
+const useInterceptSubmit = () => {
+  const callbackRef = React.useRef(null);
+
+  React.useEffect(() => {
+    const handleClick = () => {
+      if (callbackRef.current) {
+        callbackRef.current(); // Execute the stored callback
+      }
+    };
+
+    const button = document.getElementById('save-changes');
+    if (button) {
+      button.addEventListener('click', handleClick, true);
+    }
+
+    return () => {
+      if (button) {
+        button.removeEventListener('click', handleClick);
+      }
+    };
+  }, [callbackRef]);
+
+  return (callback) => {
+    callbackRef.current = callback;
+  };
+};
+
+export default useInterceptSubmit;
path: packages/shared/src/hooks/useMouseEventListener.ts
user: login: SanjalKatiyar
body: file name is `useMouseEventListener.ts`, whereas exported component is `useInterceptSubmit`.
created_at: 2025-03-17T15:48:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,173 @@
+import * as React from 'react';
+import { StorageConsumerModel } from '@odf/core/models';
+import {
+  getName,
+  ModalFooter,
+  RowComponentType,
+  SelectableTable,
+  StorageClassModel,
+  StorageClassResourceKind,
+  StorageConsumerKind,
+  useCustomTranslation,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import useInterceptSubmit from '@odf/shared/hooks/useMouseEventListener';
+import { CommonModalProps } from '@odf/shared/modals';
+import { sortRows } from '@odf/shared/utils';
+import {
+  K8sModel,
+  k8sPatch,
+  ListPageFilter,
+  useK8sWatchResource,
+  useListPageFilter,
+  WatchK8sResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { Button, Flex, FlexItem, Modal } from '@patternfly/react-core';
+import { Td } from '@patternfly/react-table';
+import { generatePatchForDistributionOfResources } from '../../components/ResourceDistribution/utils';
+import { LastHeartBeat } from '../../components/storage-consumers/client-list';
+import './storageClientAttacherComponent.scss';
+
+const RowComponent: React.FC<RowComponentType<StorageConsumerKind>> = ({
+  row: client,
+}) => {
+  const name = getName(client);
+  const clientClusterId = client?.status?.client?.clusterId;
+  const clusterName = `${client?.status?.client?.clusterName || '-'} (${
+    clientClusterId || '-'
+  })`;
+  return (
+    <>
+      <Td>{name}</Td>
+      <Td>{clusterName}</Td>
+      <Td>
+        <LastHeartBeat heartbeat={client?.status?.lastHeartbeat} />{' '}
+      </Td>
+    </>
+  );
+};
+
+const storageConsumerResource: WatchK8sResource = {
+  isList: true,
+  groupVersionKind: {
+    group: StorageConsumerModel.apiGroup,
+    version: StorageConsumerModel.apiVersion,
+    kind: StorageConsumerModel.kind,
+  },
+};
+
+type StorageClientAttacherModalProps = {
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind;
+  resourceModel: K8sModel;
+};
+
+export const StorageClientAttacherModal: ModalComponent<
+  CommonModalProps<StorageClientAttacherModalProps>
+> = ({ closeModal, isOpen, extraProps: { resource, resourceModel } }) => {
+  const setCallback = useInterceptSubmit();
+  const resourceName = getName(resource);
+  const [inProgress, setProgress] = React.useState(false);
+  const [error, setError] = React.useState('');
+
+  const [selectedResources, setSelectedResources] = React.useState<
+    StorageConsumerKind[]
+  >([]);
+  const { t } = useCustomTranslation();
+  const [storageConsumers, loaded] = useK8sWatchResource<StorageConsumerKind[]>(
+    storageConsumerResource
+  );
+  const [unfilteredData, filteredData, onFilterChange] =
+    useListPageFilter(storageConsumers);
+
+  const onSubmit = React.useCallback(() => {
+    const promises = [];
+    setProgress(true);
+    const storageClassNames =
+      resourceModel.kind === StorageClassModel.kind ? [resourceName] : [];
+    const volumeSnapshotClassNames =
+      resourceModel.kind === VolumeSnapshotClassModel.kind
+        ? [resourceName]
+        : [];
+    selectedResources.forEach((storageConsumer) => {
+      const patch = generatePatchForDistributionOfResources(
+        storageConsumer,
+        storageClassNames,
+        volumeSnapshotClassNames
+      );
+      promises.push(
+        k8sPatch({
+          model: StorageConsumerModel,
+          resource: storageConsumer,
+          data: patch,
+        })
+      );
+    });
+    Promise.all(promises)
+      .then(() => {
+        setProgress(false);
+        closeModal();
+      })
+      .catch((err) => {
+        setProgress(false);
+        setError(err?.message);
+      });
+  }, [closeModal, resourceModel.kind, resourceName, selectedResources]);
+
+  React.useEffect(() => {
+    setCallback(onSubmit);
+  }, [onSubmit, setCallback]);
+
+  const columns = [
+    {
+      columnName: t('Name'),
+      sortFunction: (a, b, c) => sortRows(a, b, c, 'metadata.name'),
+    },
+    {
+      columnName: t('Cluster name'),
+      sortFunction: (a, b, c) => sortRows(a, b, c, 'status.client.clusterName'),
+    },
+    {
+      columnName: t('Last heartbeat'),
+      sortFunction: (a, b, c) => sortRows(a, b, c, 'status.lastHeartbeat'),
+    },
+  ];
+  return (
+    <Modal
+      isOpen={isOpen}
+      onClose={closeModal}
+      title={t('Manage distribution of Storage clients')}
+    >
+      <ListPageFilter
+        data={unfilteredData}
+        loaded={loaded}
+        onFilterChange={onFilterChange}
+        hideColumnManagement={true}
+      />
+      <SelectableTable<StorageConsumerKind>
+        rows={filteredData}
+        columns={columns}
+        RowComponent={RowComponent}
+        selectedRows={selectedResources}
+        setSelectedRows={setSelectedResources}
+        loaded={loaded}
+        loadError={error}
+      />
+      <ModalFooter inProgress={inProgress} errorMessage={error}>
+        <Flex direction={{ default: 'row' }}>
+          <FlexItem>
+            <Button key="Cancel" variant="secondary" onClick={closeModal}>
+              {t('Cancel')}
+            </Button>
+          </FlexItem>
+          <FlexItem>
+            <Button key="Confirm" variant="primary" onClick={onSubmit}>
+              {t('Save changes')}
+            </Button>
+          </FlexItem>
+        </Flex>
+      </ModalFooter>
+    </Modal>
+  );
+};
path: packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherComponent.tsx
user: login: SanjalKatiyar
body: File is named `StorageClientAttacherComponent.tsx`, whereas Modal is named `StorageClientAttacherModal`...
I think we agreed on guideline that file name should be same as the exported component.
created_at: 2025-03-17T15:57:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+import { useMemo } from 'react';
+import {
+  getDriver,
+  getProvisioner,
+  StorageClassModel,
+  StorageClassResourceKind,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import { isCephDriver, isCephProvisioner } from '@odf/shared/utils';
+import {
+  Action,
+  K8sModel,
+  useModal,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { LaunchModal } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { StorageClientAttacherModal } from './StorageClientAttacherComponent';
+
+const isStorageClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is StorageClassResourceKind =>
+  resource.kind === StorageClassModel.kind;
+
+const isVolumeSnapshotClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is VolumeSnapshotClassKind =>
+  resource.kind === VolumeSnapshotClassModel.kind;
+
+export const useStorageClientAttacherAction = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+) => {
+  const launchModal = useModal();
+  const actions = useMemo(() => {
+    const items = [];
+    if (
+      isStorageClass(resource) &&
+      isCephProvisioner(getProvisioner(resource))
+    ) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          StorageClassModel,
+          launchModal
+        )
+      );
+    }
+    if (isVolumeSnapshotClass(resource) && isCephDriver(getDriver(resource))) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          VolumeSnapshotClassModel,
+          launchModal
+        )
+      );
+    }
+    return items;
+  }, [resource, launchModal]);
+  return useMemo(() => [actions, true, undefined], [actions]);
+};
+
+export const AttachResourceToStorageClientModal = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind,
+  resourceModel: K8sModel,
+  launchModal: LaunchModal
+): Action => {
+  return {
+    id: 'attach-resources',
+    label: 'Distribute to Storage clients',
+    insertBefore: 'edit-labels',
+    cta: () => {
+      launchModal(StorageClientAttacherModal, {
+        extraProps: { resource, resourceModel },
+        isOpen: true,
+
+        closeModal: null,
+      });
+    },
+  };
+};
path: packages/odf/modals/StorageClientDistributionModal/StorageClientResourceAttacherActions.ts
user: login: SanjalKatiyar
body: same for this file, name is `StorageClientResourceAttacherActions.ts`, as per previous discussions we only use this format if file is exporting a component with same name... else we use storage-client-resource-attacher-actions.ts` format.
created_at: 2025-03-17T15:59:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+import { useMemo } from 'react';
+import {
+  getDriver,
+  getProvisioner,
+  StorageClassModel,
+  StorageClassResourceKind,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import { isCephDriver, isCephProvisioner } from '@odf/shared/utils';
+import {
+  Action,
+  K8sModel,
+  useModal,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { LaunchModal } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { StorageClientAttacherModal } from './StorageClientAttacherComponent';
+
+const isStorageClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is StorageClassResourceKind =>
+  resource.kind === StorageClassModel.kind;
+
+const isVolumeSnapshotClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is VolumeSnapshotClassKind =>
+  resource.kind === VolumeSnapshotClassModel.kind;
+
+export const useStorageClientAttacherAction = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+) => {
+  const launchModal = useModal();
+  const actions = useMemo(() => {
+    const items = [];
+    if (
+      isStorageClass(resource) &&
+      isCephProvisioner(getProvisioner(resource))
+    ) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          StorageClassModel,
+          launchModal
+        )
+      );
+    }
+    if (isVolumeSnapshotClass(resource) && isCephDriver(getDriver(resource))) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          VolumeSnapshotClassModel,
+          launchModal
+        )
+      );
+    }
+    return items;
+  }, [resource, launchModal]);
+  return useMemo(() => [actions, true, undefined], [actions]);
path: packages/odf/modals/StorageClientDistributionModal/StorageClientResourceAttacherActions.ts
user: login: SanjalKatiyar
body: You can achieve same functionality with a single `useMemo` hook...
```suggestion
  const actions = useMemo(() => {
    const items = [];
    if (
      isStorageClass(resource) &&
      isCephProvisioner(getProvisioner(resource))
    ) {
      items.push(
        AttachResourceToStorageClientModal(
          resource,
          StorageClassModel,
          launchModal
        )
      );
    }
    if (isVolumeSnapshotClass(resource) && isCephDriver(getDriver(resource))) {
      items.push(
        AttachResourceToStorageClientModal(
          resource,
          VolumeSnapshotClassModel,
          launchModal
        )
      );
    }
    return [items, true, undefined];
  }, [resource, launchModal]);
  return actions;
```
created_at: 2025-03-17T16:03:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+import { useMemo } from 'react';
+import {
+  getDriver,
+  getProvisioner,
+  StorageClassModel,
+  StorageClassResourceKind,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import { isCephDriver, isCephProvisioner } from '@odf/shared/utils';
+import {
+  Action,
+  K8sModel,
+  useModal,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { LaunchModal } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { StorageClientAttacherModal } from './StorageClientAttacherComponent';
+
+const isStorageClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is StorageClassResourceKind =>
+  resource.kind === StorageClassModel.kind;
+
+const isVolumeSnapshotClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is VolumeSnapshotClassKind =>
+  resource.kind === VolumeSnapshotClassModel.kind;
+
+export const useStorageClientAttacherAction = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+) => {
+  const launchModal = useModal();
+  const actions = useMemo(() => {
+    const items = [];
+    if (
+      isStorageClass(resource) &&
+      isCephProvisioner(getProvisioner(resource))
+    ) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          StorageClassModel,
+          launchModal
+        )
+      );
+    }
+    if (isVolumeSnapshotClass(resource) && isCephDriver(getDriver(resource))) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          VolumeSnapshotClassModel,
+          launchModal
+        )
+      );
+    }
+    return items;
+  }, [resource, launchModal]);
+  return useMemo(() => [actions, true, undefined], [actions]);
+};
+
+export const AttachResourceToStorageClientModal = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind,
+  resourceModel: K8sModel,
+  launchModal: LaunchModal
+): Action => {
+  return {
+    id: 'attach-resources',
+    label: 'Distribute to Storage clients',
path: packages/odf/modals/StorageClientDistributionModal/StorageClientResourceAttacherActions.ts
user: login: SanjalKatiyar
body: shouldn't this either be `Distribute to storage clients` or `Distribute to StorageClients` ??
created_at: 2025-03-17T16:04:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+import { useMemo } from 'react';
+import {
+  getDriver,
+  getProvisioner,
+  StorageClassModel,
+  StorageClassResourceKind,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import { isCephDriver, isCephProvisioner } from '@odf/shared/utils';
+import {
+  Action,
+  K8sModel,
+  useModal,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { LaunchModal } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { StorageClientAttacherModal } from './StorageClientAttacherComponent';
+
+const isStorageClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is StorageClassResourceKind =>
+  resource.kind === StorageClassModel.kind;
+
+const isVolumeSnapshotClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is VolumeSnapshotClassKind =>
+  resource.kind === VolumeSnapshotClassModel.kind;
+
+export const useStorageClientAttacherAction = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+) => {
+  const launchModal = useModal();
+  const actions = useMemo(() => {
+    const items = [];
+    if (
+      isStorageClass(resource) &&
+      isCephProvisioner(getProvisioner(resource))
+    ) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          StorageClassModel,
+          launchModal
+        )
+      );
+    }
+    if (isVolumeSnapshotClass(resource) && isCephDriver(getDriver(resource))) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          VolumeSnapshotClassModel,
+          launchModal
+        )
+      );
+    }
+    return items;
+  }, [resource, launchModal]);
+  return useMemo(() => [actions, true, undefined], [actions]);
+};
+
+export const AttachResourceToStorageClientModal = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind,
+  resourceModel: K8sModel,
+  launchModal: LaunchModal
+): Action => {
+  return {
+    id: 'attach-resources',
+    label: 'Distribute to Storage clients',
+    insertBefore: 'edit-labels',
+    cta: () => {
+      launchModal(StorageClientAttacherModal, {
+        extraProps: { resource, resourceModel },
+        isOpen: true,
+
path: packages/odf/modals/StorageClientDistributionModal/StorageClientResourceAttacherActions.ts
user: login: SanjalKatiyar
body: ```suggestion
```
created_at: 2025-03-17T16:05:01Z
author_association: COLLABORATOR
diff_hunk: @@ -351,6 +351,14 @@
               "$codeRef": "storageProvisioners.isEncryptionKMSIdVisibleOrRequired"
             }
           },
+          "storageClient": {
+            "name": "Storage clients",
+            "hintText": "",
path: plugins/odf/console-extensions.json
user: login: SanjalKatiyar
body: ```suggestion
```
created_at: 2025-03-17T16:07:08Z
author_association: COLLABORATOR
diff_hunk: @@ -351,6 +351,14 @@
               "$codeRef": "storageProvisioners.isEncryptionKMSIdVisibleOrRequired"
             }
           },
+          "storageClient": {
+            "name": "Storage clients",
+            "hintText": "",
+            "Component": {
+              "$codeRef": "storageClassDistribution.StorageClientAttacherComponent"
+            },
+            "visible": true
+          },
path: plugins/odf/console-extensions.json
user: login: SanjalKatiyar
body: You have only added this to RBD provisioner SC creation flow, don't you need this for CephFS as well ??
created_at: 2025-03-17T16:07:50Z
author_association: COLLABORATOR
diff_hunk: @@ -351,6 +351,14 @@
               "$codeRef": "storageProvisioners.isEncryptionKMSIdVisibleOrRequired"
             }
           },
+          "storageClient": {
+            "name": "Storage clients",
+            "hintText": "",
+            "Component": {
+              "$codeRef": "storageClassDistribution.StorageClientAttacherComponent"
path: plugins/odf/console-extensions.json
user: login: SanjalKatiyar
body: `storageClassDistribution` is not a valid reference, right ??
created_at: 2025-03-17T16:09:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,173 @@
+import * as React from 'react';
+import { StorageConsumerModel } from '@odf/core/models';
+import {
+  getName,
+  ModalFooter,
+  RowComponentType,
+  SelectableTable,
+  StorageClassModel,
+  StorageClassResourceKind,
+  StorageConsumerKind,
+  useCustomTranslation,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import useInterceptSubmit from '@odf/shared/hooks/useMouseEventListener';
+import { CommonModalProps } from '@odf/shared/modals';
+import { sortRows } from '@odf/shared/utils';
+import {
+  K8sModel,
+  k8sPatch,
+  ListPageFilter,
+  useK8sWatchResource,
+  useListPageFilter,
+  WatchK8sResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { Button, Flex, FlexItem, Modal } from '@patternfly/react-core';
+import { Td } from '@patternfly/react-table';
+import { generatePatchForDistributionOfResources } from '../../components/ResourceDistribution/utils';
+import { LastHeartBeat } from '../../components/storage-consumers/client-list';
+import './storageClientAttacherComponent.scss';
+
+const RowComponent: React.FC<RowComponentType<StorageConsumerKind>> = ({
+  row: client,
+}) => {
+  const name = getName(client);
+  const clientClusterId = client?.status?.client?.clusterId;
+  const clusterName = `${client?.status?.client?.clusterName || '-'} (${
+    clientClusterId || '-'
+  })`;
+  return (
+    <>
+      <Td>{name}</Td>
+      <Td>{clusterName}</Td>
+      <Td>
+        <LastHeartBeat heartbeat={client?.status?.lastHeartbeat} />{' '}
+      </Td>
+    </>
+  );
+};
+
+const storageConsumerResource: WatchK8sResource = {
+  isList: true,
+  groupVersionKind: {
+    group: StorageConsumerModel.apiGroup,
+    version: StorageConsumerModel.apiVersion,
+    kind: StorageConsumerModel.kind,
+  },
+};
+
+type StorageClientAttacherModalProps = {
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind;
+  resourceModel: K8sModel;
+};
+
+export const StorageClientAttacherModal: ModalComponent<
+  CommonModalProps<StorageClientAttacherModalProps>
+> = ({ closeModal, isOpen, extraProps: { resource, resourceModel } }) => {
+  const setCallback = useInterceptSubmit();
+  const resourceName = getName(resource);
+  const [inProgress, setProgress] = React.useState(false);
+  const [error, setError] = React.useState('');
+
+  const [selectedResources, setSelectedResources] = React.useState<
+    StorageConsumerKind[]
+  >([]);
+  const { t } = useCustomTranslation();
+  const [storageConsumers, loaded] = useK8sWatchResource<StorageConsumerKind[]>(
+    storageConsumerResource
+  );
+  const [unfilteredData, filteredData, onFilterChange] =
+    useListPageFilter(storageConsumers);
+
+  const onSubmit = React.useCallback(() => {
+    const promises = [];
+    setProgress(true);
+    const storageClassNames =
+      resourceModel.kind === StorageClassModel.kind ? [resourceName] : [];
+    const volumeSnapshotClassNames =
+      resourceModel.kind === VolumeSnapshotClassModel.kind
+        ? [resourceName]
+        : [];
+    selectedResources.forEach((storageConsumer) => {
+      const patch = generatePatchForDistributionOfResources(
+        storageConsumer,
+        storageClassNames,
+        volumeSnapshotClassNames
+      );
+      promises.push(
+        k8sPatch({
+          model: StorageConsumerModel,
+          resource: storageConsumer,
+          data: patch,
+        })
+      );
+    });
+    Promise.all(promises)
+      .then(() => {
+        setProgress(false);
+        closeModal();
+      })
+      .catch((err) => {
+        setProgress(false);
+        setError(err?.message);
+      });
+  }, [closeModal, resourceModel.kind, resourceName, selectedResources]);
+
+  React.useEffect(() => {
+    setCallback(onSubmit);
path: packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherComponent.tsx
user: login: bipuladh
body: Removed
created_at: 2025-03-18T09:30:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -351,6 +351,14 @@
               "$codeRef": "storageProvisioners.isEncryptionKMSIdVisibleOrRequired"
             }
           },
+          "storageClient": {
+            "name": "Storage clients",
+            "hintText": "",
+            "Component": {
+              "$codeRef": "storageClassDistribution.StorageClientAttacherComponent"
path: plugins/odf/console-extensions.json
user: login: bipuladh
body: yes this code was supposed to be not here.
created_at: 2025-03-18T09:38:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -351,6 +351,14 @@
               "$codeRef": "storageProvisioners.isEncryptionKMSIdVisibleOrRequired"
             }
           },
+          "storageClient": {
+            "name": "Storage clients",
+            "hintText": "",
+            "Component": {
+              "$codeRef": "storageClassDistribution.StorageClientAttacherComponent"
+            },
+            "visible": true
+          },
path: plugins/odf/console-extensions.json
user: login: bipuladh
body: this is not required anymore.
created_at: 2025-03-18T09:38:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,79 @@
+import { useMemo } from 'react';
+import {
+  getDriver,
+  getProvisioner,
+  StorageClassModel,
+  StorageClassResourceKind,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import { isCephDriver, isCephProvisioner } from '@odf/shared/utils';
+import {
+  Action,
+  K8sModel,
+  useModal,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { LaunchModal } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { StorageClientAttacherModal } from './StorageClientAttacherComponent';
+
+const isStorageClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is StorageClassResourceKind =>
+  resource.kind === StorageClassModel.kind;
+
+const isVolumeSnapshotClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is VolumeSnapshotClassKind =>
+  resource.kind === VolumeSnapshotClassModel.kind;
+
+export const useStorageClientAttacherAction = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+) => {
+  const launchModal = useModal();
+  const actions = useMemo(() => {
+    const items = [];
+    if (
+      isStorageClass(resource) &&
+      isCephProvisioner(getProvisioner(resource))
+    ) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          StorageClassModel,
+          launchModal
+        )
+      );
+    }
+    if (isVolumeSnapshotClass(resource) && isCephDriver(getDriver(resource))) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          VolumeSnapshotClassModel,
+          launchModal
+        )
+      );
+    }
+    return items;
+  }, [resource, launchModal]);
+  return useMemo(() => [actions, true, undefined], [actions]);
+};
+
+export const AttachResourceToStorageClientModal = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind,
+  resourceModel: K8sModel,
+  launchModal: LaunchModal
+): Action => {
+  return {
+    id: 'attach-resources',
+    label: 'Distribute to Storage clients',
path: packages/odf/modals/StorageClientDistributionModal/StorageClientResourceAttacherActions.ts
user: login: bipuladh
body: Updating it to first option but will update it once more when the UXD microcopy is ready.
created_at: 2025-03-18T09:39:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,79 @@
+import { useMemo } from 'react';
+import {
+  getDriver,
+  getProvisioner,
+  StorageClassModel,
+  StorageClassResourceKind,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import { isCephDriver, isCephProvisioner } from '@odf/shared/utils';
+import {
+  Action,
+  K8sModel,
+  useModal,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { LaunchModal } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { StorageClientAttacherModal } from './StorageClientAttacherComponent';
+
+const isStorageClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is StorageClassResourceKind =>
+  resource.kind === StorageClassModel.kind;
+
+const isVolumeSnapshotClass = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+): resource is VolumeSnapshotClassKind =>
+  resource.kind === VolumeSnapshotClassModel.kind;
+
+export const useStorageClientAttacherAction = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind
+) => {
+  const launchModal = useModal();
+  const actions = useMemo(() => {
+    const items = [];
+    if (
+      isStorageClass(resource) &&
+      isCephProvisioner(getProvisioner(resource))
+    ) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          StorageClassModel,
+          launchModal
+        )
+      );
+    }
+    if (isVolumeSnapshotClass(resource) && isCephDriver(getDriver(resource))) {
+      items.push(
+        AttachResourceToStorageClientModal(
+          resource,
+          VolumeSnapshotClassModel,
+          launchModal
+        )
+      );
+    }
+    return items;
+  }, [resource, launchModal]);
+  return useMemo(() => [actions, true, undefined], [actions]);
+};
+
+export const AttachResourceToStorageClientModal = (
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind,
+  resourceModel: K8sModel,
+  launchModal: LaunchModal
+): Action => {
+  return {
+    id: 'attach-resources',
+    label: 'Distribute to Storage clients',
+    insertBefore: 'edit-labels',
+    cta: () => {
+      launchModal(StorageClientAttacherModal, {
+        extraProps: { resource, resourceModel },
+        isOpen: true,
+
+        closeModal: null,
+      });
+    },
+  };
+};
path: packages/odf/modals/StorageClientDistributionModal/StorageClientResourceAttacherActions.ts
user: login: bipuladh
body: Updating it.
created_at: 2025-03-18T09:44:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,173 @@
+import * as React from 'react';
+import { StorageConsumerModel } from '@odf/core/models';
+import {
+  getName,
+  ModalFooter,
+  RowComponentType,
+  SelectableTable,
+  StorageClassModel,
+  StorageClassResourceKind,
+  StorageConsumerKind,
+  useCustomTranslation,
+  VolumeSnapshotClassKind,
+  VolumeSnapshotClassModel,
+} from '@odf/shared';
+import useInterceptSubmit from '@odf/shared/hooks/useMouseEventListener';
+import { CommonModalProps } from '@odf/shared/modals';
+import { sortRows } from '@odf/shared/utils';
+import {
+  K8sModel,
+  k8sPatch,
+  ListPageFilter,
+  useK8sWatchResource,
+  useListPageFilter,
+  WatchK8sResource,
+} from '@openshift-console/dynamic-plugin-sdk';
+import { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
+import { Button, Flex, FlexItem, Modal } from '@patternfly/react-core';
+import { Td } from '@patternfly/react-table';
+import { generatePatchForDistributionOfResources } from '../../components/ResourceDistribution/utils';
+import { LastHeartBeat } from '../../components/storage-consumers/client-list';
+import './storageClientAttacherComponent.scss';
+
+const RowComponent: React.FC<RowComponentType<StorageConsumerKind>> = ({
+  row: client,
+}) => {
+  const name = getName(client);
+  const clientClusterId = client?.status?.client?.clusterId;
+  const clusterName = `${client?.status?.client?.clusterName || '-'} (${
+    clientClusterId || '-'
+  })`;
+  return (
+    <>
+      <Td>{name}</Td>
+      <Td>{clusterName}</Td>
+      <Td>
+        <LastHeartBeat heartbeat={client?.status?.lastHeartbeat} />{' '}
+      </Td>
+    </>
+  );
+};
+
+const storageConsumerResource: WatchK8sResource = {
+  isList: true,
+  groupVersionKind: {
+    group: StorageConsumerModel.apiGroup,
+    version: StorageConsumerModel.apiVersion,
+    kind: StorageConsumerModel.kind,
+  },
+};
+
+type StorageClientAttacherModalProps = {
+  resource: StorageClassResourceKind | VolumeSnapshotClassKind;
+  resourceModel: K8sModel;
+};
+
+export const StorageClientAttacherModal: ModalComponent<
+  CommonModalProps<StorageClientAttacherModalProps>
+> = ({ closeModal, isOpen, extraProps: { resource, resourceModel } }) => {
+  const setCallback = useInterceptSubmit();
+  const resourceName = getName(resource);
+  const [inProgress, setProgress] = React.useState(false);
+  const [error, setError] = React.useState('');
+
+  const [selectedResources, setSelectedResources] = React.useState<
+    StorageConsumerKind[]
+  >([]);
+  const { t } = useCustomTranslation();
+  const [storageConsumers, loaded] = useK8sWatchResource<StorageConsumerKind[]>(
+    storageConsumerResource
+  );
+  const [unfilteredData, filteredData, onFilterChange] =
+    useListPageFilter(storageConsumers);
+
+  const onSubmit = React.useCallback(() => {
+    const promises = [];
+    setProgress(true);
+    const storageClassNames =
+      resourceModel.kind === StorageClassModel.kind ? [resourceName] : [];
+    const volumeSnapshotClassNames =
+      resourceModel.kind === VolumeSnapshotClassModel.kind
+        ? [resourceName]
+        : [];
+    selectedResources.forEach((storageConsumer) => {
+      const patch = generatePatchForDistributionOfResources(
+        storageConsumer,
+        storageClassNames,
+        volumeSnapshotClassNames
+      );
+      promises.push(
+        k8sPatch({
+          model: StorageConsumerModel,
+          resource: storageConsumer,
+          data: patch,
+        })
+      );
+    });
+    Promise.all(promises)
+      .then(() => {
+        setProgress(false);
+        closeModal();
+      })
+      .catch((err) => {
+        setProgress(false);
+        setError(err?.message);
+      });
+  }, [closeModal, resourceModel.kind, resourceName, selectedResources]);
+
+  React.useEffect(() => {
+    setCallback(onSubmit);
+  }, [onSubmit, setCallback]);
+
+  const columns = [
+    {
+      columnName: t('Name'),
+      sortFunction: (a, b, c) => sortRows(a, b, c, 'metadata.name'),
+    },
+    {
+      columnName: t('Cluster name'),
+      sortFunction: (a, b, c) => sortRows(a, b, c, 'status.client.clusterName'),
+    },
+    {
+      columnName: t('Last heartbeat'),
+      sortFunction: (a, b, c) => sortRows(a, b, c, 'status.lastHeartbeat'),
+    },
+  ];
+  return (
+    <Modal
+      isOpen={isOpen}
+      onClose={closeModal}
+      title={t('Manage distribution of Storage clients')}
+    >
+      <ListPageFilter
+        data={unfilteredData}
+        loaded={loaded}
+        onFilterChange={onFilterChange}
+        hideColumnManagement={true}
+      />
+      <SelectableTable<StorageConsumerKind>
+        rows={filteredData}
+        columns={columns}
+        RowComponent={RowComponent}
+        selectedRows={selectedResources}
+        setSelectedRows={setSelectedResources}
+        loaded={loaded}
+        loadError={error}
+      />
+      <ModalFooter inProgress={inProgress} errorMessage={error}>
+        <Flex direction={{ default: 'row' }}>
+          <FlexItem>
+            <Button key="Cancel" variant="secondary" onClick={closeModal}>
+              {t('Cancel')}
+            </Button>
+          </FlexItem>
+          <FlexItem>
+            <Button key="Confirm" variant="primary" onClick={onSubmit}>
+              {t('Save changes')}
+            </Button>
+          </FlexItem>
+        </Flex>
+      </ModalFooter>
+    </Modal>
+  );
+};
path: packages/odf/modals/StorageClientDistributionModal/StorageClientAttacherComponent.tsx
user: login: bipuladh
body: Updating it
created_at: 2025-03-18T09:44:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: vbnrh
created_at: 2025-03-17T12:52:50Z
body: Does the modal page close button actually have a border or is it highlighted due to being in focus ?
author_association: MEMBER
user: login: SanjalKatiyar
created_at: 2025-03-18T11:00:01Z
body: /lgtm
author_association: COLLABORATOR
user: login: openshift-ci[bot]
created_at: 2025-03-18T11:00:09Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/red-hat-storage/odf-console/pull/1907#" title="Author self-approved">bipuladh</a>*, *<a href="https://github.com/red-hat-storage/odf-console/pull/1907#issuecomment-2732711270" title="LGTM">SanjalKatiyar</a>*

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

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:

- ~~[OWNERS](https://github.com/red-hat-storage/odf-console/blob/master/OWNERS)~~ [SanjalKatiyar,bipuladh]

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