Skip to content
Snippets Groups Projects
Commit d789cea6 authored by Lewis Liu's avatar Lewis Liu
Browse files

Added error and loading indicators for fields list editor

parent ccdbc242
Branches
Tags
No related merge requests found
......@@ -40,7 +40,7 @@ export const fetchData = async ({dispatch, getState, requestStatePath, existingS
const requestState = i.getIn(getState(), ["requests", ...statePath]);
if (!requestState || requestState.error || reload) {
dispatch(setRequestState({ statePath, state: "LOADING" }));
const data = await getData(existingData);
const data = await getData();
dispatch(setRequestState({ statePath, state: "LOADED" }));
return data;
......@@ -62,7 +62,7 @@ export const updateData = async ({dispatch, getState, requestStatePath, existing
const requestState = i.getIn(getState(), ["requests", ...statePath]);
dispatch(setRequestState({ statePath, state: "LOADING" }));
const data = await putData(existingData);
const data = await putData();
dispatch(setRequestState({ statePath, state: "LOADED" }));
return data;
......@@ -94,10 +94,11 @@ export const updateMetric = createThunkAction(UPDATE_METRIC, function(metric) {
return async (dispatch, getState) => {
const requestStatePath = ["metadata", "metrics", metric.id];
const existingStatePath = ["metadata", "metrics"];
const putData = async (existingMetrics) => {
const putData = async () => {
//FIXME: need to clear requestState for revisions for it to reload
const updatedMetric = await MetricApi.update(metric);
const cleanMetric = cleanResource(updatedMetric);
const existingMetrics = i.getIn(getState(), existingStatePath);
const existingMetric = existingMetrics[metric.id];
const mergedMetric = {...existingMetric, ...cleanMetric};
......@@ -117,23 +118,15 @@ const metrics = handleActions({
const FETCH_LISTS = "metabase/metadata/FETCH_LISTS";
export const fetchLists = createThunkAction(FETCH_LISTS, (reload = false) => {
return async (dispatch, getState) => {
console.log('test')
const promise = new Promise((resolve) => setTimeout(() => resolve(), 10000));
console.log('test')
console.log('test')
await promise;
// const requestStatePath = ["metadata", "lists"];
// const existingStatePath = requestStatePath;
// const getData = async () => {
// const lists = await SegmentApi.list();
// const listMap = resourceListToMap(lists);
// return listMap;
// };
//
// return await fetchData({dispatch, getState, requestStatePath, existingStatePath, getData, reload});
const requestStatePath = ["metadata", "lists"];
const existingStatePath = requestStatePath;
const getData = async () => {
const lists = await SegmentApi.list();
const listMap = resourceListToMap(lists);
return listMap;
};
return await fetchData({dispatch, getState, requestStatePath, existingStatePath, getData, reload});
};
});
......@@ -142,9 +135,10 @@ export const updateList = createThunkAction(UPDATE_LIST, function(list) {
return async (dispatch, getState) => {
const requestStatePath = ["metadata", "lists", list.id];
const existingStatePath = ["metadata", "lists"];
const putData = async (existingLists) => {
const putData = async () => {
const updatedList = await SegmentApi.update(list);
const cleanList = cleanResource(updatedList);
const existingLists = i.getIn(getState(), existingStatePath);
const existingList = existingLists[list.id];
const mergedList = {...existingList, ...cleanList};
......@@ -166,9 +160,11 @@ export const fetchDatabases = createThunkAction(FETCH_DATABASES, (reload = false
return async (dispatch, getState) => {
const requestStatePath = ["metadata", "databases"];
const existingStatePath = requestStatePath;
const getData = async (existingDatabases) => {
const getData = async () => {
const databases = await MetabaseApi.db_list();
const databaseMap = resourceListToMap(databases);
const existingDatabases = i.getIn(getState(), existingStatePath);
// to ensure existing databases with fetched metadata doesn't get
// overwritten when loading out of order, unless explicitly reloading
return {...databaseMap, ...existingDatabases};
......@@ -181,17 +177,16 @@ export const fetchDatabases = createThunkAction(FETCH_DATABASES, (reload = false
const FETCH_DATABASE_METADATA = "metabase/metadata/FETCH_DATABASE_METADATA";
export const fetchDatabaseMetadata = createThunkAction(FETCH_DATABASE_METADATA, function(dbId, reload = false) {
return async function(dispatch, getState) {
throw new Error('test');
// const requestStatePath = ["metadata", "databases", dbId];
// const existingStatePath = ["metadata"];
// const getData = async (existingMetadata) => {
// const databaseMetadata = await MetabaseApi.db_metadata({ dbId });
// await augmentDatabase(databaseMetadata);
//
// return normalize(databaseMetadata, database).entities;
// };
//
// return await fetchData({dispatch, getState, requestStatePath, existingStatePath, getData, reload});
const requestStatePath = ["metadata", "databases", dbId];
const existingStatePath = ["metadata"];
const getData = async () => {
const databaseMetadata = await MetabaseApi.db_metadata({ dbId });
await augmentDatabase(databaseMetadata);
return normalize(databaseMetadata, database).entities;
};
return await fetchData({dispatch, getState, requestStatePath, existingStatePath, getData, reload});
};
});
......@@ -200,13 +195,14 @@ export const updateDatabase = createThunkAction(UPDATE_DATABASE, function(databa
return async (dispatch, getState) => {
const requestStatePath = ["metadata", "databases", database.id];
const existingStatePath = ["metadata", "databases"];
const putData = async (existingDatabases) => {
const putData = async () => {
// make sure we don't send all the computed metadata
// there may be more that I'm missing?
const slimDatabase = _.omit(database, "tables", "tables_lookup");
const updatedDatabase = await MetabaseApi.db_update(slimDatabase);
const cleanDatabase = cleanResource(updatedDatabase);
const existingDatabases = i.getIn(getState(), existingStatePath);
const existingDatabase = existingDatabases[database.id];
const mergedDatabase = {...existingDatabase, ...cleanDatabase};
......@@ -229,13 +225,14 @@ export const updateTable = createThunkAction(UPDATE_TABLE, function(table) {
return async (dispatch, getState) => {
const requestStatePath = ["metadata", "tables", table.id];
const existingStatePath = ["metadata", "tables"];
const putData = async (existingTables) => {
const putData = async () => {
// make sure we don't send all the computed metadata
const slimTable = _.omit(table, "fields", "fields_lookup", "aggregation_options", "breakout_options", "metrics", "segments");
const updatedTable = await MetabaseApi.table_update(slimTable);
const cleanTable = cleanResource(updatedTable);
const existingTables = i.getIn(getState(), existingStatePath);
const existingTable = existingTables[table.id];
const mergedTable = {...existingTable, ...cleanTable};
......@@ -251,10 +248,11 @@ const FETCH_TABLE_METADATA = "metabase/metadata/FETCH_TABLE_METADATA";
export const fetchTableMetadata = createThunkAction(FETCH_TABLE_METADATA, function(tableId, reload = false) {
return async function(dispatch, getState) {
const requestStatePath = ["metadata", "tables", tableId];
const existingStatePath = ["metadata", "tables"];
const getData = async (existingTables) => {
if (existingTables[tableId]) {
return existingTables;
const existingStatePath = ["metadata"];
const getData = async () => {
const existingMetadata = i.getIn(getState(), existingStatePath);
if (i.getIn(existingMetadata, ['tables', tableId])) {
return existingMetadata;
}
const tableMetadata = await MetabaseApi.table_query_metadata({ tableId });
await augmentTable(tableMetadata);
......@@ -277,13 +275,14 @@ export const updateField = createThunkAction(UPDATE_FIELD, function(field) {
return async function(dispatch, getState) {
const requestStatePath = ["metadata", "fields", field.id];
const existingStatePath = ["metadata", "fields"];
const putData = async (existingFields) => {
const putData = async () => {
// make sure we don't send all the computed metadata
// there may be more that I'm missing?
const slimField = _.omit(field, "operators_lookup");
const fieldMetadata = await MetabaseApi.field_update(slimField);
const cleanField = cleanResource(fieldMetadata);
const existingFields = i.getIn(getState(), existingStatePath);
const existingField = existingFields[field.id];
const mergedField = {...existingField, ...cleanField};
......@@ -306,11 +305,12 @@ export const fetchRevisions = createThunkAction(FETCH_REVISIONS, (type, id, relo
return async (dispatch, getState) => {
const requestStatePath = ["metadata", "revisions", type, id];
const existingStatePath = ["metadata", "revisions"];
const getData = async (existingRevisions) => {
const getData = async () => {
const revisionType = type === 'list' ? 'segment' : type;
const revisions = await RevisionApi.get({id, entity: revisionType});
const revisionMap = resourceListToMap(revisions);
const existingRevisions = i.getIn(getState(), existingStatePath);
return i.assocIn(existingRevisions, [type, id], revisionMap);
};
......@@ -350,20 +350,11 @@ export const fetchListFields = createThunkAction(FETCH_LIST_FIELDS, (listId, rel
const FETCH_LIST_REVISIONS = "metabase/metadata/FETCH_LIST_REVISIONS";
export const fetchListRevisions = createThunkAction(FETCH_LIST_REVISIONS, (listId, reload = false) => {
return async (dispatch, getState) => {
console.log('test')
const promise = new Promise((resolve) => setTimeout(() => resolve(), 10000));
console.log('test')
console.log('test')
await promise;
// dispatch(fetchRevisions('list', listId));
// await dispatch(fetchLists());
// const list = i.getIn(getState(), ['metadata', 'lists', listId]);
// const tableId = list.table_id;
// await dispatch(fetchTableMetadata(tableId));
dispatch(fetchRevisions('list', listId));
await dispatch(fetchLists());
const list = i.getIn(getState(), ['metadata', 'lists', listId]);
const tableId = list.table_id;
await dispatch(fetchTableMetadata(tableId));
};
});
......
......@@ -30,21 +30,17 @@ import {
import * as metadataActions from 'metabase/redux/metadata';
import * as actions from 'metabase/reference/reference';
const mapStateToProps = (state, props) => {
console.log(state)
console.log(getError(state))
return {
section: getSection(state),
entity: getData(state) || {},
loading: getLoading(state),
// naming this 'error' will conflict with redux form
loadingError: getError(state),
user: getUser(state),
isEditing: getIsEditing(state),
hasDisplayName: getHasDisplayName(state),
hasRevisionHistory: getHasRevisionHistory(state)
}
};
const mapStateToProps = (state, props) => ({
section: getSection(state),
entity: getData(state) || {},
loading: getLoading(state),
// naming this 'error' will conflict with redux form
loadingError: getError(state),
user: getUser(state),
isEditing: getIsEditing(state),
hasDisplayName: getHasDisplayName(state),
hasRevisionHistory: getHasRevisionHistory(state)
});
const mapDispatchToProps = {
...metadataActions,
......@@ -91,7 +87,24 @@ export default class EntityItem extends Component {
} = this.props;
return (
<div style={style} className="full">
<form style={style} className="full"
onSubmit={handleSubmit(async fields => {
const editedFields = Object.keys(fields)
.filter(key => fields[key] !== undefined)
.reduce((map, key) => i.assoc(map, key, fields[key]), {});
const newEntity = {...entity, ...editedFields};
startLoading();
try {
await this.props[section.update](newEntity);
}
catch(error) {
setError(error);
console.error(error);
}
endLoading();
endEditing();
})}
>
{ isEditing &&
<div className={R.subheader}>
<div>
......@@ -163,82 +176,63 @@ export default class EntityItem extends Component {
</div>
<LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
{ () =>
<form className="flex"
onSubmit={handleSubmit(async fields => {
const editedFields = Object.keys(fields)
.filter(key => fields[key] !== undefined)
.reduce((map, key) => i.assoc(map, key, fields[key]), {});
const newEntity = {...entity, ...editedFields};
startLoading();
try {
await this.props[section.update](newEntity);
}
catch(error) {
setError(error);
console.error(error);
}
endLoading();
endEditing();
})}
>
<div className="wrapper wrapper--trim">
<List>
<div className="wrapper wrapper--trim">
<List>
<li className="relative">
<Item
id="description"
name="Description"
description={entity.description}
placeholder="No description yet"
isEditing={isEditing}
field={description}
/>
</li>
{ hasDisplayName && !isEditing &&
<li className="relative">
<Item
id="description"
name="Description"
description={entity.description}
placeholder="No description yet"
isEditing={isEditing}
field={description}
name="Actual name in database"
description={entity.name}
/>
</li>
{ hasDisplayName && !isEditing &&
<li className="relative">
<Item
name="Actual name in database"
description={entity.name}
/>
</li>
}
{ hasRevisionHistory && isEditing &&
<li className="relative">
<Item
id="revision_message"
name="Reason for changes"
description=""
placeholder="Leave a note to explain what changes you made and why they were required."
isEditing={isEditing}
field={revision_message}
/>
</li>
}
<li className="relative">
<Item
id="points_of_interest"
name={`Why this ${section.type} is interesting`}
description={entity.points_of_interest}
placeholder="Nothing interesting yet"
isEditing={isEditing}
field={points_of_interest}
/>
</li>
}
{ hasRevisionHistory && isEditing &&
<li className="relative">
<Item
id="caveats"
name={`Things to be aware of about this ${section.type}`}
description={entity.caveats}
placeholder="Nothing to be aware of yet"
id="revision_message"
name="Reason for changes"
description=""
placeholder="Leave a note to explain what changes you made and why they were required."
isEditing={isEditing}
field={caveats}
field={revision_message}
/>
</li>
</List>
</div>
</form>
}
<li className="relative">
<Item
id="points_of_interest"
name={`Why this ${section.type} is interesting`}
description={entity.points_of_interest}
placeholder="Nothing interesting yet"
isEditing={isEditing}
field={points_of_interest}
/>
</li>
<li className="relative">
<Item
id="caveats"
name={`Things to be aware of about this ${section.type}`}
description={entity.caveats}
placeholder="Nothing to be aware of yet"
isEditing={isEditing}
field={caveats}
/>
</li>
</List>
</div>
}
</LoadingAndErrorWrapper>
</div>
</form>
)
}
}
......@@ -89,6 +89,9 @@ export default class ReferenceEntityList extends Component {
isEditing,
startEditing,
endEditing,
startLoading,
endLoading,
setError,
updateField,
handleSubmit,
submitting
......@@ -100,7 +103,34 @@ export default class ReferenceEntityList extends Component {
};
return (
<div style={style} className="full">
<form style={style} className="full"
onSubmit={handleSubmit(async formFields => {
const updatedFields = Object.keys(formFields)
.map(fieldId => ({
field: entities[fieldId],
formField: Object.keys(formFields[fieldId])
.filter(key => formFields[fieldId][key] !== undefined)
.reduce((map, key) => i
.assoc(map, key, formFields[fieldId][key]), {}
)
}))
.filter(({field, formField}) => Object
.keys(formField).length !== 0
)
.map(({field, formField}) => ({...field, ...formField}));
startLoading();
try {
await Promise.all(updatedFields.map(updateField));
}
catch(error) {
setError(error);
console.error(error);
}
endLoading();
endEditing();
})}
>
{ isEditing &&
<div className={R.subheader}>
<div>
......@@ -154,70 +184,43 @@ export default class ReferenceEntityList extends Component {
</div>
<LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
{ () => Object.keys(entities).length > 0 ?
<form
onSubmit={handleSubmit(async formFields => {
const updatedFields = Object.keys(formFields)
.map(fieldId => ({
field: entities[fieldId],
formField: Object.keys(formFields[fieldId])
.filter(key => formFields[fieldId][key] !== undefined)
.reduce((map, key) => i
.assoc(map, key, formFields[fieldId][key]), {}
)
}))
.filter(({field, formField}) => Object
.keys(formField).length !== 0
)
.map(({field, formField}) => ({...field, ...formField}));
// FIXME: using Promise.all here makes values not update immediately
// could be due to the fact that metadata actions get existing data too early
await updatedFields
.reduce((promise, field) => promise
.then(() => updateField(field)),
Promise.resolve()
);
endEditing();
})}
>
<div className="wrapper wrapper--trim">
<div className={cx(S.item, F.field)}>
<div className={S.leftIcons}>
</div>
<div className={cx(S.itemTitle, F.fieldName)}>
Field name
</div>
<div className={cx(S.itemTitle, F.fieldType)}>
Field type
</div>
<div className={cx(S.itemTitle, F.fieldDataType)}>
Data type
</div>
<div className="wrapper wrapper--trim">
<div className={cx(S.item, F.field)}>
<div className={S.leftIcons}>
</div>
<div className={cx(S.itemTitle, F.fieldName)}>
Field name
</div>
<div className={cx(S.itemTitle, F.fieldType)}>
Field type
</div>
<div className={cx(S.itemTitle, F.fieldDataType)}>
Data type
</div>
<List>
{ Object.values(entities).map(entity =>
entity && entity.id && entity.name &&
<li className="relative" key={entity.id}>
<Field
field={entity}
foreignKeys={foreignKeys}
url={`${section.id}/${entity.id}`}
icon="star"
isEditing={isEditing}
formField={fields[entity.id]}
/>
</li>
)}
</List>
</div>
</form>
<List>
{ Object.values(entities).map(entity =>
entity && entity.id && entity.name &&
<li className="relative" key={entity.id}>
<Field
field={entity}
foreignKeys={foreignKeys}
url={`${section.id}/${entity.id}`}
icon="star"
isEditing={isEditing}
formField={fields[entity.id]}
/>
</li>
)}
</List>
</div>
:
<div className={S.empty}>
<EmptyState message={empty.message} icon={empty.icon} />
</div>
}
</LoadingAndErrorWrapper>
</div>
</form>
)
}
}
......@@ -450,38 +450,9 @@ export const getData = (state) => {
return selector(state);
};
export const mapFetchToRequestStatePaths = (fetch) => fetch ?
Object.keys(fetch).map(key => {
switch(key) {
case 'fetchQuestions':
return ['questions', 'fetch'];
case 'fetchMetrics':
return ['metadata', 'metrics', 'fetch'];
case 'fetchRevisions':
return ['metadata', 'revisions', fetch[key][0], fetch[key][1], 'fetch'];
case 'fetchLists':
return ['metadata', 'lists', 'fetch'];
case 'fetchDatabases':
return ['metadata', 'databases', 'fetch'];
case 'fetchDatabaseMetadata':
return ['metadata', 'databases', fetch[key][0], 'fetch'];
case 'fetchTableMetadata':
return ['metadata', 'tables', fetch[key][0], 'fetch'];
default:
return [];
}
}) : [];
const getRequests = (state) => i.getIn(state, ['requests']);
const getRequestPaths = createSelector(
[getSection],
(section) => mapFetchToRequestStatePaths(section.fetch)
);
export const getLoading = (state) => state.reference.isLoading;
export const getError = (state) => {console.log(state); return state.reference.error;}
export const getError = (state) => state.reference.error;
const getBreadcrumb = (section, index, sections) => index !== sections.length - 1 ?
[section.breadcrumb, section.id] : [section.breadcrumb];
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment