Show error page on missing configuration

This commit is contained in:
Manuel Bouza
2019-02-11 16:40:22 +01:00
parent 7b405a6de3
commit a4f3049671
10 changed files with 169 additions and 85 deletions

View File

@@ -35,7 +35,7 @@
} }
.input-group-addon { .input-group-addon {
padding: 0.5rem 0.75rem; padding: 0.25rem 0.5rem;
font-weight: normal; font-weight: normal;
color: #555555; color: #555555;
text-align: center; text-align: center;

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,9 +1,9 @@
import { parseServices, createMatcher } from 'utils/urlMatcher' import { createMatcher } from "utils/urlMatcher"
import remoteServices from "./remoteServices" import remoteServices from "./remoteServices"
const services = parseServices(remoteServices) const matcher = createMatcher(remoteServices)
const matcher = createMatcher(services)
const { version } = chrome.runtime.getManifest() const { version } = chrome.runtime.getManifest()
const registeredTabIds = new Set()
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// run only after the page is fully loaded // run only after the page is fully loaded
@@ -14,19 +14,42 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
const service = matcher(tab.url) const service = matcher(tab.url)
if (service) { if (service) {
registeredTabIds.add(tabId)
chrome.storage.sync.get( chrome.storage.sync.get(
["subdomain", "apiKey"], ["subdomain", "apiKey"],
({ subdomain, apiKey }) => { ({ subdomain, apiKey }) => {
const settings = { subdomain, apiKey, version } const payload = { subdomain, apiKey, version }
const payload = { serviceKey: service.key, settings } chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload })
chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload }, () => {
console.log("bubble mounted")
})
} }
) )
} else { } else {
chrome.tabs.sendMessage(tabId, { type: "unmountBubble" }, () => { registeredTabIds.delete(tabId)
console.log("bubble unmounted") chrome.tabs.sendMessage(tabId, { type: "unmountBubble" })
}) }
})
chrome.tabs.onRemoved.addListener(tabId => registeredTabIds.delete(tabId))
chrome.storage.onChanged.addListener(({ apiKey, subdomain }, areaName) => {
if (areaName === "sync" && (apiKey || subdomain)) {
chrome.storage.sync.get(
["subdomain", "apiKey"],
({ subdomain, apiKey }) => {
const payload = { subdomain, apiKey, version }
for (let tabId of registeredTabIds.values()) {
chrome.tabs.sendMessage(tabId, { type: "mountBubble", payload })
}
}
)
}
})
chrome.runtime.onMessage.addListener(({ type }) => {
switch (type) {
case "openOptions": {
chrome.tabs.create({
url: `chrome://extensions/?options=${chrome.runtime.id}`
})
}
} }
}) })

View File

@@ -2,9 +2,10 @@ import React, { Component } from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import ApiClient from "api/Client" import ApiClient from "api/Client"
import Modal, { Content } from "components/Modal" import Modal, { Content } from "components/Modal"
import MissingConfigurationError from "components/MissingConfigurationError"
import Form from "components/Form" import Form from "components/Form"
import { observable, computed } from "mobx" import { observable, computed, reaction } from "mobx"
import { observer } from "mobx-react" import { observer, disposeOnUnmount } from "mobx-react"
import logoUrl from "images/logo.png" import logoUrl from "images/logo.png"
import { import {
findLastProject, findLastProject,
@@ -12,6 +13,7 @@ import {
groupedProjectOptions, groupedProjectOptions,
currentDate currentDate
} from "utils" } from "utils"
import { head } from "lodash"
@observer @observer
class Bubble extends Component { class Bubble extends Component {
@@ -33,19 +35,20 @@ class Bubble extends Component {
#apiClient; #apiClient;
@observable isLoading = true; @observable isLoading = false;
@observable isOpen = false; @observable isOpen = false;
@observable projects; @observable projects;
@observable lastProjectId; @observable lastProjectId;
@observable lastTaskId; @observable lastTaskId;
@observable changeset = {}; @observable changeset = {};
@observable errors = {};
@computed get changesetWithDefaults() { @computed get changesetWithDefaults() {
const { service } = this.props const { service } = this.props
const project = const project =
findLastProject(service.projectId || this.lastProjectId)(this.projects) || findLastProject(service.projectId || this.lastProjectId)(this.projects) ||
this.projects[0] head(this.projects)
const defaults = { const defaults = {
id: service.id, id: service.id,
@@ -86,9 +89,17 @@ class Bubble extends Component {
} }
componentDidMount() { componentDidMount() {
const { settings } = this.props disposeOnUnmount(
this.#apiClient = new ApiClient(settings) this,
this.fetchData() reaction(
() =>
this.hasMissingConfiguration() ? null : this.props.settings,
this.fetchData,
{
fireImmediately: true
}
)
)
window.addEventListener("keydown", this.handleKeyDown) window.addEventListener("keydown", this.handleKeyDown)
} }
@@ -104,16 +115,28 @@ class Bubble extends Component {
this.isOpen = false this.isOpen = false
}; };
fetchData = () => { hasMissingConfiguration = () => {
const { settings } = this.props
return ["subdomain", "apiKey", "version"].some(key => !settings[key])
};
fetchData = settings => {
if (!settings) {
return
}
this.isLoading = true
this.#apiClient = new ApiClient(settings)
this.#apiClient this.#apiClient
.projects() .projects()
.then(({ data }) => { .then(({ data }) => {
this.projects = groupedProjectOptions(data.projects) this.projects = groupedProjectOptions(data.projects)
this.lastProjectId = data.last_project_id this.lastProjectId = data.last_project_id
this.lastTaskId = data.lastTaskId this.lastTaskId = data.lastTaskId
this.isLoading = false
}) })
.catch(console.error) .catch(console.error)
.finally(() => (this.isLoading = false))
}; };
// EVENT HANDLERS ----------------------------------------------------------- // EVENT HANDLERS -----------------------------------------------------------
@@ -151,17 +174,11 @@ class Bubble extends Component {
return null return null
} }
return ( let content
<> if (this.hasMissingConfiguration()) {
<img content = <MissingConfigurationError />
onClick={this.open} } else if (this.isOpen) {
src={chrome.extension.getURL(logoUrl)} content = (
width="50%"
/>
{this.isOpen && (
<Modal>
<Content>
<Form <Form
projects={this.projects} projects={this.projects}
changeset={this.changesetWithDefaults} changeset={this.changesetWithDefaults}
@@ -169,7 +186,21 @@ class Bubble extends Component {
onChange={this.handleChange} onChange={this.handleChange}
onSubmit={this.handleSubmit} onSubmit={this.handleSubmit}
/> />
</Content> )
} else {
content = null
}
return (
<>
<img
onClick={this.open}
src={chrome.extension.getURL(logoUrl)}
width="50%"
/>
{this.isOpen && (
<Modal>
<Content>{content}</Content>
</Modal> </Modal>
)} )}
</> </>

View File

@@ -0,0 +1,22 @@
import React from "react"
import configurationSettingsUrl from "images/configurationSettings.png"
const MissingConfigurationError = () => (
<div>
<h2>Fehlende Konfiguration</h2>
<p>
Bitte trage deine Internetadresse und deinen API-Schlüssel in den
Einstellungen der MOCO Browser-Erweiterung ein. Deinen API-Key findest du
in der MOCO App in deinem Profil im Register &quot;Integrationen&quot;.
</p>
<button onClick={() => chrome.runtime.sendMessage({ type: "openOptions" })}>
Einstellungen öffnen
</button>
<img
src={chrome.extension.getURL(configurationSettingsUrl)}
alt="Browser extension configuration settings"
/>
</div>
)
export default MissingConfigurationError

View File

@@ -2,10 +2,13 @@ import { createElement } from "react"
import ReactDOM from "react-dom" import ReactDOM from "react-dom"
import Bubble from "./components/Bubble" import Bubble from "./components/Bubble"
import services from "remoteServices" import services from "remoteServices"
import { createEnhancer } from "utils/urlMatcher" import { parseServices, createMatcher, createEnhancer } from "utils/urlMatcher"
import remoteServices from "./remoteServices"
import { pipe } from 'lodash/fp'
import "../css/main.scss" import "../css/main.scss"
const serviceEnhancer = createEnhancer(window.document)(services) const matcher = createMatcher(remoteServices)
const serviceEnhancer = createEnhancer(window.document)
chrome.runtime.onMessage.addListener(({ type, payload }) => { chrome.runtime.onMessage.addListener(({ type, payload }) => {
switch (type) { switch (type) {
@@ -19,7 +22,16 @@ chrome.runtime.onMessage.addListener(({ type, payload }) => {
} }
}) })
const mountBubble = ({ serviceKey, settings }) => { const mountBubble = (settings) => {
const service = pipe(
matcher,
serviceEnhancer(window.location.href)
)(window.location.href)
if (!service) {
return
}
if (!document.getElementById("moco-bx-container")) { if (!document.getElementById("moco-bx-container")) {
const domContainer = document.createElement("div") const domContainer = document.createElement("div")
domContainer.setAttribute("id", "moco-bx-container") domContainer.setAttribute("id", "moco-bx-container")
@@ -32,7 +44,6 @@ const mountBubble = ({ serviceKey, settings }) => {
document.body.appendChild(domBubble) document.body.appendChild(domBubble)
} }
const service = serviceEnhancer(serviceKey, window.location.href)
ReactDOM.render( ReactDOM.render(
createElement(Bubble, { service, settings }), createElement(Bubble, { service, settings }),
document.getElementById("moco-bx-bubble") document.getElementById("moco-bx-bubble")

View File

@@ -60,3 +60,5 @@ export const trace = curry((tag, value) => {
export const currentDate = (locale = "de") => export const currentDate = (locale = "de") =>
format(new Date(), "YYYY-MM-DD", { locale }) format(new Date(), "YYYY-MM-DD", { locale })
export const extensionSettingsUrl = () => `chrome://extensions/?id=${chrome.runtime.id}`

View File

@@ -13,7 +13,7 @@ const createEvaluator = args => fnOrValue => {
return fnOrValue return fnOrValue
} }
export const parseServices = compose( const parseServices = compose(
map(([key, config]) => ({ map(([key, config]) => ({
...config, ...config,
key, key,
@@ -22,9 +22,11 @@ export const parseServices = compose(
toPairs toPairs
) )
export const createEnhancer = document => services => (key, url) => { export const createEnhancer = document => url => service => {
const service = services[key] if (!service) {
service.key = key return
}
const route = new Route(service.urlPattern) const route = new Route(service.urlPattern)
const match = route.match(url) const match = route.match(url)
const args = [document, service, match] const args = [document, service, match]
@@ -32,14 +34,15 @@ export const createEnhancer = document => services => (key, url) => {
return { return {
...service, ...service,
key,
url, url,
id: evaluate(service.id) || match.id, id: evaluate(service.id) || match.id,
description: evaluate(service.description), description: evaluate(service.description),
projectId: evaluate(service.projectId), projectId: evaluate(service.projectId),
taskId: evaluate(service.taskId), taskId: evaluate(service.taskId)
} }
} }
export const createMatcher = services => url => export const createMatcher = remoteServices => {
services.find(service => service.route.match(url)) const services = parseServices(remoteServices)
return url => services.find(service => service.route.match(url))
}

View File

@@ -1,64 +1,56 @@
import { remoteServices } from '../data' import { remoteServices } from "../data"
import { parseServices, createMatcher, createEnhancer } from '../../src/js/utils/urlMatcher' import { createMatcher, createEnhancer } from "../../src/js/utils/urlMatcher"
import Route from "route-parser"
describe('utils', () => { describe("utils", () => {
describe("urlMatcher", () => { describe("urlMatcher", () => {
describe("parseServices", () => { let matcher
it("parses the services", () => {
const services = parseServices(remoteServices)
let service = services[0] beforeEach(() => {
expect(service.key).toEqual("github-pr") matcher = createMatcher(remoteServices)
expect(service.name).toEqual("github")
expect(service.route).toBeInstanceOf(Route)
service = services[1]
expect(service.key).toEqual("jira-cloud")
expect(service.name).toEqual("jira")
expect(service.route).toBeInstanceOf(Route)
})
}) })
describe("createMatcher", () => { describe("createMatcher", () => {
let services, matcher it("matches host and path", () => {
const service = matcher(
beforeEach(() => { "https://github.com/hundertzehn/mocoapp/pull/123"
services = parseServices(remoteServices) )
matcher = createMatcher(services) expect(service.key).toEqual("github-pr")
expect(service.name).toEqual("github")
}) })
it('matches host and path', () => { it("matches query string", () => {
const service = matcher('https://github.com/hundertzehn/mocoapp/pull/123') const service = matcher(
expect(service.key).toEqual('github-pr') "https://cloud.jira.com/browse?project=mocoapp&issue=1234"
expect(service.name).toEqual('github') )
expect(service.key).toEqual("jira-cloud")
expect(service.name).toEqual("jira")
}) })
it('matches query string', () => { it("does not match different host", () => {
const service = matcher('https://cloud.jira.com/browse?project=mocoapp&issue=1234') const service = matcher(
expect(service.key).toEqual('jira-cloud') "https://trello.com/hundertzehn/mocoapp/pull/123"
expect(service.name).toEqual('jira') )
})
it('does not match different host', () => {
const service = matcher('https://trello.com/hundertzehn/mocoapp/pull/123')
expect(service).toBeFalsy() expect(service).toBeFalsy()
}) })
}) })
describe("createEnhancer", () => { describe("createEnhancer", () => {
it("enhances a services", () => { it("enhances a services", () => {
const url = 'https://github.com/hundertzehn/mocoapp/pull/123' const url = "https://github.com/hundertzehn/mocoapp/pull/123"
const document = { const document = {
querySelector: jest.fn().mockReturnValue({ textContent: '[4321] Foo' }) querySelector: jest
.fn()
.mockReturnValue({ textContent: "[4321] Foo" })
} }
const enhancedService = createEnhancer(document)(remoteServices)('github-pr', url) const service = matcher(url)
expect(enhancedService.id).toEqual( 'hundertzehn-mocoapp-github-pr-123') const enhancedService = createEnhancer(document)(url)(service)
expect(enhancedService.description).toEqual('This is always the same text') expect(enhancedService.id).toEqual("hundertzehn-mocoapp-github-pr-123")
expect(enhancedService.projectId).toEqual('4321') expect(enhancedService.description).toEqual(
"This is always the same text"
)
expect(enhancedService.projectId).toEqual("4321")
expect(enhancedService.taskId).toBe(undefined) expect(enhancedService.taskId).toBe(undefined)
}) })
}) })
}) })
}) })

View File

@@ -38,7 +38,7 @@ module.exports = {
} }
}, },
{ {
test: /\.(png)$/, test: /\.(jpg|png)$/,
loader: "file-loader", loader: "file-loader",
options: { options: {
name: "[path][name].[ext]" name: "[path][name].[ext]"