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

Update Owner Editing UI (#26)

* Update table owner editing UI

* Minor code cleanup

* Address comments, rebase for new Redux state shape, add a loading spinner

* Fix typo in the word 'reload'

* tweak loading spinner use/style in modal

* cleanup

* Minor tweaks
parent 9198141c
......@@ -16,6 +16,11 @@ img.icon {
// TODO - Add other icons here
&.icon-delete {
-webkit-mask-image: url('/static/images/icons/Trash.svg');
mask-image: url('/static/images/icons/Trash.svg');
}
&.icon-database {
-webkit-mask-image: url('/static/images/icons/Database.svg');
mask-image: url('/static/images/icons/Database.svg');
......@@ -26,13 +31,18 @@ img.icon {
mask-image: url('/static/images/icons/Loader.svg');
}
&.icon-plus-circle {
-webkit-mask-image: url('/static/images/icons/Plus-Circle.svg');
mask-image: url('/static/images/icons/Plus-Circle.svg');
}
&.icon-preview {
-webkit-mask-image: url('/static/images/icons/Preview.svg');
-webkit-mask-image: url('/static/images/icons/Preview.svg');
mask-image: url('/static/images/icons/Preview.svg');
}
&.icon-right {
-webkit-mask-image: url('/static/images/icons/Right.svg');
-webkit-mask-image: url('/static/images/icons/Right.svg');
mask-image: url('/static/images/icons/Right.svg');
}
}
......
......@@ -13,13 +13,15 @@ $gradient-1: #f2f5fe;
$gradient-2: #cad6ff;
$gradient-3: #5679ff;
$gradient-4: #3250c8;
$gradient-5: #28409f;
$gray-base: #000;
$gray-darker: lighten($gray-base, 10%); // #1a1a1a
$gray-dark: lighten($gray-base, 20%); // #333
$gray: lighten($gray-base, 35%); // ##595959
$gray-light: lighten($gray-base, 55%); // #8c8c8c
$gray-lighter: lighten($gray-base, 85%); // #d9d9d9
$gray-lighter: lighten($gray-base, 75%); // #bfbfbf
$gray-lightest: lighten($gray-base, 95%); // #f2f2f2
// Scaffolding
......
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash-2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
\ No newline at end of file
import * as React from 'react';
import ReactDOM from 'react-dom';
import serialize from 'form-serialize';
import AvatarLabel, { AvatarLabelProps } from '../common/AvatarLabel';
import LoadingSpinner from '../common/LoadingSpinner';
import { Modal } from 'react-bootstrap';
import { UpdateMethod } from './types';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
const DEFAULT_ERROR_TEXT = 'There was a problem with the request, please reload the page.';
export interface DispatchFromProps {
onUpdateList: (updateArray: { method: UpdateMethod; id: string; }[], onSuccess?: () => any, onFailure?: () => any) => void;
}
export interface ComponentProps {
errorText?: string | null;
readOnly: boolean;
}
export interface StateFromProps {
isLoading: boolean;
itemProps: { [id: string]: AvatarLabelProps };
}
type OwnerEditorProps = ComponentProps & DispatchFromProps & StateFromProps;
interface OwnerEditorState {
errorText: string | null;
isLoading: boolean;
itemProps: { [id: string]: AvatarLabelProps };
readOnly: boolean;
showModal: boolean;
tempItemProps: { [id: string]: AvatarLabelProps };
}
class OwnerEditor extends React.Component<OwnerEditorProps, OwnerEditorState> {
private inputRef: React.RefObject<HTMLInputElement>;
public static defaultProps: OwnerEditorProps = {
errorText: null,
isLoading: false,
itemProps: {},
onUpdateList: () => undefined,
readOnly: true,
};
static getDerivedStateFromProps(nextProps, prevState) {
const { isLoading, itemProps, readOnly } = nextProps;
return { isLoading, itemProps, readOnly, tempItemProps: itemProps };
}
constructor(props) {
super(props);
this.state = {
errorText: props.errorText,
isLoading: props.isLoading,
itemProps: props.itemProps,
readOnly: props.readOnly,
showModal: false,
tempItemProps: props.itemProps,
};
this.inputRef = React.createRef();
}
handleShow = () => {
this.setState({ showModal: true });
}
cancelEdit = () => {
this.setState({ tempItemProps: this.state.itemProps, showModal: false });
}
saveEdit = () => {
const updateArray = [];
Object.keys(this.state.itemProps).forEach((key) => {
if (!this.state.tempItemProps.hasOwnProperty(key)) {
updateArray.push({ method: UpdateMethod.DELETE, id: key });
}
});
Object.keys(this.state.tempItemProps).forEach((key) => {
if (!this.state.itemProps.hasOwnProperty(key)) {
updateArray.push({ method: UpdateMethod.PUT, id: key });
}
});
const onSuccessCallback = () => {
this.setState({ showModal: false });
}
const onFailureCallback = () => {
this.setState({ errorText: DEFAULT_ERROR_TEXT, readOnly: true, showModal: false });
}
this.props.onUpdateList(updateArray, onSuccessCallback, onFailureCallback);
}
recordAddItem = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const value = this.inputRef.current.value;
if (value) {
this.inputRef.current.value = '';
const newTempItemProps = {
...this.state.tempItemProps,
[value]: { label: value },
}
this.setState({ tempItemProps: newTempItemProps });
}
}
recordDeleteItem = (deletedKey: string) => {
const newTempItemProps = Object.keys(this.state.tempItemProps)
.filter((key) => {
return key !== deletedKey
})
.reduce((obj, key) => {
obj[key] = this.state.tempItemProps[key];
return obj;
}, {});
this.setState({ tempItemProps: newTempItemProps });
}
renderModalBody = () => {
if (!this.state.showModal) {
return null;
}
if (this.state.isLoading) {
return (
<Modal.Body>
<LoadingSpinner/>
</Modal.Body>
)
}
return (
<Modal.Body>
<form className='component-form' onSubmit={this.recordAddItem}>
<input
id='add-item-input'
autoFocus={true}
placeholder='Enter an email address'
ref={ this.inputRef }
/>
<button className="btn btn-light add-btn" type="submit" aria-label="Add Item">
<span aria-hidden="true">Add</span>
</button>
</form>
<ul className='component-list'>
{
Object.keys(this.state.tempItemProps).map((key) => {
return (
<li key={`modal-list-item:${key}`}>
{ React.createElement(AvatarLabel, this.state.tempItemProps[key]) }
<button
className='btn delete-button'
aria-label='Delete Item'
/* tslint:disable - TODO: Investigate jsx-no-lambda rule */
onClick={() => this.recordDeleteItem(key)}
/* tslint:enable */
>
<img className='icon icon-delete'/>
</button>
</li>
);
})
}
</ul>
</Modal.Body>
);
}
render() {
let content;
if (this.state.errorText) {
return (
<div className='owner-editor-component'>
<label className="status-message">{this.state.errorText}</label>
</div>
);
}
if (this.state.itemProps.size === 0) {
content = <label className="status-message">No entries exist</label>;
}
else {
content = (
<ul className='component-list'>
{
Object.keys(this.state.itemProps).map((key) => {
return (
<li key={`list-item:${key}`}>
{ React.createElement(AvatarLabel, this.state.itemProps[key]) }
</li>
);
})
}
</ul>
);
}
return (
<div className='owner-editor-component'>
{ content }
{
!this.state.readOnly &&
<button
className='btn add-list-item'
onClick={this.handleShow}>
<img className='icon icon-plus-circle'/>
<span>Add</span>
</button>
}
<Modal className='owner-editor-modal' show={this.state.showModal} onHide={this.cancelEdit}>
<Modal.Header className="text-center" closeButton={false}>
<Modal.Title>Owned By</Modal.Title>
</Modal.Header>
{ this.renderModalBody() }
<Modal.Footer>
<button type="button" className="btn cancel-btn" onClick={this.cancelEdit}>Cancel</button>
<button type="button" className="btn save-btn" onClick={this.saveEdit}>Save</button>
</Modal.Footer>
</Modal>
</div>
);
}
}
export default OwnerEditor;
/* TODO: The button styles in this file are to be migrated to _buttons-default.scsss */
@import 'variables';
.owner-editor-component {
.btn.add-list-item {
color: $gray-light;
padding: 4px 1px;
-webkit-box-shadow: none !important;
box-shadow: none !important;
text-align: left;
height: 32px;
&:focus,
&:not(.disabled):hover,
&:not([disabled]):hover {
color: $gradient-4;
background-color: transparent;
border-color: transparent;
.icon {
background-color: $gradient-4;
}
}
}
label {
width: 100%;
}
}
.owner-editor-modal {
.component-list li {
margin: 0px;
padding-top: 4px;
padding-bottom: 4px;
padding-right: 16px;
padding-left: 16px;
&:focus,
&:hover {
background-color: $gray-lightest;
.btn.delete-button {
display: block;
}
}
.btn.delete-button {
height: 24px;
width: 24px;
margin: auto;
margin-right: 0px;
padding: 0;
display: none;
background-color: transparent;
border: none;
&:focus,
&:not(.disabled):hover,
&:not([disabled]):hover {
background-color: transparent;
border-color: transparent;
.icon {
background-color: $gradient-4;
}
}
}
}
.loading-spinner {
margin-top: auto;
}
.btn.save-btn {
color: white;
background-color: $gradient-4;
border-color: $gradient-4;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
&:focus,
&:not(.disabled):hover,
&:not([disabled]):hover {
color: white;
background-color: $gradient-5;
border-color: $gradient-5;
}
}
.btn.cancel-btn {
border-color: $gray-lighter;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
&:focus,
&:not(.disabled):hover,
&:not([disabled]):hover {
background-color: $gray-lighter;
}
}
.btn.add-btn {
margin-left: 8px;
}
.modal-body {
height: calc(100% - 150px);
margin: 15px 0px !important;
}
.modal-content {
height: fit-content;
}
.modal-dialog {
min-width: 400px;
height: fit-content;
width: fit-content;
p {
text-align: start;
margin-left: 4px;
}
}
form {
display: flex;
margin-left: 16px;
margin-right: 16px;
margin-bottom: 16px;
}
input {
border: 1px solid $gray-lighter;
border-radius: 4px;
outline: none;
padding: 4px;
width: 100%;
}
}
.status-message {
margin-left: 4px;
margin-bottom: 4px;
font-weight: normal;
}
.component-list {
list-style-type: none;
padding: 0;
margin: 0;
li {
display: flex;
margin: 1px;
&:first-child {
flex-grow: 1;
}
}
}
export enum UpdateMethod {
PUT = 'PUT',
DELETE = 'DELETE',
}
......@@ -7,7 +7,7 @@ import { GetTableDataRequest } from '../../ducks/tableMetadata/reducer';
import DataPreviewButton from '../../containers/TableDetail/DataPreviewButton';
import TableDescEditableText from '../../containers/TableDetail/TableDescEditableText';
import TableOwnerEditableList from '../../containers/TableDetail/TableOwnerEditableList';
import OwnerEditor from '../../containers/TableDetail/OwnerEditor';
import TagInput from '../../containers/TagInput';
import AppConfig from '../../../config/config';
......@@ -32,7 +32,6 @@ export interface StateFromProps {
isLoading: boolean;
statusCode?: number;
tableData: TableMetadata;
tableOwners: TableOwners;
}
export interface DispatchFromProps {
......@@ -46,7 +45,6 @@ interface TableDetailState {
isLoading: boolean;
statusCode: number;
tableData: TableMetadata;
tableOwners: TableOwners;
}
class TableDetail extends React.Component<TableDetailProps & RouteComponentProps<any>, TableDetailState> {
......@@ -71,15 +69,11 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
source: { source: '', source_type: '' },
watermarks: [],
},
tableOwners: {
isLoading: true,
owners: [],
},
};
static getDerivedStateFromProps(nextProps, prevState) {
const { isLoading, statusCode, tableData, tableOwners } = nextProps;
return { isLoading, statusCode, tableData, tableOwners };
const { isLoading, statusCode, tableData } = nextProps;
return { isLoading, statusCode, tableData };
}
constructor(props) {
......@@ -96,7 +90,6 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
isLoading: props.isLoading,
statusCode: props.statusCode,
tableData: props.tableData,
tableOwners: props.tableOwners,
}
}
......@@ -193,27 +186,18 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
createEntityCardSections = () => {
const data = this.state.tableData;
const tableOwners = this.state.tableOwners;
const entityCardSections = [];
// "Owned By" section
const listItemRenderer = (props) => {
return React.createElement(AvatarLabel, {label: props.label});
};
const listItemProps = tableOwners.owners.map((entry) => {
return { label: entry.display_name };
});
const listItemPropTypes = [{name:'email', property: 'label', type: 'text'}];
const ownerSectionRenderer = (readOnly: boolean) => {
return React.createElement(TableOwnerEditableList, {
readOnly,
listItemProps,
listItemPropTypes,
listItemRenderer,
});
const ownerSectionRenderer = () => {
return (
<OwnerEditor
readOnly={false}
/>
);
};
entityCardSections.push({'title': 'Owned By', 'contentRenderer': ownerSectionRenderer, 'isEditable': true});
entityCardSections.push({'title': 'Owned By', 'contentRenderer': ownerSectionRenderer, 'isEditable': false});
// "Frequent Users" section
......
......@@ -4,7 +4,7 @@ import Avatar from 'react-avatar';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
interface AvatarLabelProps {
export interface AvatarLabelProps {
label?: string;
src?: string;
}
......
......@@ -3,8 +3,6 @@
.avatar-label-component {
display: flex;
font-size: 12px;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
width: 80%;
word-wrap: break-word;
}
......@@ -12,7 +10,7 @@
.component-avatar {
margin-top: auto;
margin-bottom: auto;
margin-right: 5px;
margin-right: 4px;
}
.component-label {
......
import * as React from 'react';
import ReactDOM from 'react-dom';
import serialize from 'form-serialize';
import ConfirmDeleteButton from '../ConfirmDeleteButton';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
export interface DispatchFromProps {
onAddItem: (value: string, onSuccess?: () => any, onFailure?: () => any) => void;
onDeleteItem: (value: string, onSuccess?: () => any, onFailure?: () => any) => void;
}
export interface ComponentProps {
readOnly: boolean;
listItemProps?: object[];
listItemPropTypes: {name: string, property: string, type: string}[];
listItemRenderer: (props: object) => JSX.Element;
}
type EditableListProps = ComponentProps & DispatchFromProps;
interface EditableListState {
readOnly: boolean;
listItemProps: object[];
}
class EditableList extends React.Component<EditableListProps, EditableListState> {
public static defaultProps: EditableListProps = {
readOnly: true,
listItemProps: [],
listItemPropTypes: [],
listItemRenderer: null,
onAddItem: null,
onDeleteItem: null,
};
constructor(props) {
super(props);
this.state = {
readOnly: props.readOnly,
listItemProps: props.listItemProps,
};
}
deleteItem = (index) => {
if (!this.state.readOnly && this.state.listItemProps.length > 1) {
const onSuccessCallback = () => {
const newListItemProps = this.state.listItemProps.filter((item, i) => i !== index);
this.setState({ listItemProps: newListItemProps });
};
this.props.onDeleteItem(this.state.listItemProps[index]['label'], onSuccessCallback, null);
}
}
addItem = (event) => {
event.preventDefault();
const formElement = event.target;
const props = serialize(formElement, { hash: true });
const onSuccessCallback = () => {
const newListItemProps = this.state.listItemProps.concat([props]);
formElement.reset();
this.setState({ listItemProps: newListItemProps });
}
this.props.onAddItem(props.label, onSuccessCallback, null);
}
componentWillReceiveProps(newProps) {
if (newProps.readOnly !== this.state.readOnly) {
this.setState({ readOnly: newProps.readOnly })
}
}
render() {
if (this.state.readOnly && this.state.listItemProps.length === 0) {
return ( <label className="m-auto">No entries exist</label> );
}
const renderDeleteButton = !this.state.readOnly && this.state.listItemProps.length > 1;
const listGroup = this.state.listItemProps.map((props, index) => {
return (
<li key={`list-item:${index}`}>
{ React.createElement(this.props.listItemRenderer, props) }
{
renderDeleteButton &&
/* tslint:disable - TODO: Investigate jsx-no-lambda rule */
<ConfirmDeleteButton onConfirmHandler={() => this.deleteItem(index)}/>
/* tslint:enable */
}
</li>
)
});
return (
<div className='editable-list-component'>
{
this.state.listItemProps.length > 0 &&
<ul className='component-list'>
{ listGroup }
</ul>
}
{
!this.state.readOnly &&
<div>
<label className='component-form-title'>Add a new entry</label>
<form className='component-form' onSubmit={this.addItem}>
{
this.props.listItemPropTypes.map((entry, index) => {
return (
<div className="component-form-group" key={`${entry.property}:input`}>
<label className="component-form-label" htmlFor={entry.property}>{entry.name}:</label>
<input id={entry.property} name={entry.property} type={entry.type} autoFocus={index === 0}/>
</div>
)
})
}
<button className="add-button" type="submit" aria-label="Add Item">
<span aria-hidden="true">&#43;</span>
</button>
</form>
</div>
}
</div>
);
}
}
export default EditableList;
@import 'variables';
.editable-list-component button {
color: $gray-light;
height: 24px;
width: 24px;
margin: auto;
opacity: 1;
}
.editable-list-component button:hover {
color: $gradient-4;
opacity: 1;
}
.editable-list-component button:focus,
.editable-list-component input:focus {
outline: none;
box-shadow: 0 0 1pt 1pt $gradient-3;
}
.editable-list-component input:focus {
border: 1px solid rgba(255,255,255, 0.9);
}
.editable-list-component .add-button {
font-size: 20px;
font-weight: 700;
line-height: 1;
padding: 0;
cursor: pointer;
border: 0;
margin: 4px 0px 0px auto;
}
.component-form {
display: flex;
flex-direction: column;
margin-top: 4px;
}
.component-form-title {
margin: 0px;
}
.component-form-group {
display: flex;
flex-direction: column;
}
.component-form-label {
font-size: 12px;
}
.component-list {
list-style-type: none;
padding: 0;
margin: 0px 0px 10px 0px;
}
.component-list li {
display: flex;
margin: 1px;
}
.component-list li > :first-child {
flex-grow: 1;
}
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/OwnerEditor';
export const mapStateToProps = (state: GlobalState) => {
const ownerObj = state.tableMetadata.tableOwners.owners;
const items = Object.keys(ownerObj).reduce((obj, ownerId) => {
obj[ownerId] = { label: ownerObj[ownerId].display_name }
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);
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { updateTableOwner, UpdateMethod } from '../../../ducks/tableMetadata/owners/reducer';
import EditableList, { ComponentProps, DispatchFromProps } from '../../../components/common/EditableList';
function onAddItem(value, onSuccess, onFailure) {
return updateTableOwner(value, UpdateMethod.PUT, onSuccess, onFailure);
}
function onDeleteItem(value, onSuccess, onFailure) {
return updateTableOwner(value, UpdateMethod.DELETE, onSuccess, onFailure);
}
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ onAddItem, onDeleteItem } , dispatch);
};
export default connect<{}, DispatchFromProps, ComponentProps>(null, mapDispatchToProps)(EditableList);
......@@ -11,7 +11,6 @@ export const mapStateToProps = (state: GlobalState) => {
isLoading: state.tableMetadata.isLoading,
statusCode: state.tableMetadata.statusCode,
tableData: state.tableMetadata.tableData,
tableOwners: state.tableMetadata.tableOwners,
};
};
......
......@@ -21,7 +21,12 @@ export function getTableDataFromResponseData(responseData) {
* Parses the response for table metadata to return the array of table owners
*/
export function getTableOwnersFromResponseData(responseData) {
return responseData.owners;
// TODO: owner needs proper id, until then we have to remember that we are using display_name
const ownerObj = responseData.owners.reduce((resultObj, currentOwner) => {
resultObj[currentOwner.display_name] = currentOwner;
return resultObj;
}, {});
return ownerObj;
}
/**
......
......@@ -54,7 +54,7 @@ export function metadataGetTableData(action) {
};
})
.catch((error) => {
return { data: {}, owners: [], tags: [], statusCode: error.response.status };
return { data: {}, owners: {}, tags: [], statusCode: error.response.status };
});
}
......@@ -85,18 +85,31 @@ export function metadataUpdateTableDescription(description, tableData) {
}
}
export function metadataUpdateTableOwner(owner, method, tableData) {
return axios({
method,
url: `${API_PATH}/update_table_owner`,
data: {
owner,
db: tableData.database,
cluster: tableData.cluster,
schema: tableData.schema,
table: tableData.table_name,
}
export function metadataTableOwners(tableData) {
const tableParams = getTableParams(tableData);
return axios.get(`${API_PATH}/table?${tableParams}&index=&source=`).then((response) => {
return getTableOwnersFromResponseData(response.data.tableData);
})
.catch((error) => {
return {};
});
}
export function metadataUpdateTableOwner(action, tableData) {
const updatePayloads = action.updateArray.map(item => ({
method: item.method,
url: `${API_PATH}/update_table_owner`,
data: {
cluster: tableData.cluster,
db: tableData.database,
owner: item.id,
schema: tableData.schema,
table: tableData.table_name,
},
}
));
return updatePayloads.map(payload => { axios(payload) });
}
export function metadataGetColumnDescription(columnIndex, tableData) {
......
import { GetTableData, GetTableDataRequest, GetTableDataResponse } from '../reducer';
import { User } from '../../../components/TableDetail/types';
import { UpdateMethod } from '.../../../components/OwnerEditor/types';
import { User } from '.../../../components/TableDetail/types';
/* updateTableOwner */
export enum UpdateTableOwner {
......@@ -8,29 +9,28 @@ export enum UpdateTableOwner {
FAILURE = 'amundsen/tableMetadata/UPDATE_TABLE_OWNER_FAILURE',
}
export enum UpdateMethod {
PUT = 'PUT',
DELETE = 'DELETE',
interface UpdatePayload {
method: UpdateMethod;
id: string;
}
export interface UpdateTableOwnerRequest {
type: UpdateTableOwner.ACTION;
method: UpdateMethod;
value: string;
updateArray: UpdatePayload[];
onSuccess?: () => any;
onFailure?: () => any;
}
interface UpdateTableOwnerResponse {
export interface UpdateTableOwnerResponse {
type: UpdateTableOwner.SUCCESS | UpdateTableOwner.FAILURE;
payload: { [id: string] : User };
}
export function updateTableOwner(value: string, method: UpdateMethod, onSuccess?: () => any, onFailure?: () => any): UpdateTableOwnerRequest {
export function updateTableOwner(updateArray: UpdatePayload[], onSuccess?: () => any, onFailure?: () => any): UpdateTableOwnerRequest {
return {
value,
method,
onSuccess,
onFailure,
updateArray,
type: UpdateTableOwner.ACTION,
};
}
......@@ -42,21 +42,26 @@ export type TableOwnerReducerAction =
export interface TableOwnerReducerState {
isLoading: boolean;
owners: User[];
owners: { [id: string] : User };
}
export const initialOwnersState: TableOwnerReducerState = {
isLoading: true,
owners: [],
owners: {},
};
export default function reducer(state: TableOwnerReducerState = initialOwnersState, action: TableOwnerReducerAction): TableOwnerReducerState {
switch (action.type) {
case GetTableData.ACTION:
return { isLoading: true, owners: [] };
return { isLoading: true, owners: {} };
case GetTableData.FAILURE:
case GetTableData.SUCCESS:
return { isLoading: false, owners: action.payload.owners };
case UpdateTableOwner.ACTION:
return { ...state, isLoading: true };
case UpdateTableOwner.FAILURE:
case UpdateTableOwner.SUCCESS:
return { isLoading: false, owners: action.payload };
default:
return state;
}
......
import { call, select, takeEvery } from 'redux-saga/effects';
import { all, call, put, select, takeEvery } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga';
import { UpdateTableOwner, UpdateTableOwnerRequest } from './reducer';
import { metadataUpdateTableOwner } from '../api/v0';
import { metadataUpdateTableOwner, metadataTableOwners } from '../api/v0';
// updateTableOwner
export function* updateTableOwnerWorker(action: UpdateTableOwnerRequest): SagaIterator {
const state = yield select();
const tableData = state.tableMetadata.tableData;
try {
yield call(metadataUpdateTableOwner, action.value, action.method, state.tableMetadata.tableData);
yield all(metadataUpdateTableOwner(action, tableData));
const newOwners = yield call(metadataTableOwners, tableData);
yield put({ type: UpdateTableOwner.SUCCESS, payload: newOwners });
if (action.onSuccess) {
yield call(action.onSuccess);
}
} catch (e) {
yield put({ type: UpdateTableOwner.FAILURE, payload: state.tableMetadata.tableOwners.owners });
if (action.onFailure) {
yield call(action.onFailure);
}
......
/* TODO: Reorganize types to allow for some consistency with imports */
import { PreviewData, PreviewQueryParams, TableMetadata, User } from '../../components/TableDetail/types';
import { Tag } from '../../components/Tags/types';
import tableOwnersReducer, { initialOwnersState, TableOwnerReducerState } from './owners/reducer';
import tableOwnersReducer, {
initialOwnersState,
TableOwnerReducerState,
UpdateTableOwner,
UpdateTableOwnerRequest,
UpdateTableOwnerResponse,
} from './owners/reducer';
import tableTagsReducer, {
initialTagsState,
TableTagsReducerState,
......@@ -32,7 +39,7 @@ export interface GetTableDataResponse {
payload: {
statusCode: number;
data: TableMetadata;
owners: User[];
owners: { [id: string] : User };
tags: Tag[];
}
}
......@@ -216,7 +223,8 @@ export type TableMetadataReducerAction =
UpdateColumnDescriptionRequest | UpdateColumnDescriptionResponse |
GetLastIndexedRequest | GetLastIndexedResponse |
GetPreviewDataRequest | GetPreviewDataResponse |
UpdateTagsRequest | UpdateTagsResponse ;
UpdateTagsRequest | UpdateTagsResponse |
UpdateTableOwnerRequest | UpdateTableOwnerResponse ;
export interface TableMetadataReducerState {
isLoading: boolean;
......@@ -287,6 +295,10 @@ export default function reducer(state: TableMetadataReducerState = initialState,
case GetPreviewData.SUCCESS:
case GetPreviewData.FAILURE:
return { ...state, preview: action.payload };
case UpdateTableOwner.ACTION:
case UpdateTableOwner.FAILURE:
case UpdateTableOwner.SUCCESS:
return { ...state, tableOwners: tableOwnersReducer(state.tableOwners, action) };
case UpdateTags.ACTION:
case UpdateTags.FAILURE:
case UpdateTags.SUCCESS:
......
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