feat(owners): display email in owner selectors (#37906)

This commit is contained in:
Michael S. Molina
2026-02-13 09:01:05 -03:00
committed by GitHub
parent 0c0d915391
commit e3e2bece6b
25 changed files with 407 additions and 70 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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,
}));
},

View File

@@ -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);
};

View File

@@ -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) && (

View File

@@ -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) && (

View File

@@ -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')}>

View File

@@ -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 = {

View File

@@ -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: 'jdoe@example.com' }));
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('jdoe@example.com')).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);
});

View File

@@ -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>
);

View File

@@ -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 },
},

View File

@@ -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 },
},

View File

@@ -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 },
},

View File

@@ -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 },
},

View File

@@ -26,4 +26,5 @@ export default interface Owner {
id: number;
last_name?: string;
full_name?: string;
email?: string;
}

View File

@@ -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>,

View File

@@ -167,6 +167,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
"owners.first_name",
"owners.id",
"owners.last_name",
"owners.email",
"dashboards.id",
"dashboards.dashboard_title",
"params",

View File

@@ -1671,6 +1671,7 @@ class UserSchema(Schema):
id = fields.Int()
first_name = fields.String()
last_name = fields.String()
email = fields.String()
class DashboardSchema(Schema):

View File

@@ -197,6 +197,7 @@ BASE_LIST_COLUMNS = [
"owners.id",
"owners.first_name",
"owners.last_name",
"owners.email",
"roles.id",
"roles.name",
"is_managed_externally",

View File

@@ -414,6 +414,7 @@ class DatasetColumnDrillInfoSchema(Schema):
class UserSchema(Schema):
first_name = fields.String()
last_name = fields.String()
email = fields.String()
class DatasetDrillInfoSchema(Schema):

View File

@@ -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",

View File

@@ -1035,6 +1035,7 @@ class TestChartApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCase):
"id": 1,
"first_name": "admin",
"last_name": "user",
"email": "admin@fab.org",
}
],
"params": None,

View File

@@ -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": "admin@fab.org",
"first_name": "admin",
"id": 1,
"last_name": "user",
} in data["result"]["owners"]
assert {
"email": "alpha@fab.org",
"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