This is an automated email from the ASF dual-hosted git repository.

michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new e3e2bece6bc feat(owners): display email in owner selectors (#37906)
e3e2bece6bc is described below

commit e3e2bece6bca8def875bef61a9c7e995f2773b08
Author: Michael S. Molina <[email protected]>
AuthorDate: Fri Feb 13 09:01:05 2026 -0300

    feat(owners): display email in owner selectors (#37906)
---
 .../DatasourceEditor/DatasourceEditor.tsx          | 38 ++++++++--
 .../src/components/ListView/Filters/Select.tsx     | 13 +++-
 .../src/components/ListView/Filters/index.tsx      |  2 +
 superset-frontend/src/components/ListView/types.ts |  8 ++-
 .../PropertiesModal/hooks/useAccessOptions.ts      | 32 +++++++--
 .../dashboard/components/PropertiesModal/index.tsx | 17 ++++-
 .../PropertiesModal/sections/AccessSection.tsx     | 22 +++++-
 .../explore/components/PropertiesModal/index.tsx   | 50 ++++++++++---
 .../src/features/alerts/AlertReportModal.tsx       | 46 +++++++++---
 superset-frontend/src/features/alerts/types.ts     |  4 +-
 .../OwnerSelectLabel/OwnerSelectLabel.test.tsx     | 38 ++++++++++
 .../src/features/owners/OwnerSelectLabel/index.tsx | 62 ++++++++++++++++
 .../src/pages/AlertReportList/index.tsx            | 11 ++-
 superset-frontend/src/pages/ChartList/index.tsx    |  6 +-
 .../src/pages/DashboardList/index.tsx              |  6 +-
 superset-frontend/src/pages/DatasetList/index.tsx  |  6 +-
 superset-frontend/src/types/Owner.ts               |  1 +
 superset-frontend/src/views/CRUD/utils.tsx         | 83 +++++++++++++++++-----
 superset/charts/api.py                             |  1 +
 superset/charts/schemas.py                         |  1 +
 superset/dashboards/api.py                         |  1 +
 superset/datasets/schemas.py                       |  1 +
 superset/reports/api.py                            |  7 ++
 tests/integration_tests/charts/api_tests.py        |  1 +
 tests/integration_tests/reports/api_tests.py       | 20 ++++--
 25 files changed, 407 insertions(+), 70 deletions(-)

diff --git 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
index fb0c1f597fc..c7819929074 100644
--- 
a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
+++ 
b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import rison from 'rison';
-import { PureComponent, useCallback, ReactNode } from 'react';
+import { PureComponent, useCallback, type ReactNode } from 'react';
 import { connect, ConnectedProps } from 'react-redux';
 import type { JsonObject } from '@superset-ui/core';
 import type { SupersetTheme } from '@apache-superset/core/ui';
@@ -77,6 +77,12 @@ import {
 import Mousetrap from 'mousetrap';
 import { clearDatasetCache } from 'src/utils/cachedSupersetGet';
 import { makeUrl } from 'src/utils/pathUtils';
+import {
+  OwnerSelectLabel,
+  OWNER_TEXT_LABEL_PROP,
+  OWNER_EMAIL_PROP,
+  OWNER_OPTION_FILTER_PROPS,
+} from 'src/features/owners/OwnerSelectLabel';
 import { DatabaseSelector } from '../../../DatabaseSelector';
 import CollectionTable from '../CollectionTable';
 import Fieldset from '../Fieldset';
@@ -98,9 +104,11 @@ const extensionsRegistry = getExtensionsRegistry();
 interface Owner {
   id?: number;
   value?: number;
-  label?: string;
+  label?: ReactNode;
   first_name?: string;
   last_name?: string;
+  email?: string;
+  [key: string]: unknown;
 }
 
 interface Currency {
@@ -757,7 +765,12 @@ function OwnersSelector({
           .filter(item => item.extra.active)
           .map(item => ({
             value: item.value as number,
-            label: item.text as string,
+            label: OwnerSelectLabel({
+              name: item.text as string,
+              email: item.extra?.email as string | undefined,
+            }),
+            [OWNER_TEXT_LABEL_PROP]: item.text as string,
+            [OWNER_EMAIL_PROP]: (item.extra?.email as string) ?? '',
           })),
         totalCount: response.json.count,
       }));
@@ -775,6 +788,7 @@ function OwnersSelector({
       onChange={value => onChange(value as Owner[])}
       header={<FormLabel>{t('Owners')}</FormLabel>}
       allowClear
+      optionFilterProps={OWNER_OPTION_FILTER_PROPS}
     />
   );
 }
@@ -847,10 +861,20 @@ class DatasourceEditor extends PureComponent<
     this.state = {
       datasource: {
         ...props.datasource,
-        owners: props.datasource.owners.map(owner => ({
-          value: owner.value || owner.id,
-          label: owner.label || `${owner.first_name} ${owner.last_name}`,
-        })),
+        owners: props.datasource.owners.map(owner => {
+          const ownerName =
+            owner.label || `${owner.first_name} ${owner.last_name}`;
+          return {
+            value: owner.value || owner.id,
+            label: OwnerSelectLabel({
+              name: typeof ownerName === 'string' ? ownerName : '',
+              email: owner.email,
+            }),
+            [OWNER_TEXT_LABEL_PROP]:
+              typeof ownerName === 'string' ? ownerName : '',
+            [OWNER_EMAIL_PROP]: owner.email ?? '',
+          };
+        }),
         metrics: props.datasource.metrics?.map(metric => {
           const {
             certified_by: certifiedByMetric,
diff --git a/superset-frontend/src/components/ListView/Filters/Select.tsx 
b/superset-frontend/src/components/ListView/Filters/Select.tsx
index 0d309b1b255..7c7273e5ee2 100644
--- a/superset-frontend/src/components/ListView/Filters/Select.tsx
+++ b/superset-frontend/src/components/ListView/Filters/Select.tsx
@@ -35,6 +35,7 @@ interface SelectFilterProps extends BaseFilter {
   fetchSelects?: Filter['fetchSelects'];
   name?: string;
   onSelect: (selected: SelectOption | undefined, isClear?: boolean) => void;
+  optionFilterProps?: string[];
   paginate?: boolean;
   selects: Filter['selects'];
   loading?: boolean;
@@ -48,6 +49,7 @@ function SelectFilter(
     fetchSelects,
     initialValue,
     onSelect,
+    optionFilterProps,
     selects = [],
     loading = false,
     dropdownStyle,
@@ -58,7 +60,15 @@ function SelectFilter(
 
   const onChange = (selected: SelectOption) => {
     onSelect(
-      selected ? { label: selected.label, value: selected.value } : undefined,
+      selected
+        ? {
+            label:
+              typeof selected.label === 'string'
+                ? selected.label
+                : String(selected.value),
+            value: selected.value,
+          }
+        : undefined,
     );
     setSelectedOption(selected);
   };
@@ -108,6 +118,7 @@ function SelectFilter(
           onChange={onChange}
           onClear={onClear}
           options={fetchAndFormatSelects}
+          optionFilterProps={optionFilterProps}
           placeholder={placeholder}
           dropdownStyle={dropdownStyle}
           showSearch
diff --git a/superset-frontend/src/components/ListView/Filters/index.tsx 
b/superset-frontend/src/components/ListView/Filters/index.tsx
index de85669a717..a0895fe87ef 100644
--- a/superset-frontend/src/components/ListView/Filters/index.tsx
+++ b/superset-frontend/src/components/ListView/Filters/index.tsx
@@ -72,6 +72,7 @@ function UIFilters(
             key,
             id,
             input,
+            optionFilterProps,
             paginate,
             selects,
             toolTipDescription,
@@ -109,6 +110,7 @@ function UIFilters(
 
                   updateFilterValue(index, option);
                 }}
+                optionFilterProps={optionFilterProps}
                 paginate={paginate}
                 selects={selects}
                 loading={loading ?? false}
diff --git a/superset-frontend/src/components/ListView/types.ts 
b/superset-frontend/src/components/ListView/types.ts
index 1d7042798ac..5d72d3dabe2 100644
--- a/superset-frontend/src/components/ListView/types.ts
+++ b/superset-frontend/src/components/ListView/types.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode } from 'react';
+import { type ReactNode } from 'react';
 
 export interface SortColumn {
   id: string;
@@ -24,8 +24,9 @@ export interface SortColumn {
 }
 
 export interface SelectOption {
-  label: string;
+  label: ReactNode;
   value: any;
+  [key: string]: unknown;
 }
 
 export interface CardSortSelectOption {
@@ -59,6 +60,7 @@ export interface ListViewFilter {
     page: number,
     pageSize: number,
   ) => Promise<{ data: SelectOption[]; totalCount: number }>;
+  optionFilterProps?: string[];
   paginate?: boolean;
   loading?: boolean;
   dateFilterValueType?: 'unix' | 'iso';
@@ -81,7 +83,7 @@ export type InnerFilterValue =
   | undefined
   | string[]
   | number[]
-  | { label: string; value: string | number }
+  | { label: ReactNode; value: string | number }
   | [number | null, number | null];
 
 export interface ListViewFilterValue {
diff --git 
a/superset-frontend/src/dashboard/components/PropertiesModal/hooks/useAccessOptions.ts
 
b/superset-frontend/src/dashboard/components/PropertiesModal/hooks/useAccessOptions.ts
index 99e3aa11440..13b84a6d036 100644
--- 
a/superset-frontend/src/dashboard/components/PropertiesModal/hooks/useAccessOptions.ts
+++ 
b/superset-frontend/src/dashboard/components/PropertiesModal/hooks/useAccessOptions.ts
@@ -19,6 +19,11 @@
 import { useCallback } from 'react';
 import { SupersetClient } from '@superset-ui/core';
 import rison from 'rison';
+import {
+  OwnerSelectLabel,
+  OWNER_TEXT_LABEL_PROP,
+  OWNER_EMAIL_PROP,
+} from 'src/features/owners/OwnerSelectLabel';
 
 /**
  * Hook for loading dashboard access options (owners and roles)
@@ -38,10 +43,29 @@ export const useAccessOptions = () => {
           .filter((item: { extra: { active: boolean } }) =>
             item.extra.active !== undefined ? item.extra.active : true,
           )
-          .map((item: { value: number; text: string }) => ({
-            value: item.value,
-            label: item.text,
-          })),
+          .map(
+            (item: {
+              value: number;
+              text: string;
+              extra: { email?: string };
+            }) => {
+              if (accessType === 'owners') {
+                return {
+                  value: item.value,
+                  label: OwnerSelectLabel({
+                    name: item.text,
+                    email: item.extra?.email,
+                  }),
+                  [OWNER_TEXT_LABEL_PROP]: item.text,
+                  [OWNER_EMAIL_PROP]: item.extra?.email ?? '',
+                };
+              }
+              return {
+                value: item.value,
+                label: item.text,
+              };
+            },
+          ),
         totalCount: response.json.count,
       }));
     },
diff --git 
a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx 
b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
index 4b4ec463f23..9f3b3bbc747 100644
--- a/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/dashboard/components/PropertiesModal/index.tsx
@@ -38,6 +38,10 @@ import {
 } from '@superset-ui/core';
 
 import withToasts from 'src/components/MessageToasts/withToasts';
+import {
+  OWNER_TEXT_LABEL_PROP,
+  OWNER_EMAIL_PROP,
+} from 'src/features/owners/OwnerSelectLabel';
 import { fetchTags, OBJECT_TYPES } from 'src/features/tags/tags';
 import {
   applyColors,
@@ -79,6 +83,7 @@ type Owners = {
   full_name?: string;
   first_name?: string;
   last_name?: string;
+  email?: string;
 }[];
 type DashboardInfo = {
   id: number;
@@ -240,10 +245,16 @@ const PropertiesModal = ({
     }
   };
 
-  const handleOnChangeOwners = (owners: { value: number; label: string }[]) => 
{
-    const parsedOwners: Owners = ensureIsArray(owners).map(o => ({
+  const handleOnChangeOwners = (
+    owners: { value: number; label: string }[],
+    options: Record<string, unknown>[],
+  ) => {
+    const parsedOwners: Owners = ensureIsArray(owners).map((o, i) => ({
       id: o.value,
-      full_name: o.label,
+      full_name:
+        (options?.[i]?.[OWNER_TEXT_LABEL_PROP] as string) ||
+        (typeof o.label === 'string' ? o.label : ''),
+      email: (options?.[i]?.[OWNER_EMAIL_PROP] as string) || '',
     }));
     setOwners(parsedOwners);
   };
diff --git 
a/superset-frontend/src/dashboard/components/PropertiesModal/sections/AccessSection.tsx
 
b/superset-frontend/src/dashboard/components/PropertiesModal/sections/AccessSection.tsx
index 1798625cec7..1ef452d8bf7 100644
--- 
a/superset-frontend/src/dashboard/components/PropertiesModal/sections/AccessSection.tsx
+++ 
b/superset-frontend/src/dashboard/components/PropertiesModal/sections/AccessSection.tsx
@@ -25,6 +25,12 @@ import { loadTags } from 'src/components/Tag/utils';
 import getOwnerName from 'src/utils/getOwnerName';
 import Owner from 'src/types/Owner';
 import { ModalFormField } from 'src/components/Modal';
+import {
+  OwnerSelectLabel,
+  OWNER_TEXT_LABEL_PROP,
+  OWNER_EMAIL_PROP,
+  OWNER_OPTION_FILTER_PROPS,
+} from 'src/features/owners/OwnerSelectLabel';
 import { useAccessOptions } from '../hooks/useAccessOptions';
 
 type Roles = { id: number; name: string }[];
@@ -33,6 +39,7 @@ type Owners = {
   full_name?: string;
   first_name?: string;
   last_name?: string;
+  email?: string;
 }[];
 
 interface AccessSectionProps {
@@ -40,7 +47,10 @@ interface AccessSectionProps {
   owners: Owners;
   roles: Roles;
   tags: TagType[];
-  onChangeOwners: (owners: { value: number; label: string }[]) => void;
+  onChangeOwners: (
+    owners: { value: number; label: string }[],
+    options: Record<string, unknown>[],
+  ) => void;
   onChangeRoles: (roles: { value: number; label: string }[]) => void;
   onChangeTags: (tags: { label: string; value: number }[]) => void;
   onClearTags: () => void;
@@ -60,9 +70,14 @@ const AccessSection = ({
 
   const ownersSelectValue = useMemo(
     () =>
-      (owners || []).map((owner: Owner) => ({
+      (owners || []).map((owner: Owner & { email?: string }) => ({
         value: owner.id,
-        label: getOwnerName(owner),
+        label: OwnerSelectLabel({
+          name: getOwnerName(owner),
+          email: owner.email,
+        }),
+        [OWNER_TEXT_LABEL_PROP]: getOwnerName(owner),
+        [OWNER_EMAIL_PROP]: owner.email ?? '',
       })),
     [owners],
   );
@@ -107,6 +122,7 @@ const AccessSection = ({
           value={ownersSelectValue}
           showSearch
           placeholder={t('Search owners')}
+          optionFilterProps={OWNER_OPTION_FILTER_PROPS}
         />
       </ModalFormField>
       {isFeatureEnabled(FeatureFlag.DashboardRbac) && (
diff --git a/superset-frontend/src/explore/components/PropertiesModal/index.tsx 
b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
index f5a08698529..1c16cbe6e63 100644
--- a/superset-frontend/src/explore/components/PropertiesModal/index.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal/index.tsx
@@ -37,6 +37,12 @@ import {
 import Chart, { Slice } from 'src/types/Chart';
 import withToasts from 'src/components/MessageToasts/withToasts';
 import { type TagType } from 'src/components';
+import {
+  OwnerSelectLabel,
+  OWNER_TEXT_LABEL_PROP,
+  OWNER_EMAIL_PROP,
+  OWNER_OPTION_FILTER_PROPS,
+} from 'src/features/owners/OwnerSelectLabel';
 import { TagTypeEnum } from 'src/components/Tag/TagType';
 import { loadTags } from 'src/components/Tag/utils';
 import {
@@ -153,6 +159,7 @@ function PropertiesModal({
           'owners.id',
           'owners.first_name',
           'owners.last_name',
+          'owners.email',
           'tags.id',
           'tags.name',
           'tags.type',
@@ -164,10 +171,25 @@ function PropertiesModal({
         });
         const chart = response.json.result;
         setSelectedOwners(
-          chart?.owners?.map((owner: any) => ({
-            value: owner.id,
-            label: `${owner.first_name} ${owner.last_name}`,
-          })),
+          chart?.owners?.map(
+            (owner: {
+              id: number;
+              first_name: string;
+              last_name: string;
+              email?: string;
+            }) => {
+              const ownerName = `${owner.first_name} ${owner.last_name}`;
+              return {
+                value: owner.id,
+                label: OwnerSelectLabel({
+                  name: ownerName,
+                  email: owner.email,
+                }),
+                [OWNER_TEXT_LABEL_PROP]: ownerName,
+                [OWNER_EMAIL_PROP]: owner.email ?? '',
+              };
+            },
+          ),
         );
         if (isFeatureEnabled(FeatureFlag.TaggingSystem)) {
           const customTags = chart.tags?.filter(
@@ -196,10 +218,21 @@ function PropertiesModal({
         }).then(response => ({
           data: response.json.result
             .filter((item: { extra: { active: boolean } }) => 
item.extra.active)
-            .map((item: { value: number; text: string }) => ({
-              value: item.value,
-              label: item.text,
-            })),
+            .map(
+              (item: {
+                value: number;
+                text: string;
+                extra: { email?: string };
+              }) => ({
+                value: item.value,
+                label: OwnerSelectLabel({
+                  name: item.text,
+                  email: item.extra?.email,
+                }),
+                [OWNER_TEXT_LABEL_PROP]: item.text,
+                [OWNER_EMAIL_PROP]: item.extra?.email ?? '',
+              }),
+            ),
           totalCount: response.json.count,
         }));
       },
@@ -372,6 +405,7 @@ function PropertiesModal({
                     options={loadOptions}
                     disabled={!selectedOwners}
                     allowClear
+                    optionFilterProps={OWNER_OPTION_FILTER_PROPS}
                   />
                 </ModalFormField>
                 {isFeatureEnabled(FeatureFlag.TaggingSystem) && (
diff --git a/superset-frontend/src/features/alerts/AlertReportModal.tsx 
b/superset-frontend/src/features/alerts/AlertReportModal.tsx
index 741421e71c1..3974db42e33 100644
--- a/superset-frontend/src/features/alerts/AlertReportModal.tsx
+++ b/superset-frontend/src/features/alerts/AlertReportModal.tsx
@@ -39,6 +39,12 @@ import rison from 'rison';
 import { useSingleViewResource } from 'src/views/CRUD/hooks';
 import withToasts from 'src/components/MessageToasts/withToasts';
 import Owner from 'src/types/Owner';
+import {
+  OwnerSelectLabel,
+  OWNER_TEXT_LABEL_PROP,
+  OWNER_EMAIL_PROP,
+  OWNER_OPTION_FILTER_PROPS,
+} from 'src/features/owners/OwnerSelectLabel';
 // import { Form as AntdForm } from 'src/components/Form';
 import { propertyComparator } from '@superset-ui/core/components/Select/utils';
 import {
@@ -980,9 +986,18 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
           endpoint: `/api/v1/report/related/created_by?q=${query}`,
         }).then(response => ({
           data: response.json.result.map(
-            (item: { value: number; text: string }) => ({
+            (item: {
+              value: number;
+              text: string;
+              extra: { email?: string };
+            }) => ({
               value: item.value,
-              label: item.text,
+              label: OwnerSelectLabel({
+                name: item.text,
+                email: item.extra?.email,
+              }),
+              [OWNER_TEXT_LABEL_PROP]: item.text,
+              [OWNER_EMAIL_PROP]: item.extra?.email ?? '',
             }),
           ),
           totalCount: response.json.count,
@@ -1850,7 +1865,12 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
           ? [
               {
                 value: currentUser.userId,
-                label: `${currentUser.firstName} ${currentUser.lastName}`,
+                label: OwnerSelectLabel({
+                  name: `${currentUser.firstName} ${currentUser.lastName}`,
+                  email: currentUser.email,
+                }),
+                [OWNER_TEXT_LABEL_PROP]: `${currentUser.firstName} 
${currentUser.lastName}`,
+                [OWNER_EMAIL_PROP]: currentUser.email ?? '',
               },
             ]
           : [],
@@ -1936,12 +1956,21 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
               label: (resource.database as DatabaseObject).database_name,
             }
           : undefined,
-        owners: (alert?.owners || []).map(owner => ({
-          value: (owner as MetaObject).value || owner.id,
-          label:
+        owners: (resource.owners || []).map(owner => {
+          const ownerName =
             (owner as MetaObject).label ||
-            `${(owner as Owner).first_name} ${(owner as Owner).last_name}`,
-        })),
+            `${(owner as Owner).first_name} ${(owner as Owner).last_name}`;
+          return {
+            value: (owner as MetaObject).value || owner.id,
+            label: OwnerSelectLabel({
+              name: typeof ownerName === 'string' ? ownerName : '',
+              email: (owner as Owner).email,
+            }),
+            [OWNER_TEXT_LABEL_PROP]:
+              typeof ownerName === 'string' ? ownerName : '',
+            [OWNER_EMAIL_PROP]: (owner as Owner).email ?? '',
+          };
+        }),
         validator_config_json:
           resource.validator_type === 'not null'
             ? {
@@ -2108,6 +2137,7 @@ const AlertReportModal: 
FunctionComponent<AlertReportModalProps> = ({
                       options={loadOwnerOptions}
                       onChange={onOwnersChange}
                       data-test="owners-select"
+                      optionFilterProps={OWNER_OPTION_FILTER_PROPS}
                     />
                   </ModalFormField>
                   <ModalFormField label={t('Description')}>
diff --git a/superset-frontend/src/features/alerts/types.ts 
b/superset-frontend/src/features/alerts/types.ts
index 0ea83f8d234..c8bb02c74dc 100644
--- a/superset-frontend/src/features/alerts/types.ts
+++ b/superset-frontend/src/features/alerts/types.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+import type { ReactNode } from 'react';
 import Owner from 'src/types/Owner';
 import { NotificationFormats } from 'src/features/reports/types';
 
@@ -85,8 +86,9 @@ export type Recipient = {
 
 export type MetaObject = {
   id?: number;
-  label?: string;
+  label?: ReactNode;
   value?: number | string;
+  [key: string]: unknown;
 };
 
 export type DashboardState = {
diff --git 
a/superset-frontend/src/features/owners/OwnerSelectLabel/OwnerSelectLabel.test.tsx
 
b/superset-frontend/src/features/owners/OwnerSelectLabel/OwnerSelectLabel.test.tsx
new file mode 100644
index 00000000000..35670fb7c0d
--- /dev/null
+++ 
b/superset-frontend/src/features/owners/OwnerSelectLabel/OwnerSelectLabel.test.tsx
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { render, screen } from 'spec/helpers/testing-library';
+import { OwnerSelectLabel } from '.';
+
+test('renders name and email', () => {
+  render(OwnerSelectLabel({ name: 'John Doe', email: '[email protected]' }));
+  expect(screen.getByText('John Doe')).toBeInTheDocument();
+  expect(screen.getByText('[email protected]')).toBeInTheDocument();
+});
+
+test('renders only name when email is undefined', () => {
+  render(OwnerSelectLabel({ name: 'Jane Smith' }));
+  expect(screen.getByText('Jane Smith')).toBeInTheDocument();
+});
+
+test('renders only name when email is empty string', () => {
+  render(OwnerSelectLabel({ name: 'Jane Smith', email: '' }));
+  expect(screen.getByText('Jane Smith')).toBeInTheDocument();
+  const container = screen.getByText('Jane Smith').parentElement;
+  expect(container?.children).toHaveLength(1);
+});
diff --git a/superset-frontend/src/features/owners/OwnerSelectLabel/index.tsx 
b/superset-frontend/src/features/owners/OwnerSelectLabel/index.tsx
new file mode 100644
index 00000000000..ad1db260f0e
--- /dev/null
+++ b/superset-frontend/src/features/owners/OwnerSelectLabel/index.tsx
@@ -0,0 +1,62 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { styled } from '@apache-superset/core/ui';
+
+const StyledLabelContainer = styled.div`
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: block;
+`;
+
+const StyledLabel = styled.span`
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: block;
+`;
+
+const StyledLabelDetail = styled.span`
+  ${({ theme: { fontSizeSM, colorTextSecondary } }) => `
+    overflow: hidden;
+    text-overflow: ellipsis;
+    font-size: ${fontSizeSM}px;
+    color: ${colorTextSecondary};
+    line-height: 1.6;
+    display: block;
+  `}
+`;
+
+export const OWNER_TEXT_LABEL_PROP = 'textLabel';
+export const OWNER_EMAIL_PROP = 'ownerEmail';
+export const OWNER_OPTION_FILTER_PROPS = [
+  OWNER_TEXT_LABEL_PROP,
+  OWNER_EMAIL_PROP,
+];
+
+export const OwnerSelectLabel = ({
+  name,
+  email,
+}: {
+  name: string;
+  email?: string;
+}) => (
+  <StyledLabelContainer>
+    <StyledLabel>{name}</StyledLabel>
+    {email && <StyledLabelDetail>{email}</StyledLabelDetail>}
+  </StyledLabelContainer>
+);
diff --git a/superset-frontend/src/pages/AlertReportList/index.tsx 
b/superset-frontend/src/pages/AlertReportList/index.tsx
index 174b9564042..6f8060bd2dd 100644
--- a/superset-frontend/src/pages/AlertReportList/index.tsx
+++ b/superset-frontend/src/pages/AlertReportList/index.tsx
@@ -53,7 +53,12 @@ import {
   useListViewResource,
   useSingleViewResource,
 } from 'src/views/CRUD/hooks';
-import { createErrorHandler, createFetchRelated } from 'src/views/CRUD/utils';
+import {
+  createErrorHandler,
+  createFetchRelated,
+  createFetchOwners,
+} from 'src/views/CRUD/utils';
+import { OWNER_OPTION_FILTER_PROPS } from 
'src/features/owners/OwnerSelectLabel';
 import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
 import Owner from 'src/types/Owner';
 import AlertReportModal from 'src/features/alerts/AlertReportModal';
@@ -480,14 +485,14 @@ function AlertList({
         input: 'select',
         operator: FilterOperator.RelationManyMany,
         unfilteredLabel: t('All'),
-        fetchSelects: createFetchRelated(
+        fetchSelects: createFetchOwners(
           'report',
-          'owners',
           createErrorHandler(errMsg =>
             t('An error occurred while fetching owners values: %s', errMsg),
           ),
           user,
         ),
+        optionFilterProps: OWNER_OPTION_FILTER_PROPS,
         paginate: true,
         dropdownStyle: { minWidth: WIDER_DROPDOWN_WIDTH },
       },
diff --git a/superset-frontend/src/pages/ChartList/index.tsx 
b/superset-frontend/src/pages/ChartList/index.tsx
index 560fb82b30f..d54a6932d48 100644
--- a/superset-frontend/src/pages/ChartList/index.tsx
+++ b/superset-frontend/src/pages/ChartList/index.tsx
@@ -33,8 +33,10 @@ import { useSelector } from 'react-redux';
 import {
   createErrorHandler,
   createFetchRelated,
+  createFetchOwners,
   handleChartDelete,
 } from 'src/views/CRUD/utils';
+import { OWNER_OPTION_FILTER_PROPS } from 
'src/features/owners/OwnerSelectLabel';
 import {
   useChartEditModal,
   useFavoriteStatus,
@@ -676,9 +678,8 @@ function ChartList(props: ChartListProps) {
         input: 'select',
         operator: FilterOperator.RelationManyMany,
         unfilteredLabel: t('All'),
-        fetchSelects: createFetchRelated(
+        fetchSelects: createFetchOwners(
           'chart',
-          'owners',
           createErrorHandler(errMsg =>
             addDangerToast(
               t(
@@ -689,6 +690,7 @@ function ChartList(props: ChartListProps) {
           ),
           props.user,
         ),
+        optionFilterProps: OWNER_OPTION_FILTER_PROPS,
         paginate: true,
         dropdownStyle: { minWidth: WIDER_DROPDOWN_WIDTH },
       },
diff --git a/superset-frontend/src/pages/DashboardList/index.tsx 
b/superset-frontend/src/pages/DashboardList/index.tsx
index 6b9454f7adc..6b96d36e13e 100644
--- a/superset-frontend/src/pages/DashboardList/index.tsx
+++ b/superset-frontend/src/pages/DashboardList/index.tsx
@@ -29,9 +29,11 @@ import { Link } from 'react-router-dom';
 import rison from 'rison';
 import {
   createFetchRelated,
+  createFetchOwners,
   createErrorHandler,
   handleDashboardDelete,
 } from 'src/views/CRUD/utils';
+import { OWNER_OPTION_FILTER_PROPS } from 
'src/features/owners/OwnerSelectLabel';
 import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
 import {
   CertifiedBadge,
@@ -582,9 +584,8 @@ function DashboardList(props: DashboardListProps) {
         input: 'select',
         operator: FilterOperator.RelationManyMany,
         unfilteredLabel: t('All'),
-        fetchSelects: createFetchRelated(
+        fetchSelects: createFetchOwners(
           'dashboard',
-          'owners',
           createErrorHandler(errMsg =>
             addDangerToast(
               t(
@@ -595,6 +596,7 @@ function DashboardList(props: DashboardListProps) {
           ),
           props.user,
         ),
+        optionFilterProps: OWNER_OPTION_FILTER_PROPS,
         paginate: true,
         dropdownStyle: { minWidth: WIDER_DROPDOWN_WIDTH },
       },
diff --git a/superset-frontend/src/pages/DatasetList/index.tsx 
b/superset-frontend/src/pages/DatasetList/index.tsx
index 7822e15a9e5..d88a0ae8fb3 100644
--- a/superset-frontend/src/pages/DatasetList/index.tsx
+++ b/superset-frontend/src/pages/DatasetList/index.tsx
@@ -25,8 +25,10 @@ import rison from 'rison';
 import {
   createFetchRelated,
   createFetchDistinct,
+  createFetchOwners,
   createErrorHandler,
 } from 'src/views/CRUD/utils';
+import { OWNER_OPTION_FILTER_PROPS } from 
'src/features/owners/OwnerSelectLabel';
 import { ColumnObject } from 'src/features/datasets/types';
 import { useListViewResource } from 'src/views/CRUD/hooks';
 import {
@@ -579,9 +581,8 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
         input: 'select',
         operator: FilterOperator.RelationManyMany,
         unfilteredLabel: 'All',
-        fetchSelects: createFetchRelated(
+        fetchSelects: createFetchOwners(
           'dataset',
-          'owners',
           createErrorHandler(errMsg =>
             t(
               'An error occurred while fetching dataset owner values: %s',
@@ -590,6 +591,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
           ),
           user,
         ),
+        optionFilterProps: OWNER_OPTION_FILTER_PROPS,
         paginate: true,
         dropdownStyle: { minWidth: WIDER_DROPDOWN_WIDTH },
       },
diff --git a/superset-frontend/src/types/Owner.ts 
b/superset-frontend/src/types/Owner.ts
index b8c0f4962cb..a025c77e811 100644
--- a/superset-frontend/src/types/Owner.ts
+++ b/superset-frontend/src/types/Owner.ts
@@ -26,4 +26,5 @@ export default interface Owner {
   id: number;
   last_name?: string;
   full_name?: string;
+  email?: string;
 }
diff --git a/superset-frontend/src/views/CRUD/utils.tsx 
b/superset-frontend/src/views/CRUD/utils.tsx
index 4f29f67223b..f234527fa6a 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -36,6 +36,11 @@ import SupersetText from 'src/utils/textUtils';
 import { findPermission } from 'src/utils/findPermission';
 import { User } from 'src/types/bootstrapTypes';
 import { RecentActivity, WelcomeTable } from 'src/features/home/types';
+import {
+  OwnerSelectLabel,
+  OWNER_TEXT_LABEL_PROP,
+  OWNER_EMAIL_PROP,
+} from 'src/features/owners/OwnerSelectLabel';
 import { Dashboard, Filter, TableTab } from './types';
 
 // Modifies the rison encoding slightly to match the backend's rison 
encoding/decoding. Applies globally.
@@ -91,6 +96,7 @@ const createFetchResourceMethod =
     });
 
     let fetchedLoggedUser = false;
+    let loggedUserExtra: Record<string, unknown> | undefined;
     const loggedUser = user
       ? {
           label: `${user.firstName} ${user.lastName}`,
@@ -98,26 +104,42 @@ const createFetchResourceMethod =
         }
       : undefined;
 
-    const data: { label: string; value: string | number }[] = [];
+    const data: {
+      label: string;
+      value: string | number;
+      extra?: Record<string, unknown>;
+    }[] = [];
     json?.result
       ?.filter(({ text }: { text: string }) => text.trim().length > 0)
-      .forEach(({ text, value }: { text: string; value: string | number }) => {
-        if (
-          loggedUser &&
-          value === loggedUser.value &&
-          text === loggedUser.label
-        ) {
-          fetchedLoggedUser = true;
-        } else {
-          data.push({
-            label: text,
-            value,
-          });
-        }
-      });
+      .forEach(
+        ({
+          text,
+          value,
+          extra,
+        }: {
+          text: string;
+          value: string | number;
+          extra?: Record<string, unknown>;
+        }) => {
+          if (
+            loggedUser &&
+            value === loggedUser.value &&
+            text === loggedUser.label
+          ) {
+            fetchedLoggedUser = true;
+            loggedUserExtra = extra;
+          } else {
+            data.push({
+              label: text,
+              value,
+              extra,
+            });
+          }
+        },
+      );
 
     if (loggedUser && (!filterValue || fetchedLoggedUser)) {
-      data.unshift(loggedUser);
+      data.unshift({ ...loggedUser, extra: loggedUserExtra });
     }
 
     return {
@@ -240,6 +262,35 @@ export const getRecentActivityObjs = (
 export const createFetchRelated = createFetchResourceMethod('related');
 export const createFetchDistinct = createFetchResourceMethod('distinct');
 
+export const createFetchOwners = (
+  resource: string,
+  handleError: (error: Response) => void,
+  user?: { userId: string | number; firstName: string; lastName: string },
+) => {
+  const fetchRelated = createFetchRelated(
+    resource,
+    'owners',
+    handleError,
+    user,
+  );
+  return async (filterValue = '', page: number, pageSize: number) => {
+    const result = await fetchRelated(filterValue, page, pageSize);
+    return {
+      ...result,
+      data: result.data.map(item => {
+        const email = item.extra?.email as string | undefined;
+        return {
+          label: OwnerSelectLabel({ name: item.label, email }),
+          value: item.value,
+          title: item.label,
+          [OWNER_TEXT_LABEL_PROP]: item.label,
+          [OWNER_EMAIL_PROP]: email ?? '',
+        };
+      }),
+    };
+  };
+};
+
 export function createErrorHandler(
   handleErrorFunc: (
     errMsg?: string | Record<string, string[] | string>,
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 66c0781ffb3..f5dd6ce3cbd 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -167,6 +167,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
         "owners.first_name",
         "owners.id",
         "owners.last_name",
+        "owners.email",
         "dashboards.id",
         "dashboards.dashboard_title",
         "params",
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index 8cc500ecac1..4f37a810223 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -1671,6 +1671,7 @@ class UserSchema(Schema):
     id = fields.Int()
     first_name = fields.String()
     last_name = fields.String()
+    email = fields.String()
 
 
 class DashboardSchema(Schema):
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 4592138866e..cdbed39552b 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -197,6 +197,7 @@ BASE_LIST_COLUMNS = [
     "owners.id",
     "owners.first_name",
     "owners.last_name",
+    "owners.email",
     "roles.id",
     "roles.name",
     "is_managed_externally",
diff --git a/superset/datasets/schemas.py b/superset/datasets/schemas.py
index 1506ef45d16..82f1e7faa1c 100644
--- a/superset/datasets/schemas.py
+++ b/superset/datasets/schemas.py
@@ -414,6 +414,7 @@ class DatasetColumnDrillInfoSchema(Schema):
 class UserSchema(Schema):
     first_name = fields.String()
     last_name = fields.String()
+    email = fields.String()
 
 
 class DatasetDrillInfoSchema(Schema):
diff --git a/superset/reports/api.py b/superset/reports/api.py
index a0ebabb2028..b0cc9475461 100644
--- a/superset/reports/api.py
+++ b/superset/reports/api.py
@@ -82,6 +82,11 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
     resource_name = "report"
     allow_browser_login = True
 
+    extra_fields_rel_fields = {
+        **BaseSupersetModelRestApi.extra_fields_rel_fields,
+        "created_by": ["email", "active"],
+    }
+
     base_filters = [
         ["id", ReportScheduleFilter, lambda: []],
     ]
@@ -113,6 +118,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
         "owners.first_name",
         "owners.id",
         "owners.last_name",
+        "owners.email",
         "recipients.id",
         "recipients.recipient_config_json",
         "recipients.type",
@@ -152,6 +158,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
         "owners.first_name",
         "owners.id",
         "owners.last_name",
+        "owners.email",
         "recipients.id",
         "recipients.type",
         "timezone",
diff --git a/tests/integration_tests/charts/api_tests.py 
b/tests/integration_tests/charts/api_tests.py
index b8b60355419..733c9e0e676 100644
--- a/tests/integration_tests/charts/api_tests.py
+++ b/tests/integration_tests/charts/api_tests.py
@@ -1035,6 +1035,7 @@ class TestChartApi(ApiOwnersTestCaseMixin, 
InsertChartMixin, SupersetTestCase):
                     "id": 1,
                     "first_name": "admin",
                     "last_name": "user",
+                    "email": "[email protected]",
                 }
             ],
             "params": None,
diff --git a/tests/integration_tests/reports/api_tests.py 
b/tests/integration_tests/reports/api_tests.py
index 58bde34ac72..41edf717020 100644
--- a/tests/integration_tests/reports/api_tests.py
+++ b/tests/integration_tests/reports/api_tests.py
@@ -301,12 +301,18 @@ class TestReportSchedulesApi(SupersetTestCase):
         for key in expected_result:
             assert data["result"][key] == expected_result[key]
         # needed because order may vary
-        assert {"first_name": "admin", "id": 1, "last_name": "user"} in 
data["result"][
-            "owners"
-        ]
-        assert {"first_name": "alpha", "id": 5, "last_name": "user"} in 
data["result"][
-            "owners"
-        ]
+        assert {
+            "email": "[email protected]",
+            "first_name": "admin",
+            "id": 1,
+            "last_name": "user",
+        } in data["result"]["owners"]
+        assert {
+            "email": "[email protected]",
+            "first_name": "alpha",
+            "id": 5,
+            "last_name": "user",
+        } in data["result"]["owners"]
         assert len(data["result"]["owners"]) == 2
 
     def test_info_report_schedule(self):
@@ -382,7 +388,7 @@ class TestReportSchedulesApi(SupersetTestCase):
         assert expected_fields == data_keys
 
         # Assert nested fields
-        expected_owners_fields = ["first_name", "id", "last_name"]
+        expected_owners_fields = ["email", "first_name", "id", "last_name"]
         data_keys = sorted(list(data["result"][0]["owners"][0].keys()))  # 
noqa: C414
         assert expected_owners_fields == data_keys
 


Reply via email to