mirror of
https://github.com/apache/superset.git
synced 2026-05-21 15:55:10 +00:00
feat(owners): display email in owner selectors (#37906)
This commit is contained in:
committed by
GitHub
parent
0c0d915391
commit
e3e2bece6b
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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) && (
|
||||
|
||||
@@ -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) && (
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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 },
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
|
||||
@@ -26,4 +26,5 @@ export default interface Owner {
|
||||
id: number;
|
||||
last_name?: string;
|
||||
full_name?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -167,6 +167,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
"owners.first_name",
|
||||
"owners.id",
|
||||
"owners.last_name",
|
||||
"owners.email",
|
||||
"dashboards.id",
|
||||
"dashboards.dashboard_title",
|
||||
"params",
|
||||
|
||||
@@ -1671,6 +1671,7 @@ class UserSchema(Schema):
|
||||
id = fields.Int()
|
||||
first_name = fields.String()
|
||||
last_name = fields.String()
|
||||
email = fields.String()
|
||||
|
||||
|
||||
class DashboardSchema(Schema):
|
||||
|
||||
@@ -197,6 +197,7 @@ BASE_LIST_COLUMNS = [
|
||||
"owners.id",
|
||||
"owners.first_name",
|
||||
"owners.last_name",
|
||||
"owners.email",
|
||||
"roles.id",
|
||||
"roles.name",
|
||||
"is_managed_externally",
|
||||
|
||||
@@ -414,6 +414,7 @@ class DatasetColumnDrillInfoSchema(Schema):
|
||||
class UserSchema(Schema):
|
||||
first_name = fields.String()
|
||||
last_name = fields.String()
|
||||
email = fields.String()
|
||||
|
||||
|
||||
class DatasetDrillInfoSchema(Schema):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1035,6 +1035,7 @@ class TestChartApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCase):
|
||||
"id": 1,
|
||||
"first_name": "admin",
|
||||
"last_name": "user",
|
||||
"email": "admin@fab.org",
|
||||
}
|
||||
],
|
||||
"params": None,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user