Allow registration of content bundles in GitHubHint (#3094)

* Clear woner & error on success

* WIP buttons

* Selection bar

* Sanitize GitHub urls

* Complete hint registration

* button-row icons

* PR comments, url check & validation TODO

* PR comments, TODO for validation to show intent
This commit is contained in:
Jaco Greeff 2016-11-02 18:16:50 +01:00 committed by GitHub
parent e4c75bde4c
commit b3d502ba78
2 changed files with 192 additions and 45 deletions

View File

@ -82,6 +82,10 @@
.capture { .capture {
} }
.capture+.capture {
margin-top: 0.5em;
}
.capture * { .capture * {
display: inline-block; display: inline-block;
padding: 0.75em; padding: 0.75em;
@ -124,3 +128,20 @@
.hashOk { .hashOk {
opacity: 0.5; opacity: 0.5;
} }
.typeButtons {
text-align: center;
padding: 0 0 1em 0;
}
.typeButtons>div {
border-radius: 0 !important;
&:first-child {
border-radius: 5px 0 0 5px !important;
}
&:last-child {
border-radius: 0 5px 5px 0 !important;
}
}

View File

@ -33,12 +33,15 @@ export default class Application extends Component {
loading: true, loading: true,
url: '', url: '',
urlError: null, urlError: null,
commit: '',
commitError: null,
contentHash: '', contentHash: '',
contentHashError: null, contentHashError: null,
contentHashOwner: null, contentHashOwner: null,
registerBusy: false, registerBusy: false,
registerError: null, registerError: null,
registerState: '' registerState: '',
registerType: 'file'
} }
componentDidMount () { componentDidMount () {
@ -65,7 +68,7 @@ export default class Application extends Component {
} }
renderPage () { renderPage () {
const { fromAddress, registerBusy, url, urlError, contentHash, contentHashError, contentHashOwner } = this.state; const { fromAddress, registerBusy, url, urlError, contentHash, contentHashError, contentHashOwner, commit, commitError, registerType, repo, repoError } = this.state;
let hashClass = null; let hashClass = null;
if (contentHashError) { if (contentHashError) {
@ -74,14 +77,31 @@ export default class Application extends Component {
hashClass = styles.hashOk; hashClass = styles.hashOk;
} }
return ( let valueInputs = null;
<div className={ styles.container }> if (registerType === 'content') {
<div className={ styles.form }> valueInputs = [
<div className={ styles.box }> <div className={ styles.capture } key='repo'>
<div className={ styles.description }> <input
Provide a valid URL to register. The content information can be used in other contracts that allows for reverse lookups, e.g. image registries, dapp registries, etc. type='text'
placeholder='owner/repo'
disabled={ registerBusy }
value={ repo }
className={ repoError ? styles.error : null }
onChange={ this.onChangeRepo } />
</div>,
<div className={ styles.capture } key='hash'>
<input
type='text'
placeholder='commit hash sha3'
disabled={ registerBusy }
value={ commit }
className={ commitError ? styles.error : null }
onChange={ this.onChangeCommit } />
</div> </div>
<div className={ styles.capture }> ];
} else {
valueInputs = (
<div className={ styles.capture } key='url'>
<input <input
type='text' type='text'
placeholder='http://domain/filename' placeholder='http://domain/filename'
@ -90,6 +110,27 @@ export default class Application extends Component {
className={ urlError ? styles.error : null } className={ urlError ? styles.error : null }
onChange={ this.onChangeUrl } /> onChange={ this.onChangeUrl } />
</div> </div>
);
}
return (
<div className={ styles.container }>
<div className={ styles.form }>
<div className={ styles.typeButtons }>
<Button
disabled={ registerBusy }
invert={ registerType !== 'file' }
onClick={ this.onClickTypeNormal }>File Link</Button>
<Button
disabled={ registerBusy }
invert={ registerType !== 'content' }
onClick={ this.onClickTypeContent }>Content Bundle</Button>
</div>
<div className={ styles.box }>
<div className={ styles.description }>
Provide a valid URL to register. The content information can be used in other contracts that allows for reverse lookups, e.g. image registries, dapp registries, etc.
</div>
{ valueInputs }
<div className={ hashClass }> <div className={ hashClass }>
{ contentHashError || contentHash } { contentHashError || contentHash }
</div> </div>
@ -101,7 +142,7 @@ export default class Application extends Component {
} }
renderButtons () { renderButtons () {
const { accounts, fromAddress, url, urlError, contentHashError, contentHashOwner } = this.state; const { accounts, fromAddress, urlError, repoError, commitError, contentHashError, contentHashOwner } = this.state;
const account = accounts[fromAddress]; const account = accounts[fromAddress];
return ( return (
@ -114,7 +155,7 @@ export default class Application extends Component {
</div> </div>
<Button <Button
onClick={ this.onClickRegister } onClick={ this.onClickRegister }
disabled={ (!!contentHashError && contentHashOwner !== fromAddress) || !!urlError || url.length === 0 }>register url</Button> disabled={ (contentHashError && contentHashOwner !== fromAddress) || urlError || repoError || commitError }>register url</Button>
</div> </div>
); );
} }
@ -147,53 +188,86 @@ export default class Application extends Component {
); );
} }
onClickContentHash = () => { onClickTypeNormal = () => {
this.setState({ fileHash: false, commit: '' }); const { url } = this.state;
this.setState({ registerType: 'file', commitError: null, repoError: null }, () => {
this.onChangeUrl({ target: { value: url } });
});
} }
onClickFileHash = () => { onClickTypeContent = () => {
this.setState({ fileHash: true, commit: 0 }); const { repo, commit } = this.state;
this.setState({ registerType: 'content', urlError: null }, () => {
this.onChangeRepo({ target: { value: repo } });
this.onChangeCommit({ target: { value: commit } });
});
}
onChangeCommit = (event) => {
const commit = event.target.value;
const commitError = null;
// TODO: field validation
this.setState({ commit, commitError, contentHashError: 'hash lookup in progress' }, () => {
const { repo } = this.state;
this.lookupHash(`https://codeload.github.com/${repo}/zip/${commit}`);
});
}
onChangeRepo = (event) => {
let repo = event.target.value;
const repoError = null;
// TODO: field validation
if (!repoError) {
repo = repo.replace('https://github.com/', '');
}
this.setState({ repo, repoError, contentHashError: 'hash lookup in progress' }, () => {
const { commit } = this.state;
this.lookupHash(`https://codeload.github.com/${repo}/zip/${commit}`);
});
} }
onChangeUrl = (event) => { onChangeUrl = (event) => {
const url = event.target.value; let url = event.target.value;
let urlError = null; const urlError = null;
if (url && url.length) { // TODO: field validation
const re = /^https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}/g; // eslint-disable-line if (!urlError) {
urlError = re.test(url) const parts = url.split('/');
? null
: 'not matching rexex'; if (parts[2] === 'github.com' || parts[2] === 'raw.githubusercontent.com') {
url = `https://raw.githubusercontent.com/${parts.slice(3).join('/')}`.replace('/blob/', '/');
}
} }
this.setState({ url, urlError, contentHashError: 'hash lookup in progress' }, () => { this.setState({ url, urlError, contentHashError: 'hash lookup in progress' }, () => {
this.lookupHash(); this.lookupHash(url);
}); });
} }
onClickRegister = () => { onClickRegister = () => {
const { url, urlError, contentHash, contentHashError, contentHashOwner, fromAddress, instance } = this.state; const { commit, commitError, contentHashError, contentHashOwner, fromAddress, url, urlError, registerType, repo, repoError } = this.state;
if ((!!contentHashError && contentHashOwner !== fromAddress) || !!urlError || url.length === 0) { // TODO: No errors are currently set, validation to be expanded and added for each
// field (query is fast to pick up the issues, so not burning atm)
if ((contentHashError && contentHashOwner !== fromAddress) || repoError || urlError || commitError) {
return; return;
} }
this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' }); if (registerType === 'file') {
this.registerUrl(url);
} else {
this.registerContent(repo, commit);
}
}
const values = [contentHash, url]; trackRequest (promise) {
const options = { from: fromAddress }; return promise
instance
.hintURL.estimateGas(options, values)
.then((gas) => {
this.setState({ registerState: 'Gas estimated, Posting transaction to the network' });
const gasPassed = gas.mul(1.2);
options.gas = gasPassed.toFixed(0);
console.log(`gas estimated at ${gas.toFormat(0)}, passing ${gasPassed.toFormat(0)}`);
return instance.hintURL.postTransaction(options, values);
})
.then((signerRequestId) => { .then((signerRequestId) => {
this.setState({ signerRequestId, registerState: 'Transaction posted, Waiting for transaction authorization' }); this.setState({ signerRequestId, registerState: 'Transaction posted, Waiting for transaction authorization' });
@ -211,7 +285,7 @@ export default class Application extends Component {
}); });
}) })
.then((txReceipt) => { .then((txReceipt) => {
this.setState({ txReceipt, registerBusy: false, registerState: 'Network confirmed, Received transaction receipt', url: '', contentHash: '' }); this.setState({ txReceipt, registerBusy: false, registerState: 'Network confirmed, Received transaction receipt', url: '', commit: '', commitError: null, contentHash: '', contentHashOwner: null, contentHashError: null });
}) })
.catch((error) => { .catch((error) => {
console.error('onSend', error); console.error('onSend', error);
@ -219,6 +293,52 @@ export default class Application extends Component {
}); });
} }
registerContent (repo, commit) {
const { contentHash, fromAddress, instance } = this.state;
this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' });
const values = [contentHash, repo, commit];
const options = { from: fromAddress };
this.trackRequest(
instance
.hint.estimateGas(options, values)
.then((gas) => {
this.setState({ registerState: 'Gas estimated, Posting transaction to the network' });
const gasPassed = gas.mul(1.2);
options.gas = gasPassed.toFixed(0);
console.log(`gas estimated at ${gas.toFormat(0)}, passing ${gasPassed.toFormat(0)}`);
return instance.hint.postTransaction(options, values);
})
);
}
registerUrl (url) {
const { contentHash, fromAddress, instance } = this.state;
this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' });
const values = [contentHash, url];
const options = { from: fromAddress };
this.trackRequest(
instance
.hintURL.estimateGas(options, values)
.then((gas) => {
this.setState({ registerState: 'Gas estimated, Posting transaction to the network' });
const gasPassed = gas.mul(1.2);
options.gas = gasPassed.toFixed(0);
console.log(`gas estimated at ${gas.toFormat(0)}, passing ${gasPassed.toFormat(0)}`);
return instance.hintURL.postTransaction(options, values);
})
);
}
onSelectFromAddress = () => { onSelectFromAddress = () => {
const { accounts, fromAddress } = this.state; const { accounts, fromAddress } = this.state;
const addresses = Object.keys(accounts); const addresses = Object.keys(accounts);
@ -238,8 +358,14 @@ export default class Application extends Component {
this.setState({ fromAddress: addresses[index] }); this.setState({ fromAddress: addresses[index] });
} }
lookupHash () { lookupHash (url) {
const { url, instance } = this.state; const { instance } = this.state;
if (!url || !url.length) {
return;
}
console.log(`lookupHash ${url}`);
api.ethcore api.ethcore
.hashContent(url) .hashContent(url)