Skip to content

Commit ef1286f

Browse files
committed
OpenConceptLab/ocl_issues#2290 | Concept/Mapping history tab
1 parent 35789f1 commit ef1286f

6 files changed

Lines changed: 270 additions & 10 deletions

File tree

src/components/concepts/ConceptHome.jsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@ import { useTranslation } from 'react-i18next'
33
import { useLocation, useHistory } from 'react-router-dom'
44
import Fade from '@mui/material/Fade';
55
import Skeleton from '@mui/material/Skeleton';
6+
67
import APIService from '../../services/APIService';
78
import { toParentURI } from '../../common/utils'
9+
10+
import { OperationsContext } from '../app/LayoutContext';
11+
import RetireConfirmDialog from '../common/RetireConfirmDialog'
12+
813
import ConceptHeader from './ConceptHeader';
914
import ConceptTabs from './ConceptTabs';
1015
import ConceptForm from './ConceptForm'
16+
import ConceptIcon from './ConceptIcon'
1117
import ConceptDetails from './ConceptDetails'
12-
import RetireConfirmDialog from '../common/RetireConfirmDialog'
13-
import { OperationsContext } from '../app/LayoutContext';
18+
import History from './History'
1419

1520
const ConceptHome = props => {
1621
const { t } = useTranslation()
@@ -19,6 +24,7 @@ const ConceptHome = props => {
1924
const isInitialMount = React.useRef(true);
2025

2126
const [concept, setConcept] = React.useState(props.concept || {})
27+
const [versions, setVersions] = React.useState([])
2228

2329
const [repo, setRepo] = React.useState(props.repo || {})
2430
const [tab, setTab] = React.useState('metadata')
@@ -37,11 +43,14 @@ const ConceptHome = props => {
3743
React.useEffect(() => {
3844
setLoading(true)
3945
setConcept(props.concept || {})
46+
setVersions([])
4047
getService().get().then(response => {
4148
const resource = response.data
4249
setConcept(resource)
4350
props.repo?.id ? setRepo(repo) : fetchRepo(resource)
4451
getMappings(resource)
52+
if(tab === 'history')
53+
fetchVersions(resource?.url)
4554
})
4655
}, [props.concept?.id, props.url])
4756

@@ -75,6 +84,26 @@ const ConceptHome = props => {
7584
return APIService.new().overrideURL(encodeURI(url))
7685
}
7786

87+
const fetchVersions = conceptURL => {
88+
if(versions?.length === 0 || conceptURL) {
89+
let _conceptURL = conceptURL || (props.concept?.id ? props.concept : concept)?.url
90+
if(!_conceptURL)
91+
return
92+
setLoading(true)
93+
const service = APIService.new().overrideURL(_conceptURL)
94+
service.appendToUrl('versions/').get(null, null, {includeCollectionVersions: true, includeSourceVersions: true}).then(response => {
95+
setVersions(response.data || [])
96+
setLoading(false)
97+
})
98+
}
99+
}
100+
101+
const onTabChange = newTab => {
102+
setTab(newTab)
103+
if(newTab === 'history')
104+
fetchVersions()
105+
}
106+
78107
const getMappings = (concept, directOnly) => {
79108
getService()
80109
.appendToUrl('$cascade/')
@@ -202,7 +231,7 @@ const ConceptHome = props => {
202231
<div className='col-xs-12 padding-0'>
203232
<ConceptHeader concept={concept} onClose={props.onClose} repoURL={getRepoURL()} onEdit={() => setEdit(true)} repo={repo} nested={props.nested} loading={loading} onRetire={() => setRetireDialog(true)} />
204233
</div>
205-
<ConceptTabs tab={tab} onTabChange={(event, newTab) => setTab(newTab)} loading={loading} />
234+
<ConceptTabs tab={tab} onTabChange={(event, newTab) => onTabChange(newTab)} loading={loading} />
206235
{
207236
tab === 'metadata' &&
208237
<ConceptDetails
@@ -218,6 +247,15 @@ const ConceptHome = props => {
218247
onLoadOwnerMappings={() => getOwnerMappings(concept)}
219248
/>
220249
}
250+
{
251+
tab === 'history' &&
252+
<History
253+
versions={versions}
254+
loading={loading}
255+
resource='concepts'
256+
icon={<ConceptIcon selected fontSize='small' />}
257+
/>
258+
}
221259
<RetireConfirmDialog
222260
open={retireDialog}
223261
onClose={() => setRetireDialog(false)}

src/components/concepts/ConceptTabs.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const ConceptTabs = ({ tab, onTabChange }) => {
1616
sx={{width: '100%', borderBottom: '1px solid', borderColor: 'surface.n90'}}
1717
>
1818
<Tab value="metadata" label={t('common.details')} sx={TAB_STYLES} />
19-
<Tab disabled value="history" label={t('common.history')} sx={TAB_STYLES} />
19+
<Tab value="history" label={t('common.history')} sx={TAB_STYLES} />
2020
</Tabs>
2121
)
2222
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import React from 'react'
2+
import moment from 'moment'
3+
import Skeleton from '@mui/material/Skeleton';
4+
import Timeline from '@mui/lab/Timeline';
5+
import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
6+
import TimelineSeparator from '@mui/lab/TimelineSeparator';
7+
import TimelineConnector from '@mui/lab/TimelineConnector';
8+
import TimelineContent from '@mui/lab/TimelineContent';
9+
import TimelineDot from '@mui/lab/TimelineDot';
10+
import Card from '@mui/material/Card';
11+
import CardContent from '@mui/material/CardContent';
12+
import Typography from '@mui/material/Typography';
13+
import IconButton from '@mui/material/IconButton';
14+
import Button from '@mui/material/Button';
15+
import Tooltip from '@mui/material/Tooltip';
16+
import CopyIcon from '@mui/icons-material/ContentCopy';
17+
18+
import compact from 'lodash/compact'
19+
import map from 'lodash/map'
20+
21+
const History = ({ versions, loading, icon, resource }) => {
22+
const getRepoDetails = uri => {
23+
const parts = compact(uri.split('/'))
24+
return {
25+
ownerType: parts[0],
26+
owner: parts[1],
27+
id: parts[3],
28+
version: parts[4],
29+
uri: uri
30+
}
31+
}
32+
return (
33+
<Timeline
34+
sx={{
35+
[`& .${timelineItemClasses.root}:before`]: {
36+
flex: 0,
37+
padding: 0,
38+
},
39+
}}
40+
>
41+
{
42+
loading ?
43+
<>
44+
<TimelineItem>
45+
<TimelineSeparator>
46+
<TimelineDot sx={{border: 0, padding: 0, background: 'none', boxShadow: 'none'}}>
47+
<Skeleton variant="circular" width={20} height={20} />
48+
</TimelineDot>
49+
<TimelineConnector />
50+
</TimelineSeparator>
51+
<TimelineContent>
52+
<Skeleton variant='text' sx={{marginTop: '-20px', height: '100px'}} />
53+
</TimelineContent>
54+
</TimelineItem>
55+
<TimelineItem>
56+
<TimelineSeparator>
57+
<TimelineDot sx={{border: 0, padding: 0, background: 'none', boxShadow: 'none'}}>
58+
<Skeleton variant="circular" width={20} height={20} />
59+
</TimelineDot>
60+
<TimelineConnector />
61+
</TimelineSeparator>
62+
<TimelineContent>
63+
<Skeleton variant='text' sx={{marginTop: '-20px', height: '100px'}} />
64+
</TimelineContent>
65+
</TimelineItem>
66+
<TimelineItem>
67+
<TimelineSeparator>
68+
<TimelineDot sx={{border: 0, padding: 0, background: 'none', boxShadow: 'none'}}>
69+
<Skeleton variant="circular" width={20} height={20} />
70+
</TimelineDot>
71+
</TimelineSeparator>
72+
<TimelineContent>
73+
<Skeleton variant='text' sx={{marginTop: '-20px', height: '100px'}} />
74+
</TimelineContent>
75+
</TimelineItem>
76+
</> :
77+
<>
78+
{
79+
map(versions, (version, index) => {
80+
const primaryRepo = getRepoDetails(version.url)
81+
const url = version.source_versions?.length > 0 ? version.source_versions[0] + resource + '/' + version.id + '/' : `${version.owner_url}sources/${primaryRepo.id}/HEAD/concepts/${version.id}/`
82+
return (
83+
<TimelineItem key={version.version_url}>
84+
<TimelineSeparator>
85+
<TimelineDot variant='outlined' color='primary' sx={{cursor: 'pointer', padding: 0, border: 0}}>
86+
<Button variant='text' color='primary' href={`#${url}`} sx={{margin: 0, padding: 0, fontSize: '12px', textTransform: 'none', minWidth: 'auto'}}>
87+
{icon}
88+
</Button>
89+
</TimelineDot>
90+
{
91+
index !== (versions.length - 1) &&
92+
<TimelineConnector />
93+
}
94+
</TimelineSeparator>
95+
<TimelineContent>
96+
<Card variant="outlined">
97+
<CardContent sx={{padding: '4px 12px !important'}}>
98+
<div className='col-xs-12 padding-0' style={{display: 'flex'}}>
99+
<div className='col-xs-8 padding-left-0'>
100+
<Button variant='text' color='primary' href={`#${version.version_url}`} sx={{margin: 0, padding: 0, fontSize: '12px', textTransform: 'none', minWidth: 'auto'}}>
101+
<Typography sx={{fontSize: '16px'}} component="div">
102+
{
103+
version.update_comment ?
104+
version.update_comment :
105+
<i>-</i>
106+
}
107+
</Typography>
108+
</Button>
109+
<Typography sx={{ fontSize: '12px' }} color="text.secondary">
110+
<Button variant='text' href={`#/users/${version.version_updated_by}`} sx={{margin: 0, padding: 0, fontSize: '12px', textTransform: 'none', minWidth: 'auto'}}>{version.version_updated_by}</Button> updated {moment(version.version_updated_on).fromNow()}
111+
</Typography>
112+
{
113+
version.source_versions?.length > 0 ?
114+
<div className='col-xs-12 padding-0'>
115+
<Typography sx={{ fontSize: '12px' }} color="text.secondary" component='span'>
116+
Source Versions:
117+
</Typography>
118+
{
119+
map(version.source_versions, repoVersion => {
120+
const repo = getRepoDetails(repoVersion)
121+
return (
122+
<Button key={repoVersion} variant='text' href={`#${repoVersion}`} sx={{margin: '0 4px', padding: 0, fontSize: '12px', textTransform: 'none', minWidth: 'auto'}}>{repo.id}/{repo.version}</Button>
123+
)
124+
})
125+
}
126+
</div> : null
127+
}
128+
{
129+
version.collection_versions?.length > 0 ?
130+
<div className='col-xs-12 padding-0'>
131+
<Typography sx={{ fontSize: '12px' }} color="text.secondary" component='span'>
132+
Collection Versions:
133+
</Typography>
134+
{
135+
map(version.collection_versions, repoVersion => {
136+
const repo = getRepoDetails(repoVersion)
137+
return (
138+
<Button key={repoVersion} variant='text' href={`#${repoVersion}`} sx={{margin: '0 4px', padding: 0, fontSize: '12px', textTransform: 'none', minWidth: 'auto'}}>{repo.id}/{repo.version}</Button>
139+
)
140+
})
141+
}
142+
</div> : null
143+
}
144+
</div>
145+
<div className='col-xs-4 padding-0' style={{textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column'}}>
146+
<div className='col-xs-12 padding-0' style={{display: 'flex', alignItems: 'center', justifyContent: 'flex-end'}}>
147+
<Tooltip title='Standard Checksum' placement='left'>
148+
<Typography sx={{ fontSize: '12px' }} color="text.secondary">
149+
{version.checksums?.standard?.slice(0, 8)}
150+
</Typography>
151+
</Tooltip>
152+
<IconButton size='small'><CopyIcon sx={{fontSize: '16px'}} /></IconButton>
153+
</div>
154+
<div className='col-xs-12 padding-0' style={{display: 'flex', alignItems: 'center', justifyContent: 'flex-end'}}>
155+
<Tooltip title='Smart Checksum' placement='left'>
156+
<Typography sx={{ fontSize: '12px' }} color="text.secondary">
157+
{version.checksums?.smart?.slice(0, 8)}
158+
</Typography>
159+
</Tooltip>
160+
<IconButton size='small'><CopyIcon sx={{fontSize: '16px'}} /></IconButton>
161+
</div>
162+
</div>
163+
</div>
164+
</CardContent>
165+
</Card>
166+
</TimelineContent>
167+
</TimelineItem>
168+
)
169+
})
170+
}
171+
</>
172+
}
173+
</Timeline>
174+
)
175+
}
176+
177+
export default History

src/components/mappings/MappingHome.jsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import React from 'react';
22
import { useTranslation } from 'react-i18next'
33
import { useLocation, useHistory } from 'react-router-dom'
4+
45
import Fade from '@mui/material/Fade';
6+
57
import APIService from '../../services/APIService';
68
import { toParentURI } from '../../common/utils'
9+
10+
import { OperationsContext } from '../app/LayoutContext';
11+
import RetireConfirmDialog from '../common/RetireConfirmDialog'
712
import MappingHeader from './MappingHeader';
813
import MappingTabs from './MappingTabs';
914
import MappingDetails from './MappingDetails'
1015
import MappingForm from './MappingForm'
11-
import RetireConfirmDialog from '../common/RetireConfirmDialog'
12-
import { OperationsContext } from '../app/LayoutContext';
16+
import MappingIcon from './MappingIcon'
17+
import History from '../concepts/History'
1318

1419
const MappingHome = props => {
1520
const { t } = useTranslation()
@@ -18,19 +23,27 @@ const MappingHome = props => {
1823
const isInitialMount = React.useRef(true);
1924

2025
const [mapping, setMapping] = React.useState(props.mapping || {})
26+
const [versions, setVersions] = React.useState([])
2127
const [repo, setRepo] = React.useState(props.repo || {})
2228
const [tab, setTab] = React.useState('metadata')
2329
const [edit, setEdit] = React.useState(false)
30+
const [loading, setLoading] = React.useState(false)
2431

2532
const [retireDialog, setRetireDialog] = React.useState(false)
2633
const { setAlert } = React.useContext(OperationsContext);
2734

2835
React.useEffect(() => {
36+
setLoading(true)
2937
setMapping(props.mapping || {})
38+
setVersions([])
3039
getService().get().then(response => {
3140
const resource = response.data
3241
setMapping(resource)
3342
props.repo?.id ? setRepo(props.repo) : fetchRepo(resource)
43+
if(tab === 'history')
44+
fetchVersions(resource?.url)
45+
else
46+
setLoading(false)
3447
})
3548
}, [props.mapping?.id, props.url])
3649

@@ -64,6 +77,26 @@ const MappingHome = props => {
6477
return APIService.new().overrideURL(encodeURI(url))
6578
}
6679

80+
const fetchVersions = url => {
81+
if(versions?.length === 0 || url) {
82+
let _url = url || (props.mapping?.id ? props.mapping : mapping)?.url
83+
if(!_url)
84+
return
85+
setLoading(true)
86+
const service = APIService.new().overrideURL(_url)
87+
service.appendToUrl('versions/').get(null, null, {includeCollectionVersions: true, includeSourceVersions: true}).then(response => {
88+
setVersions(response.data || [])
89+
setLoading(false)
90+
})
91+
}
92+
}
93+
94+
const onTabChange = newTab => {
95+
setTab(newTab)
96+
if(newTab === 'history')
97+
fetchVersions()
98+
}
99+
67100
const toggleRetire = reason => {
68101
setRetireDialog(false)
69102
const isRetired = mapping.retired
@@ -109,13 +142,22 @@ const MappingHome = props => {
109142
<div className='col-xs-12 padding-0' style={{marginBottom: '12px'}}>
110143
<MappingHeader mapping={mapping} onClose={props.onClose} repoURL={getRepoURL()} repo={repo} nested={props.nested} onEdit={() => setEdit(true)} onRetire={() => setRetireDialog(true)} />
111144
</div>
112-
<MappingTabs tab={tab} onTabChange={(event, newTab) => setTab(newTab)} />
145+
<MappingTabs tab={tab} onTabChange={(event, newTab) => onTabChange(newTab)} />
113146
{
114147
tab === 'metadata' &&
115148
<div className='col-xs-12' style={{padding: '16px 0', height: 'calc(100vh - 330px)', overflow: 'auto'}}>
116149
<MappingDetails mapping={mapping} />
117150
</div>
118151
}
152+
{
153+
tab === 'history' &&
154+
<History
155+
versions={versions}
156+
loading={loading}
157+
resource='mappings'
158+
icon={<MappingIcon color='primary' fontSize='small' />}
159+
/>
160+
}
119161
<RetireConfirmDialog
120162
open={retireDialog}
121163
onClose={() => setRetireDialog(false)}

src/components/mappings/MappingTabs.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const MappingTabs = ({ tab, onTabChange }) => {
1616
sx={{width: '100%', borderBottom: '1px solid', borderColor: 'surface.n90'}}
1717
>
1818
<Tab value="metadata" label={t('common.details')} sx={TAB_STYLES} />
19-
<Tab disabled value="history" label={t('common.history')} sx={TAB_STYLES} />
19+
<Tab value="history" label={t('common.history')} sx={TAB_STYLES} />
2020
</Tabs>
2121
)
2222
}

0 commit comments

Comments
 (0)