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