-
Notifications
You must be signed in to change notification settings - Fork 44
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Feature/versioning #225
Feature/versioning #225
Changes from 21 commits
e693798
2587ba0
10ecef2
fce5251
780ceb1
beea4aa
353a2ff
95c2fc0
e6b1735
2ed616e
a3f8874
27ad46a
5996542
7b58b68
d2f2414
6e67224
9f7c44f
6ea55ce
6c3fcf3
27c6933
ee4bb41
3cf2084
f6e4c8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export function transformGet(entity) { | ||
|
||
} | ||
|
||
export function transformSet(entity, operation) { | ||
if (operation === 'update') { | ||
delete entity.id; | ||
delete entity.identifier; | ||
delete entity.provider; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
|
||
import _ from 'lodash/fp'; | ||
|
||
/** | ||
* Original source code was taken from {@link https://github.com/prototypejs/prototype/blob/5fddd3e/src/prototype/lang/string.js#L702} | ||
*/ | ||
const isJSON = (str) => { | ||
if (/^\s*$/.test(str)) return false; | ||
|
||
str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'); | ||
str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'); | ||
str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, ''); | ||
|
||
return (/^[\],:{}\s]*$/).test(str); | ||
}; | ||
|
||
const parse = (value) => { | ||
if (_.isString(value) && isJSON(value)) { | ||
return JSON.parse(value); | ||
} | ||
|
||
return value; | ||
} | ||
|
||
export function parseJSON(value) { | ||
return _.cond([ | ||
[_.isArray, _.map(_.mapValues(parseJSON))], | ||
[_.isObject, _.mapValues(parseJSON)], | ||
[_.stubTrue, parse] | ||
])(value); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import React, {Children, cloneElement, useState, useCallback} from 'react'; | ||
import {makeStyles} from '@material-ui/core'; | ||
import ReactDiffViewer from 'react-diff-viewer'; | ||
import { | ||
List, | ||
Datagrid, | ||
Button, | ||
TextField, | ||
SelectInput, | ||
TextInput, | ||
Filter, | ||
FunctionField, | ||
useRefresh, | ||
useNotify | ||
} from 'react-admin'; | ||
import {parseJSON} from '../utils/json'; | ||
import {SettingsBackupRestore} from "@material-ui/icons"; | ||
|
||
const ListActionsToolbar = ({children, ...props}) => { | ||
const classes = makeStyles({ | ||
toolbar: { | ||
alignItems: 'center', | ||
display: 'flex', | ||
marginTop: -1, | ||
marginBottom: -1, | ||
}, | ||
}); | ||
|
||
return ( | ||
<div className={classes.toolbar}> | ||
{Children.map(children, button => cloneElement(button, props))} | ||
</div> | ||
); | ||
}; | ||
|
||
const MyFilter = (props) => ( | ||
<Filter {...props}> | ||
<SelectInput label="Entity Type" source="entity_type" choices={[{id: 'apps', name: 'Apps'}, {id: 'routes', name: 'Routes'}]} alwaysOn/> | ||
<TextInput label="Entity ID" source="entity_id" alwaysOn /> | ||
<TextInput label="Created by" source="created_by" alwaysOn /> | ||
</Filter> | ||
); | ||
|
||
const beautifyJson = v => JSON.stringify(parseJSON(JSON.parse(v)), null, 2); | ||
|
||
|
||
const MyPanel = ({ id, record, resource }) => ( | ||
<ReactDiffViewer | ||
oldValue={beautifyJson(record.data || record.data_after)} | ||
newValue={beautifyJson(record.data_after || record.data)} | ||
splitView={false} | ||
showDiffOnly={false} | ||
compareMethod={'diffWords'} | ||
styles={{ | ||
diffContainer: { | ||
pre: { | ||
lineHeight: '1', | ||
}, | ||
}, | ||
gutter: { | ||
minWidth: 0, | ||
} | ||
}} | ||
/> | ||
); | ||
|
||
const RevertButton = ({ | ||
record, | ||
...rest | ||
}) => { | ||
const [disabled, setDisabled] = useState(false); | ||
const refresh = useRefresh(); | ||
const notify = useNotify(); | ||
|
||
const handleClick = useCallback(() => { | ||
if (confirm(`Are you sure that you want to revert change with ID "${record.id}"?`)) { | ||
setDisabled(true); | ||
fetch(`/api/v1/versioning/${record.id}/revert`, {method: 'POST'}).then(async res => { | ||
setDisabled(false); | ||
if (!res.ok) { | ||
if (res.status < 500) { | ||
const resInfo = await res.json(); | ||
return notify(resInfo.reason, 'error', { smart_count: 1 }); | ||
} | ||
throw new Error(`Unexpected network error. Returned code "${res.status}"`); | ||
} | ||
notify('Change was successfully reverted', 'info', { smart_count: 1 }); | ||
refresh(); | ||
}).catch(err => { | ||
setDisabled(false); | ||
notify('Oops! Something went wrong.', 'error', { smart_count: 1 }); | ||
console.error(err); | ||
}); | ||
} | ||
}, []); | ||
|
||
return ( | ||
<Button | ||
label="Revert" | ||
disabled={disabled} | ||
onClick={handleClick} | ||
{...rest} | ||
> | ||
<SettingsBackupRestore/> | ||
</Button> | ||
); | ||
}; | ||
|
||
const PostList = props => { | ||
return ( | ||
<List | ||
{...props} | ||
title="History" | ||
filters={<MyFilter/>} | ||
exporter={false} | ||
perPage={25} | ||
bulkActionButtons={false} | ||
> | ||
<Datagrid expand={<MyPanel />} > | ||
<TextField sortable={false} source="id" /> | ||
<TextField sortable={false} source="entity_type" /> | ||
<TextField sortable={false} source="entity_id" /> | ||
<FunctionField label="Operation" render={record => record.data && record.data_after ? 'UPDATE' : record.data ? 'DELETE' : 'CREATE'} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So if you create an object that I mentioned here you could use it here with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that using here the same variable is a little bit incorrect since it's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I agree with that |
||
<TextField sortable={false} source="created_by" /> | ||
<FunctionField label="Created At" render={record => new Date(record.created_at * 1000).toLocaleString()} /> | ||
<ListActionsToolbar> | ||
<RevertButton /> | ||
</ListActionsToolbar> | ||
</Datagrid> | ||
</List> | ||
); | ||
}; | ||
|
||
export default PostList; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export function transformGet(setting) { | ||
|
||
} | ||
|
||
export function transformSet(setting) { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import Icon from '@material-ui/icons/History'; | ||
import React, {Children, cloneElement} from 'react'; | ||
|
||
import List from './List'; | ||
|
||
export default { | ||
list: List, | ||
icon: Icon, | ||
options: {label: 'History'}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion, it would be nice to have one object with operations and use it everywhere
because as I see operations are copy/pasted in each of the files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed