Skip to content

Commit

Permalink
Concept Comparison view with diff highlighting
Browse files Browse the repository at this point in the history
  • Loading branch information
snyaggarwal committed Dec 21, 2020
1 parent 5f1c13b commit aac0ad5
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 66 deletions.
9 changes: 8 additions & 1 deletion src/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import alertifyjs from 'alertifyjs';
import moment from 'moment';
import {
filter, difference, compact, find, reject, intersectionBy, size, keys, omitBy, isEmpty,
get, includes, map, isArray, values
get, includes, map, isArray, values, pick
} from 'lodash';
import { DATE_FORMAT, DATETIME_FORMAT } from './constants';

Expand Down Expand Up @@ -168,3 +168,10 @@ export const isAdminUser = () => {
const currentUser = getCurrentUser();
return get(currentUser, 'is_staff') || get(currentUser, 'is_superuser');
}

export const toObjectArray = obj => {
if(isEmpty(obj))
return [];

return map(keys(obj), k => pick(obj, k));
}
17 changes: 17 additions & 0 deletions src/components/app/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -446,3 +446,20 @@ div.login-paper {
.border-primary {
border: 1px solid rgb(51, 115, 170);
}
.diff-row {
table tbody tr td:nth-child(odd) {
display: none;
}
table tbody tr {
td pre span {
padding: 0px;
}
td:nth-child(4) {
padding-left: 16px;
background: #fff;
pre {
background: #e6ffed;
}
}
}
}
274 changes: 209 additions & 65 deletions src/components/concepts/ConceptsComparison.jsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
import React from 'react';
import {
TableContainer, Table, TableHead, TableBody, TableCell, TableRow,
Tooltip
Tooltip, CircularProgress
} from '@material-ui/core';
import { Flag as FlagIcon } from '@material-ui/icons';
import { get, startCase, map, isEmpty, includes, isEqual } from 'lodash';
import {
get, startCase, map, isEmpty, includes, isEqual, size, filter, reject, isObject, keys, values
} from 'lodash';
import APIService from '../../services/APIService';
import { formatDate } from '../../common/utils';
import { formatDate, toObjectArray } from '../../common/utils';
import ReactDiffViewer from 'react-diff-viewer';

const ATTRIBUTES = {
text1: ['datatype', 'display_locale', 'external_id'],
list: ['names', 'descriptions'],
textFormatted: ['owner'],
list: ['names', 'descriptions', 'extras'],
bool: ['retired'],
text2: ['created_by', 'updated_by'],
date: ['created_on', 'updated_on'],
}

const getLocaleLabel = locale => {
const getLocaleLabelFormatted = locale => {
if(!locale)
return ''
const nameAttr = get(locale, 'name') ? 'name' : 'description';
const typeValue = get(locale, 'name_type') || get(locale, 'description_type');
return (
<React.Fragment key={locale.uuid}>
<div className='flex-vertical-center'>
<span className='gray-italics'>
{get(locale, 'name_type') || get(locale, 'description_type')}
</span>
{
typeValue &&
<span className='gray-italics'>
{typeValue}
</span>
}
<span style={{marginLeft: '5px'}}>
{get(locale, 'name') || get(locale, 'description')}
</span>
Expand All @@ -42,6 +53,20 @@ const getLocaleLabel = locale => {
)
}

const getLocaleLabel = locale => {
if(!locale)
return '';

let typeValue = get(locale, 'name_type') || get(locale, 'description_type') || '';
if(typeValue)
typeValue += ' | '

const nameValue = get(locale, 'name') || get(locale, 'description');
const preferredText = locale.locale_preferred ? '(preferred)' : '';

return `[${locale.locale}] ${typeValue}${nameValue} ${preferredText}`
}

class ConceptsComparison extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -72,11 +97,27 @@ class ConceptsComparison extends React.Component {
if(uri && attr && loadingAttr) {
APIService.new().overrideURL(uri).get().then(response => {
if(get(response, 'status') === 200)
this.setState({[attr]: response.data, [loadingAttr]: false})
this.setState({[attr]: this.formatConcept(response.data), [loadingAttr]: false})
})
}
}

formatConcept(concept) {
concept.names = this.sortLocales(concept.names)
concept.descriptions = this.sortLocales(concept.descriptions)
concept.extras = toObjectArray(concept.extras)
return concept
}

sortLocales = locales => {
return [
...filter(locales, {name_type: 'FULLY_SPECIFIED', locale_preferred: true}),
...filter(reject(locales, {name_type: 'FULLY_SPECIFIED'}), {locale_preferred: true}),
...filter(locales, {name_type: 'FULLY_SPECIFIED', locale_preferred: false}),
...reject(reject(locales, {name_type: 'FULLY_SPECIFIED'}), {locale_preferred: true}),
]
}

getConceptHeader(concept) {
return (
<React.Fragment>
Expand All @@ -98,73 +139,176 @@ class ConceptsComparison extends React.Component {
)
}

getValue(concept, attr, type) {
getValue(concept, attr, type, formatted=false) {
if(type === 'list') {
if(includes(['names', 'descriptions'], attr)) {
const locales = get(concept, attr)
return isEmpty(locales) ? '-' : map(locales, getLocaleLabel)
if(isEmpty(locales)) return '';
if(formatted)
return map(locales, getLocaleLabelFormatted)
return map(locales, getLocaleLabel).join('\n')
}
} else if(type === 'date') {
const date = get(concept, attr);
return date ? formatDate(date) : '-';
let date = get(concept, attr);

if(attr === 'created_on')
date ||= get(concept, 'created_at')
if(attr === 'updated_on')
date ||= get(concept, 'updated_at')

return date ? formatDate(date) : '';
} else if (type === 'textFormatted') {
if(attr === 'owner')
return `${concept.owner_type}: ${concept.owner}`
} else if (type === 'bool') {
return get(concept, attr) ? 'True' : 'False'
} else {
return get(concept, attr, '-')
if(includes(['created_by', 'updated_by'], attr))
return get(concept, attr) || get(concept, `version_${attr}`) || ''
return get(concept, attr, '')
}
}

maxArrayElement(v1, v2) {
const v1Length = isObject(v1) ? size(v1) : 0
const v2Length = isObject(v2) ? size(v2) : 0
return v1Length > v2Length ? v1 : v2;
}

getListAttrValue(attr, val, formatted=false) {
if(includes(['names', 'descriptions'], attr))
return formatted ? getLocaleLabelFormatted(val) : getLocaleLabel(val)
if(includes(['extras'], attr))
return this.getExtraAttributeLabel(val)
}

getExtraAttributeLabel(val) {
if(!val)
return ''
return `${keys(val)[0]}: ${JSON.stringify(values(val)[0])}`
}

render() {
const { lhs, rhs } = this.state;
const { lhs, rhs, isLoadingLHS, isLoadingRHS } = this.state;
const isLoading = isLoadingLHS || isLoadingRHS;
return (
<div className='col-md-12' style={{paddingTop: '10px', paddingBottom: '10px'}}>
<TableContainer style={{borderRadius: '4px'}}>
<Table size='small'>
<TableHead>
<TableRow colSpan="12">
<TableCell colSpan="2" style={{width: '20%'}}></TableCell>
<TableCell colSpan="5" style={{width: '40%'}}>
<div style={{fontSize: '12px'}}>
{this.getConceptHeader(lhs)}
</div>
<div style={{fontSize: '18px'}}>
{get(lhs, 'display_name')}
</div>
</TableCell>
<TableCell colSpan="5" style={{width: '40%'}}>
<div style={{fontSize: '12px'}}>
{this.getConceptHeader(rhs)}
</div>
<div style={{fontSize: '18px'}}>
{get(rhs, 'display_name')}
</div>
</TableCell>
</TableRow>
</TableHead>
<TableBody style={{border: '1px solid lightgray'}}>
{
map(ATTRIBUTES, (attrs, type) => {
return map(attrs, attr => {
const isDiff = !isEqual(get(lhs, attr), get(rhs, attr));
const className = isDiff ? 'diff-red' : '';
return (
<TableRow key={attr} colSpan='12' className={className}>
<TableCell colSpan='2' style={{width: '20%', fontWeight: 'bold'}}>
{startCase(attr)}
</TableCell>
<TableCell colSpan='5' style={{width: '40%'}}>
{this.getValue(lhs, attr, type)}
</TableCell>
<TableCell colSpan='5' style={{width: '40%'}}>
{this.getValue(rhs, attr, type)}
</TableCell>
</TableRow>
)
})
})
}
</TableBody>
</Table>
</TableContainer>
</div>
<React.Fragment>
{
isLoading ?
<div style={{textAlign: 'center'}}>
<CircularProgress color='primary' />
</div> :
<div className='col-md-12' style={{paddingTop: '10px', paddingBottom: '10px'}}>
<TableContainer style={{borderRadius: '4px', border: '1px solid lightgray'}}>
<Table size='small'>
<TableHead>
<TableRow colSpan="12">
<TableCell colSpan="2" style={{width: '10%'}}></TableCell>
<TableCell colSpan="5" style={{width: '45%'}}>
<div style={{fontSize: '14px'}}>
{this.getConceptHeader(lhs)}
</div>
<div style={{fontSize: '18px'}}>
{get(lhs, 'display_name')}
</div>
</TableCell>
<TableCell colSpan="5" style={{width: '45%'}}>
<div style={{fontSize: '14px'}}>
{this.getConceptHeader(rhs)}
</div>
<div style={{fontSize: '18px'}}>
{get(rhs, 'display_name')}
</div>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{
map(ATTRIBUTES, (attrs, type) => {
return map(attrs, attr => {
const lhsValue = this.getValue(lhs, attr, type);
const rhsValue = this.getValue(rhs, attr, type);
const isDiff = !isEqual(lhsValue, rhsValue);
const maxLengthAttr = type === 'list' ? this.maxArrayElement(get(lhs, attr), get(rhs, attr)) : [];
const rowSpan = size(maxLengthAttr);
return (
<React.Fragment key={attr}>
{
type === 'list' ?
map(maxLengthAttr, (_attr, index) => {
const _lhsVal = get(lhs, `${attr}.${index}`, '')
const _rhsVal = get(rhs, `${attr}.${index}`, '')
const _lhsValCleaned = this.getListAttrValue(attr, _lhsVal)
const _rhsValCleaned = this.getListAttrValue(attr, _rhsVal)
const _isDiff = !isEqual(_lhsValCleaned, _rhsValCleaned);
return (
<TableRow key={_attr.uuid || index} colSpan='12'>
{
index === 0 &&
<TableCell colSpan='2' rowSpan={rowSpan} style={{width: '10%', fontWeight: 'bold'}}>
{startCase(attr)}
</TableCell>
}
{
_isDiff ?
<TableCell colSpan='10' style={{width: '90%'}} className='diff-row'>
<ReactDiffViewer
oldValue={this.getListAttrValue(attr, _lhsVal)}
newValue={this.getListAttrValue(attr, _rhsVal)}
showDiffOnly={false}
splitView
hideLineNumbers
/>
</TableCell> :
<React.Fragment>
<TableCell colSpan='5' style={{width: '45%'}}>
{this.getListAttrValue(attr, _lhsVal, true)}
</TableCell>
<TableCell colSpan='5' style={{width: '45%'}}>
{this.getListAttrValue(attr, _rhsVal, true)}
</TableCell>
</React.Fragment>
}
</TableRow>
)
}) :
<TableRow key={attr} colSpan='12'>
<TableCell colSpan='2' style={{width: '10%', fontWeight: 'bold'}}>
{startCase(attr)}
</TableCell>
{
isDiff ?
<TableCell colSpan='10' style={{width: '90%'}} className='diff-row'>
<ReactDiffViewer
oldValue={lhsValue}
newValue={rhsValue}
showDiffOnly={false}
splitView
hideLineNumbers
/>
</TableCell> :
<React.Fragment>
<TableCell colSpan='5' style={{width: '45%'}}>
{this.getValue(lhs, attr, type, true)}
</TableCell>
<TableCell colSpan='5' style={{width: '45%'}}>
{this.getValue(rhs, attr, type, true)}
</TableCell>
</React.Fragment>
}
</TableRow>
}
</React.Fragment>
)
})
})
}
</TableBody>
</Table>
</TableContainer>
</div>
}
</React.Fragment>
)
}
}
Expand Down

0 comments on commit aac0ad5

Please sign in to comment.