Skip to content

Commit

Permalink
[WIP] Endpoint badge
Browse files Browse the repository at this point in the history
This reimplements the idea @bkdotcom came up with in #1519, and took a stab at in #1525. It’s a really powerful way to add all sorts of custom badges, particularly considering [tools like RunKit endpoints and Jupyter Kernel Gateway](#2259 (comment)), not to mention all the other ways cloud functions can be deployed these days.

Ref #1752 #2259
  • Loading branch information
paulmelnikow committed Dec 7, 2018
1 parent d0c9da0 commit 1aacf7e
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 0 deletions.
158 changes: 158 additions & 0 deletions frontend/components/endpoint-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React from 'react'
import { Link } from 'react-router-dom'
import Meta from './meta'
import Header from './header'
import Footer from './footer'
import { baseUrl } from '../constants'

const example = JSON.stringify(
{
schemaVersion: 1,
label: 'hello',
message: 'sweet world',
color: 'orange',
},
undefined,
2
)

const EndpointPage = ({ baseUrl }) => (
<div>
<Meta />
<Header />

<h3 id="static-badge">Endpoint</h3>

<p>
<code>
{baseUrl}
/badge/endpoint.svg?url=&lt;URL&gt;&amp;style=&lt;STYLE&gt;
</code>
</p>

<p style={{ textAlign: 'left' }}>
Using the endpoint badge, you can create badges from your own JSON
endpoint.
</p>

<p>The endpoint must return an object like this:</p>

<code
style={{
display: 'block',
width: '250px',
margin: '0 auto',
padding: '10px 30px',
textAlign: 'left',
}}
>
{example}
</code>

<h4>Schema</h4>

<style jsx>{`
.schema {
display: inline-block;
overflow: hidden;
text-align: left;
background: #efefef;
padding: 10px;
max-width: 800px;
}
.schema dt,
.schema dd {
padding: 0 1%;
margin-top: 8px;
margin-bottom: 8px;
float: left;
}
.schema dt {
width: 100px;
clear: both;
}
.schema dd {
margin-left: 20px;
width: 75%;
}
@media (max-width: 600px) {
.data_table {
text-align: center;
}
}
`}</style>

<dl className="schema">
<dt>schemaVersion</dt>
<dd>
Required. Always the number <code>1</code>.
</dd>
<dt>label</dt>
<dd>
Required. The left text, or the empty string to omit the left side of
the badge. This can be overridden by the query string.
</dd>
<dt>message</dt>
<dd>Required. Can't be empty. The right text.</dd>
<dt>color</dt>
<dd>
Default: <code>lightgrey</code>. The right color. Supports the eight
named colors above, as well as hex, rgb, rgba, hsl, hsla and css named
colors.
</dd>
<dt>labelColor</dt>
<dd>
Default: <code>grey</code>. The left color.
</dd>
<dt>isError</dt>
<dd>
Default: <code>false</code>. <code>true</code> to treat this as an error
badge. In the future this will inhibit the query string from overriding
the color and may affect cache behavior.
</dd>
<dt>link</dt>
<dd>
Default: none. Specify what clicking on the left/right of a badge should
do.
</dd>
<dt>namedLogo</dt>
<dd>
Default: none. One of the named logos supported by Shields or {}
<a href="https://simpleicons.org/">simple-icons</a>. Can be overridden
by the query string.
</dd>
<dt>logoSvg</dt>
<dd>Default: none. An SVG string containing a custom logo.</dd>
<dt>logoColor</dt>
<dd>
Default: none. Same meaning as the query string. Can be overridden by
the query string.
</dd>
<dt>logoWidth</dt>
<dd>
Default: none. Same meaning as the query string. Can be overridden by
the query string.
</dd>
<dt>logoPosition</dt>
<dd>
Default: none. Same meaning as the query string. Can be overridden by
the query string.
</dd>
<dt>style</dt>
<dd>
Default: <code>flat</code>. The default template to use. Can be
overridden by the query string.
</dd>
<dt>cacheSeconds</dt>
<dd>
Default: <code>300</code>. Set the HTTP cache lifetime in seconds, which
should respected by the Shields' CDN and downstream users. This lets you
tune performance and traffic vs. responsiveness. Can be overridden by
the query string, but only to a larger value.
</dd>
</dl>
<br style={{ clear: 'both' }} />
<Footer />
</div>
)
export default EndpointPage
15 changes: 15 additions & 0 deletions frontend/components/usage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { Fragment } from 'react'
import { Link } from 'react-router-dom'
import PropTypes from 'prop-types'
import StaticBadgeMaker from './static-badge-maker'
import DynamicBadgeMaker from './dynamic-badge-maker'
Expand Down Expand Up @@ -128,6 +129,20 @@ export default class Usage extends React.PureComponent {

{this.renderColorExamples()}

<h3 id="static-badge">Endpoint</h3>

<p>
Create badges from{' '}
<Link to={'/endpoint'}>your own JSON endpoint</Link>
</p>

<p>
<code>
{baseUrl}
/badge/endpoint.svg?url=&lt;URL&gt;&amp;style=&lt;STYLE&gt;
</code>
</p>

<h3 id="dynamic-badge">Dynamic</h3>

<DynamicBadgeMaker baseUrl={baseUrl} />
Expand Down
2 changes: 2 additions & 0 deletions pages/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from 'react'
import { HashRouter, StaticRouter, Route } from 'react-router-dom'
import ExamplesPage from '../frontend/components/examples-page'
import EndpointPage from '../frontend/components/endpoint-page'

export default class Router extends React.Component {
render() {
const router = (
<div>
<Route path="/" exact component={ExamplesPage} />
<Route path="/examples/:id" component={ExamplesPage} />
<Route path="/endpoint" component={EndpointPage} />
</div>
)

Expand Down
116 changes: 116 additions & 0 deletions services/endpoint/endpoint.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use strict'

const { URL } = require('url')
const Joi = require('joi')
const { errorMessages } = require('../dynamic/dynamic-helpers')
const BaseJsonService = require('../base-json')
const { InvalidParameter } = require('../errors')
const { optionalUrl } = require('../validators')

const blockedDomains = ['github.com', 'shields.io']

const queryParamSchema = Joi.object({
url: optionalUrl.required(),
}).required()

const endpointSchema = Joi.object({
schemaVersion: 1,
label: Joi.string()
.allow('')
.required(),
message: Joi.string().required(),
color: Joi.string(),
labelColor: Joi.string(),
isError: Joi.boolean().default(false),
link: Joi.string(),
namedLogo: Joi.string(),
logoSvg: Joi.string(),
logoColor: Joi.forbidden(),
logoWidth: Joi.forbidden(),
logoPosition: Joi.forbidden(),
style: Joi.string(),
cacheSeconds: Joi.number(),
})
.oxor('namedLogo', 'logoSvg')
.when(
Joi.alternatives().try(
Joi.object({ namedLogo: Joi.string().required() }).unknown(),
Joi.object({ logoSvg: Joi.string().required() }).unknown()
),
{
then: Joi.object({
logoColor: Joi.string(),
logoWidth: Joi.number(),
logoPosition: Joi.number(),
}),
}
)
.required()

module.exports = class Endpoint extends BaseJsonService {
static get category() {
return 'dynamic'
}

static get route() {
return {
base: 'badge/endpoint',
pattern: '',
queryParams: ['url'],
}
}

static get _cacheLength() {
return 300
}

static get defaultBadgeData() {
return {
label: 'custom badge',
}
}

static render({
label,
message,
color,
labelColor,
namedLogo,
logoSvg,
logoColor,
logoWidth,
logoPosition,
style,
isError,
cacheSeconds,
}) {
return {
label,
message,
color,
}
}

async handle(namedParams, queryParams) {
const { url } = this.constructor._validateQueryParams(
queryParams,
queryParamSchema
)

const { protocol, hostname } = new URL(url)
if (protocol !== 'https:') {
throw new InvalidParameter({ prettyMessage: 'please use https' })
}
if (blockedDomains.some(domain => hostname.endsWith(domain))) {
throw new InvalidParameter({ prettyMessage: 'domain is blocked' })
}

const data = await this._requestJson({
schema: endpointSchema,
url,
errorMessages,
})

return this.constructor.render(data)
}
}
36 changes: 36 additions & 0 deletions services/endpoint/endpoint.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict'

const t = (module.exports = require('../create-service-tester')())

t.create('Valid schema (mocked)')
.get('.json?url=https://example.com/badge')
.only()
.intercept(nock =>
nock('https://example.com/')
.get('/badge')
.reply(200, {
schemaVersion: 1,
label: '',
message: 'yo',
})
)
.expectJSON({ name: '', value: 'yo' })

t.create('Invalid schema (mocked)')
.get('.json?url=https://example.com/badge')
.intercept(nock =>
nock('https://example.com/')
.get('/badge')
.reply(200, {
schemaVersion: -1,
})
)
.expectJSON({ name: 'custom badge', value: 'invalid response data' })

t.create('Bad scheme')
.get('.json?url=http://example.com/badge')
.expectJSON({ name: 'custom badge', value: 'please use https' })

t.create('Blocked domain')
.get('.json?url=https://img.shields.io/badge/foo-bar-blue.json')
.expectJSON({ name: 'custom badge', value: 'domain is blocked' })

0 comments on commit 1aacf7e

Please sign in to comment.