Unverified Commit d2fe9ce9 authored by Tamika Tannis's avatar Tamika Tannis Committed by GitHub

chore: Re-useable OwnerEditor (#548)

* Generic OwnerEditor; Create TableOwnerEditor
Signed-off-by: 's avatarTamika Tannis <ttannis@lyft.com>

* Create & use DashboardOwnerEditor
Signed-off-by: 's avatarTamika Tannis <ttannis@lyft.com>

* Code cleanup
Signed-off-by: 's avatarTamika Tannis <ttannis@lyft.com>

* Add tests + user fixtures
Signed-off-by: 's avatarTamika Tannis <ttannis@lyft.com>

* Code cleanup
Signed-off-by: 's avatarTamika Tannis <ttannis@lyft.com>
parent 4f6164d9
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { mocked } from 'ts-jest/utils';
import { indexUsersEnabled } from 'config/config-utils';
import { GlobalState } from 'ducks/rootReducer';
import globalState from 'fixtures/globalState';
import { dashboardMetadata } from 'fixtures/metadata/dashboard';
import { activeUser0 } from 'fixtures/metadata/users';
import { mapStateToProps, DASHBOARD_OWNER_SOURCE } from '.';
jest.mock('config/config-utils', () => ({
indexUsersEnabled: jest.fn(),
}));
describe('mapStateToProps', () => {
let result;
let expectedItemProps;
let mockState: GlobalState;
beforeAll(() => {
mockState = {
...globalState,
dashboard: {
dashboard: {
...dashboardMetadata,
owners: [activeUser0],
},
isLoading: false,
statusCode: 200,
},
};
});
it('returns expected itemProps when indexUsersEnabled()', () => {
mocked(indexUsersEnabled).mockImplementation(() => true);
result = mapStateToProps(mockState);
const id = activeUser0.user_id;
expectedItemProps = {
[id]: {
label: activeUser0.display_name,
link: `/user/${id}?source=${DASHBOARD_OWNER_SOURCE}`,
isExternal: false,
},
};
expect(result.itemProps).toEqual(expectedItemProps);
});
it('returns expected itemProps when !indexUsersEnabled()', () => {
mocked(indexUsersEnabled).mockImplementation(() => false);
result = mapStateToProps(mockState);
expectedItemProps = {
[activeUser0.user_id]: {
label: activeUser0.display_name,
link: activeUser0.profile_url,
isExternal: true,
},
};
expect(result.itemProps).toEqual(expectedItemProps);
});
});
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import { connect } from 'react-redux';
import { GlobalState } from 'ducks/rootReducer';
import OwnerEditor, {
ComponentProps,
StateFromProps,
OwnerItemProps,
} from 'components/common/OwnerEditor';
import { User } from 'interfaces';
import { indexUsersEnabled } from 'config/config-utils';
export const DASHBOARD_OWNER_SOURCE = 'dashboard_page_owner';
const convertDashboardOwners = (owners: User[]): OwnerItemProps => {
return owners.reduce((obj, user) => {
const { profile_url, user_id, display_name } = user;
let profileLink = profile_url;
let isExternalLink = true;
if (indexUsersEnabled()) {
isExternalLink = false;
profileLink = `/user/${user_id}?source=${DASHBOARD_OWNER_SOURCE}`;
}
obj[user_id] = {
label: display_name,
link: profileLink,
isExternal: isExternalLink,
};
return obj;
}, {});
};
export const mapStateToProps = (state: GlobalState) => {
return {
isLoading: false,
itemProps: convertDashboardOwners(state.dashboard.dashboard.owners),
};
};
export default connect<StateFromProps, {}, ComponentProps>(
mapStateToProps,
null
)(OwnerEditor);
export const TABLES_PER_PAGE = 10; export const TABLES_PER_PAGE = 10;
export const DASHBOARD_SOURCE = 'dashboard_page'; export const DASHBOARD_SOURCE = 'dashboard_page';
export const DASHBOARD_OWNER_SOURCE = 'dashboard_page_owner';
export const NO_OWNER_TEXT = 'No owner'; export const OWNER_HEADER_TEXT = 'Owners';
export const ADD_DESC_TEXT = 'Add Description in'; export const ADD_DESC_TEXT = 'Add Description in';
export const EDIT_DESC_TEXT = 'Click to edit description in'; export const EDIT_DESC_TEXT = 'Click to edit description in';
......
...@@ -20,6 +20,7 @@ import * as LogUtils from 'utils/logUtils'; ...@@ -20,6 +20,7 @@ import * as LogUtils from 'utils/logUtils';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import { BadgeStyle } from 'config/config-types'; import { BadgeStyle } from 'config/config-types';
import ChartList from './ChartList'; import ChartList from './ChartList';
import DashboardOwnerEditor from './DashboardOwnerEditor';
import ImagePreview from './ImagePreview'; import ImagePreview from './ImagePreview';
import { DashboardPage, DashboardPageProps, MatchProps } from '.'; import { DashboardPage, DashboardPageProps, MatchProps } from '.';
...@@ -211,19 +212,9 @@ describe('DashboardPage', () => { ...@@ -211,19 +212,9 @@ describe('DashboardPage', () => {
}); });
}); });
describe('renders owners', () => { it('renders owners', () => {
it('with correct AvatarLabel if no owners exist', () => { const { wrapper } = setup();
const { wrapper } = setup({ expect(wrapper.find(DashboardOwnerEditor).exists()).toBe(true);
dashboard: {
...dashboardMetadata,
owners: [],
},
});
expect(wrapper.find(AvatarLabel).props().label).toBe(
Constants.NO_OWNER_TEXT
);
});
}); });
it('renders a Flag for last run state', () => { it('renders a Flag for last run state', () => {
......
...@@ -22,6 +22,7 @@ import { GetDashboardRequest } from 'ducks/dashboard/types'; ...@@ -22,6 +22,7 @@ import { GetDashboardRequest } from 'ducks/dashboard/types';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
import { logClick } from 'ducks/utilMethods'; import { logClick } from 'ducks/utilMethods';
import { DashboardMetadata } from 'interfaces/Dashboard'; import { DashboardMetadata } from 'interfaces/Dashboard';
import DashboardOwnerEditor from 'components/DashboardPage/DashboardOwnerEditor';
import QueryList from 'components/DashboardPage/QueryList'; import QueryList from 'components/DashboardPage/QueryList';
import ChartList from 'components/DashboardPage/ChartList'; import ChartList from 'components/DashboardPage/ChartList';
import { formatDateTimeShort } from 'utils/dateUtils'; import { formatDateTimeShort } from 'utils/dateUtils';
...@@ -29,10 +30,9 @@ import ResourceList from 'components/common/ResourceList'; ...@@ -29,10 +30,9 @@ import ResourceList from 'components/common/ResourceList';
import { import {
ADD_DESC_TEXT, ADD_DESC_TEXT,
EDIT_DESC_TEXT, EDIT_DESC_TEXT,
DASHBOARD_OWNER_SOURCE,
DASHBOARD_SOURCE, DASHBOARD_SOURCE,
LAST_RUN_SUCCEEDED, LAST_RUN_SUCCEEDED,
NO_OWNER_TEXT, OWNER_HEADER_TEXT,
TABLES_PER_PAGE, TABLES_PER_PAGE,
} from 'components/DashboardPage/constants'; } from 'components/DashboardPage/constants';
import TagInput from 'components/common/Tags/TagInput'; import TagInput from 'components/common/Tags/TagInput';
...@@ -240,27 +240,9 @@ export class DashboardPage extends React.Component< ...@@ -240,27 +240,9 @@ export class DashboardPage extends React.Component<
</EditableSection> </EditableSection>
<section className="column-layout-2"> <section className="column-layout-2">
<section className="left-panel"> <section className="left-panel">
<section className="metadata-section"> <EditableSection title={OWNER_HEADER_TEXT} readOnly>
<div className="section-title title-3">Owners</div> <DashboardOwnerEditor resourceType={ResourceType.dashboard} />
<div> </EditableSection>
{dashboard.owners.length > 0 &&
dashboard.owners.map((owner) => (
<Link
key={owner.user_id}
to={`/user/${owner.user_id}?source=${DASHBOARD_OWNER_SOURCE}`}
>
<AvatarLabel label={owner.display_name} />
</Link>
))}
{dashboard.owners.length === 0 && (
<AvatarLabel
avatarClass="gray-avatar"
labelClass="text-placeholder"
label={NO_OWNER_TEXT}
/>
)}
</div>
</section>
<section className="metadata-section"> <section className="metadata-section">
<div className="section-title title-3">Created</div> <div className="section-title title-3">Created</div>
<time className="body-2 text-primary"> <time className="body-2 text-primary">
......
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { mocked } from 'ts-jest/utils';
import { indexUsersEnabled } from 'config/config-utils';
import { GlobalState } from 'ducks/rootReducer';
import globalState from 'fixtures/globalState';
import { activeUser0 } from 'fixtures/metadata/users';
import { mapDispatchToProps, mapStateToProps } from '.';
jest.mock('config/config-utils', () => ({
indexUsersEnabled: jest.fn(),
}));
describe('mapStateToProps', () => {
let result;
let expectedItemProps;
let mockState: GlobalState;
beforeAll(() => {
mockState = {
...globalState,
tableMetadata: {
...globalState.tableMetadata,
tableOwners: {
owners: {
[activeUser0.user_id]: activeUser0,
},
isLoading: false,
},
},
};
});
it('returns expected itemProps when indexUsersEnabled()', () => {
mocked(indexUsersEnabled).mockImplementation(() => true);
result = mapStateToProps(mockState);
const id = activeUser0.user_id;
expectedItemProps = {
[id]: {
label: activeUser0.display_name,
link: `/user/${id}?source=owned_by`,
isExternal: false,
},
};
expect(result.itemProps).toEqual(expectedItemProps);
});
it('returns expected itemProps when !indexUsersEnabled()', () => {
mocked(indexUsersEnabled).mockImplementation(() => false);
result = mapStateToProps(mockState);
expectedItemProps = {
[activeUser0.user_id]: {
label: activeUser0.display_name,
link: activeUser0.profile_url,
isExternal: true,
},
};
expect(result.itemProps).toEqual(expectedItemProps);
});
});
describe('mapDispatchToProps', () => {
let dispatch;
let result;
beforeAll(() => {
dispatch = jest.fn(() => Promise.resolve());
result = mapDispatchToProps(dispatch);
});
it('sets onUpdateList props to trigger desired action', () => {
expect(result.onUpdateList).toBeInstanceOf(Function);
});
});
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { GlobalState } from 'ducks/rootReducer';
import { updateTableOwner } from 'ducks/tableMetadata/owners/reducer';
import OwnerEditor, {
ComponentProps,
DispatchFromProps,
StateFromProps,
} from 'components/common/OwnerEditor';
import { indexUsersEnabled } from 'config/config-utils';
export const mapStateToProps = (state: GlobalState) => {
const ownerObj = state.tableMetadata.tableOwners.owners;
const items = Object.keys(ownerObj).reduce((obj, ownerId) => {
const { profile_url, user_id, display_name } = ownerObj[ownerId];
let profileLink = profile_url;
let isExternalLink = true;
if (indexUsersEnabled()) {
isExternalLink = false;
profileLink = `/user/${user_id}?source=owned_by`;
}
obj[ownerId] = {
label: display_name,
link: profileLink,
isExternal: isExternalLink,
};
return obj;
}, {});
return {
isLoading: state.tableMetadata.tableOwners.isLoading,
itemProps: items,
};
};
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ onUpdateList: updateTableOwner }, dispatch);
};
export default connect<StateFromProps, DispatchFromProps, ComponentProps>(
mapStateToProps,
mapDispatchToProps
)(OwnerEditor);
...@@ -34,7 +34,7 @@ import DataPreviewButton from 'components/TableDetail/DataPreviewButton'; ...@@ -34,7 +34,7 @@ import DataPreviewButton from 'components/TableDetail/DataPreviewButton';
import ExploreButton from 'components/TableDetail/ExploreButton'; import ExploreButton from 'components/TableDetail/ExploreButton';
import FrequentUsers from 'components/TableDetail/FrequentUsers'; import FrequentUsers from 'components/TableDetail/FrequentUsers';
import LineageLink from 'components/TableDetail/LineageLink'; import LineageLink from 'components/TableDetail/LineageLink';
import OwnerEditor from 'components/TableDetail/OwnerEditor'; import TableOwnerEditor from 'components/TableDetail/TableOwnerEditor';
import SourceLink from 'components/TableDetail/SourceLink'; import SourceLink from 'components/TableDetail/SourceLink';
import TableDashboardResourceList from 'components/TableDetail/TableDashboardResourceList'; import TableDashboardResourceList from 'components/TableDetail/TableDashboardResourceList';
import TableDescEditableText from 'components/TableDetail/TableDescEditableText'; import TableDescEditableText from 'components/TableDetail/TableDescEditableText';
...@@ -319,7 +319,7 @@ export class TableDetail extends React.Component< ...@@ -319,7 +319,7 @@ export class TableDetail extends React.Component<
/> />
</EditableSection> </EditableSection>
<EditableSection title="Owners"> <EditableSection title="Owners">
<OwnerEditor /> <TableOwnerEditor resourceType={ResourceType.table} />
</EditableSection> </EditableSection>
{this.renderProgrammaticDesc( {this.renderProgrammaticDesc(
data.programmatic_descriptions.right data.programmatic_descriptions.right
......
...@@ -5,12 +5,9 @@ import { mount } from 'enzyme'; ...@@ -5,12 +5,9 @@ import { mount } from 'enzyme';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import AvatarLabel from 'components/common/AvatarLabel'; import AvatarLabel from 'components/common/AvatarLabel';
import { import { ResourceType } from 'interfaces';
OwnerEditor,
OwnerEditorProps, import { OwnerEditor, OwnerEditorProps } from '.';
mapStateToProps,
mapDispatchToProps,
} from '.';
import * as Constants from './constants'; import * as Constants from './constants';
describe('OwnerEditor', () => { describe('OwnerEditor', () => {
...@@ -23,6 +20,7 @@ describe('OwnerEditor', () => { ...@@ -23,6 +20,7 @@ describe('OwnerEditor', () => {
setEditMode: jest.fn(), setEditMode: jest.fn(),
onUpdateList: jest.fn(), onUpdateList: jest.fn(),
readOnly: null, readOnly: null,
resourceType: ResourceType.table,
...propOverrides, ...propOverrides,
}; };
const wrapper = mount<OwnerEditor>(<OwnerEditor {...props} />); const wrapper = mount<OwnerEditor>(<OwnerEditor {...props} />);
......
...@@ -2,21 +2,17 @@ ...@@ -2,21 +2,17 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import * as React from 'react'; import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import AppConfig from 'config/config'; import AppConfig from 'config/config';
import AvatarLabel, { AvatarLabelProps } from 'components/common/AvatarLabel'; import AvatarLabel, { AvatarLabelProps } from 'components/common/AvatarLabel';
import LoadingSpinner from 'components/common/LoadingSpinner'; import LoadingSpinner from 'components/common/LoadingSpinner';
import { Modal } from 'react-bootstrap'; import { Modal } from 'react-bootstrap';
import { UpdateMethod, UpdateOwnerPayload } from 'interfaces'; import { ResourceType, UpdateMethod, UpdateOwnerPayload } from 'interfaces';
// TODO: Use css-modules instead of 'import' // TODO: Use css-modules instead of 'import'
import './styles.scss'; import './styles.scss';
import { GlobalState } from 'ducks/rootReducer';
import { updateTableOwner } from 'ducks/tableMetadata/owners/reducer';
import { EditableSectionChildProps } from 'components/common/EditableSection'; import { EditableSectionChildProps } from 'components/common/EditableSection';
import { logClick } from 'ducks/utilMethods'; import { logClick } from 'ducks/utilMethods';
...@@ -32,6 +28,7 @@ export interface DispatchFromProps { ...@@ -32,6 +28,7 @@ export interface DispatchFromProps {
export interface ComponentProps { export interface ComponentProps {
errorText?: string | null; errorText?: string | null;
resourceType: ResourceType;
} }
interface OwnerAvatarLabelProps extends AvatarLabelProps { interface OwnerAvatarLabelProps extends AvatarLabelProps {
...@@ -41,7 +38,7 @@ interface OwnerAvatarLabelProps extends AvatarLabelProps { ...@@ -41,7 +38,7 @@ interface OwnerAvatarLabelProps extends AvatarLabelProps {
export interface StateFromProps { export interface StateFromProps {
isLoading: boolean; isLoading: boolean;
itemProps: { [id: string]: OwnerAvatarLabelProps }; itemProps: OwnerItemProps;
} }
export type OwnerEditorProps = ComponentProps & export type OwnerEditorProps = ComponentProps &
...@@ -49,9 +46,11 @@ export type OwnerEditorProps = ComponentProps & ...@@ -49,9 +46,11 @@ export type OwnerEditorProps = ComponentProps &
StateFromProps & StateFromProps &
EditableSectionChildProps; EditableSectionChildProps;
export type OwnerItemProps = { [id: string]: OwnerAvatarLabelProps };
interface OwnerEditorState { interface OwnerEditorState {
errorText: string | null; errorText: string | null;
itemProps: { [id: string]: OwnerAvatarLabelProps }; itemProps: OwnerItemProps;
tempItemProps: { [id: string]: AvatarLabelProps }; tempItemProps: { [id: string]: AvatarLabelProps };
} }
...@@ -61,7 +60,7 @@ export class OwnerEditor extends React.Component< ...@@ -61,7 +60,7 @@ export class OwnerEditor extends React.Component<
> { > {
private inputRef: React.RefObject<HTMLInputElement>; private inputRef: React.RefObject<HTMLInputElement>;
public static defaultProps: OwnerEditorProps = { public static defaultProps: Partial<OwnerEditorProps> = {
errorText: null, errorText: null,
isLoading: false, isLoading: false,
itemProps: {}, itemProps: {},
...@@ -205,6 +204,7 @@ export class OwnerEditor extends React.Component< ...@@ -205,6 +204,7 @@ export class OwnerEditor extends React.Component<
}; };
render() { render() {
const { isEditing, readOnly, resourceType } = this.props;
const hasItems = Object.keys(this.state.itemProps).length > 0; const hasItems = Object.keys(this.state.itemProps).length > 0;
if (this.state.errorText) { if (this.state.errorText) {
...@@ -229,7 +229,7 @@ export class OwnerEditor extends React.Component< ...@@ -229,7 +229,7 @@ export class OwnerEditor extends React.Component<
<a <a
href={owner.link} href={owner.link}
target="_blank" target="_blank"
id={`table-owners:${key}`} id={`${resourceType}-owners:${key}`}
onClick={logClick} onClick={logClick}
rel="noopener noreferrer" rel="noopener noreferrer"
> >
...@@ -240,7 +240,7 @@ export class OwnerEditor extends React.Component< ...@@ -240,7 +240,7 @@ export class OwnerEditor extends React.Component<
listItem = ( listItem = (
<Link <Link
to={owner.link} to={owner.link}
id={`table-owners:${key}`} id={`${resourceType}-owners:${key}`}
onClick={logClick} onClick={logClick}
> >
{avatarLabel} {avatarLabel}
...@@ -255,14 +255,14 @@ export class OwnerEditor extends React.Component< ...@@ -255,14 +255,14 @@ export class OwnerEditor extends React.Component<
return ( return (
<div className="owner-editor-component"> <div className="owner-editor-component">
{ownerList} {ownerList}
{this.props.readOnly && !hasItems && ( {readOnly && !hasItems && (
<AvatarLabel <AvatarLabel
avatarClass="gray-avatar" avatarClass="gray-avatar"
labelClass="text-placeholder" labelClass="text-placeholder"
label={Constants.NO_OWNER_TEXT} label={Constants.NO_OWNER_TEXT}
/> />
)} )}
{!this.props.readOnly && !hasItems && ( {!readOnly && !hasItems && (
<button <button
type="button" type="button"
className="btn btn-flat-icon add-item-button" className="btn btn-flat-icon add-item-button"
...@@ -272,10 +272,10 @@ export class OwnerEditor extends React.Component< ...@@ -272,10 +272,10 @@ export class OwnerEditor extends React.Component<
<span>{Constants.ADD_OWNER}</span> <span>{Constants.ADD_OWNER}</span>
</button> </button>
)} )}
{!this.props.readOnly && ( {!readOnly && (
<Modal <Modal
className="owner-editor-modal" className="owner-editor-modal"
show={this.props.isEditing} show={isEditing}
onHide={this.cancelEdit} onHide={this.cancelEdit}
> >
<Modal.Header className="text-center" closeButton={false}> <Modal.Header className="text-center" closeButton={false}>
...@@ -305,35 +305,4 @@ export class OwnerEditor extends React.Component< ...@@ -305,35 +305,4 @@ export class OwnerEditor extends React.Component<
} }
} }
export const mapStateToProps = (state: GlobalState) => { export default OwnerEditor;
const ownerObj = state.tableMetadata.tableOwners.owners;
const items = Object.keys(ownerObj).reduce((obj, ownerId) => {
const { profile_url, user_id, display_name } = ownerObj[ownerId];
let profileLink = profile_url;
let isExternalLink = true;
if (AppConfig.indexUsers.enabled) {
isExternalLink = false;
profileLink = `/user/${user_id}?source=owned_by`;
}
obj[ownerId] = {
label: display_name,
link: profileLink,
isExternal: isExternalLink,
};
return obj;
}, {});
return {
isLoading: state.tableMetadata.tableOwners.isLoading,
itemProps: items,
};
};
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ onUpdateList: updateTableOwner }, dispatch);
};
export default connect<StateFromProps, DispatchFromProps, ComponentProps>(
mapStateToProps,
mapDispatchToProps
)(OwnerEditor);
export const activeUser0 = {
manager_id: null,
manager_fullname: null,
manager_email: null,
profile_url: '/test0',
role_name: null,
display_name: null,
github_username: null,
team_name: null,
last_name: null,
full_name: null,
slack_id: null,
first_name: null,
employee_type: null,
other_key_values: {},
is_active: true,
email: 'user0@test.com',
user_id: 'user0',
};
export const activeUser1 = {
manager_id: null,
manager_fullname: null,
manager_email: null,
profile_url: '/test1',
role_name: null,
display_name: null,
github_username: null,
team_name: null,
last_name: null,
full_name: null,
slack_id: null,
first_name: null,
employee_type: null,
other_key_values: {},
is_active: true,
email: 'user10@test.com',
user_id: 'user1',
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment