From ee7a69b1fc08aafbb0b73677ed054ca00a42be1e Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Tue, 12 Nov 2019 00:02:15 -0800 Subject: [PATCH] Give structure to dashboard app (#163) * Separate out files for different responsibilities. 1. Add src/plugins directory. This directory holds one typescript file per plugin. Each plugin is optionally can be displayed as a tab on the UI. 2. Move WebsocketApi to ws.ts. This file contains all websocket APIs provided by dashboard.py backend. * Make dashboard pluggable * Move devtools under core too * Register tabs dynamically * Typescript fixes for abstract interfaces * Initialize plugin app body skeleton * Call activated / deactivated on tab change * Move plugin name within plugin classes and initialize plugin within proxy dashboard constructor * templatize api development plugin * eslint fixes * use globs * Remove useless constructors --- dashboard/.eslintrc.json | 10 +- dashboard/package.json | 2 +- dashboard/src/{ => core}/devtools.ts | 0 dashboard/src/core/plugin.ts | 52 ++++ dashboard/src/core/plugins/home.ts | 26 ++ dashboard/src/core/plugins/inspect_traffic.ts | 30 +++ dashboard/src/core/plugins/settings.ts | 26 ++ dashboard/src/core/plugins/traffic_control.ts | 26 ++ dashboard/src/core/ws.ts | 141 +++++++++++ dashboard/src/plugins/mock_rest_api.ts | 178 ++++++++++++++ dashboard/src/plugins/shortlink.ts | 26 ++ dashboard/src/proxy.css | 4 +- dashboard/src/proxy.html | 100 +------- dashboard/src/proxy.ts | 231 ++++-------------- dashboard/test/test.ts | 13 +- 15 files changed, 577 insertions(+), 288 deletions(-) rename dashboard/src/{ => core}/devtools.ts (100%) create mode 100644 dashboard/src/core/plugin.ts create mode 100644 dashboard/src/core/plugins/home.ts create mode 100644 dashboard/src/core/plugins/inspect_traffic.ts create mode 100644 dashboard/src/core/plugins/settings.ts create mode 100644 dashboard/src/core/plugins/traffic_control.ts create mode 100644 dashboard/src/core/ws.ts create mode 100644 dashboard/src/plugins/mock_rest_api.ts create mode 100644 dashboard/src/plugins/shortlink.ts diff --git a/dashboard/.eslintrc.json b/dashboard/.eslintrc.json index 1ad2585834..03b8e6bc36 100644 --- a/dashboard/.eslintrc.json +++ b/dashboard/.eslintrc.json @@ -19,5 +19,13 @@ "@typescript-eslint" ], "rules": { - } + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "rules": { + "@typescript-eslint/no-unused-vars": [2, { "args": "none" }] + } + } + ] } diff --git a/dashboard/package.json b/dashboard/package.json index 333dd34733..a744b5c460 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "clean": "rm -rf tsbuild", - "lint": "eslint --global $ src/*.ts", + "lint": "eslint --global $ \"src/**/*.ts\" \"test/**/*.ts\"", "pretest": "npm run clean && npm run lint && tsc --target es5 --outDir tsbuild test/test.ts", "test": "jasmine tsbuild/test/test.js", "build": "npm test && rollup -c", diff --git a/dashboard/src/devtools.ts b/dashboard/src/core/devtools.ts similarity index 100% rename from dashboard/src/devtools.ts rename to dashboard/src/core/devtools.ts diff --git a/dashboard/src/core/plugin.ts b/dashboard/src/core/plugin.ts new file mode 100644 index 0000000000..bb146ad4fc --- /dev/null +++ b/dashboard/src/core/plugin.ts @@ -0,0 +1,52 @@ +/* + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable + proxy server for Application debugging, testing and development. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +*/ +import { WebsocketApi } from './ws' + +export interface IDashboardPlugin { + name: string + initializeTab(): JQuery + initializeSkeleton(): JQuery + activated(): void + deactivated(): void +} + +export interface IPluginConstructor { + new (websocketApi: WebsocketApi): IDashboardPlugin +} + +export abstract class DashboardPlugin implements IDashboardPlugin { + public abstract readonly name: string + protected websocketApi: WebsocketApi + + public constructor (websocketApi: WebsocketApi) { + this.websocketApi = websocketApi + } + + public makeTab (name: string, icon: string) : JQuery { + return $('') + .attr({ + href: '#', + plugin_name: this.name + }) + .addClass('nav-link') + .text(name) + .prepend( + $('') + .addClass('fa') + .addClass('fa-fw') + .addClass(icon) + ) + } + + public abstract initializeTab() : JQuery + public abstract initializeSkeleton(): JQuery + public abstract activated(): void + public abstract deactivated(): void +} diff --git a/dashboard/src/core/plugins/home.ts b/dashboard/src/core/plugins/home.ts new file mode 100644 index 0000000000..0ba6cf6133 --- /dev/null +++ b/dashboard/src/core/plugins/home.ts @@ -0,0 +1,26 @@ +/* + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable + proxy server for Application debugging, testing and development. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +*/ +import { DashboardPlugin } from '../plugin' + +export class HomePlugin extends DashboardPlugin { + public name: string = 'home' + + public initializeTab () : JQuery { + return this.makeTab('Home', 'fa-home') + } + + public initializeSkeleton (): JQuery { + return $('
') + } + + public activated (): void {} + + public deactivated (): void {} +} diff --git a/dashboard/src/core/plugins/inspect_traffic.ts b/dashboard/src/core/plugins/inspect_traffic.ts new file mode 100644 index 0000000000..ba030da781 --- /dev/null +++ b/dashboard/src/core/plugins/inspect_traffic.ts @@ -0,0 +1,30 @@ +/* + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable + proxy server for Application debugging, testing and development. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +*/ +import { DashboardPlugin } from '../plugin' + +export class InspectTrafficPlugin extends DashboardPlugin { + public name: string = 'inspect_traffic' + + public initializeTab () : JQuery { + return this.makeTab('Inspect Traffic', 'fa-binoculars') + } + + public initializeSkeleton (): JQuery { + return $('
') + } + + public activated (): void { + this.websocketApi.enableInspection() + } + + public deactivated (): void { + this.websocketApi.disableInspection() + } +} diff --git a/dashboard/src/core/plugins/settings.ts b/dashboard/src/core/plugins/settings.ts new file mode 100644 index 0000000000..7cbb92d427 --- /dev/null +++ b/dashboard/src/core/plugins/settings.ts @@ -0,0 +1,26 @@ +/* + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable + proxy server for Application debugging, testing and development. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +*/ +import { DashboardPlugin } from '../plugin' + +export class SettingsPlugin extends DashboardPlugin { + public name: string = 'settings' + + public initializeTab () : JQuery { + return this.makeTab('Settings', 'fa-clog') + } + + public initializeSkeleton (): JQuery { + return $('
') + } + + public activated (): void {} + + public deactivated (): void {} +} diff --git a/dashboard/src/core/plugins/traffic_control.ts b/dashboard/src/core/plugins/traffic_control.ts new file mode 100644 index 0000000000..552b9067cd --- /dev/null +++ b/dashboard/src/core/plugins/traffic_control.ts @@ -0,0 +1,26 @@ +/* + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable + proxy server for Application debugging, testing and development. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +*/ +import { DashboardPlugin } from '../plugin' + +export class TrafficControlPlugin extends DashboardPlugin { + public name: string = 'traffic_control' + + public initializeTab () : JQuery { + return this.makeTab('Traffic Controls', 'fa-lock') + } + + public initializeSkeleton (): JQuery { + return $('
') + } + + public activated (): void {} + + public deactivated (): void {} +} diff --git a/dashboard/src/core/ws.ts b/dashboard/src/core/ws.ts new file mode 100644 index 0000000000..674b183886 --- /dev/null +++ b/dashboard/src/core/ws.ts @@ -0,0 +1,141 @@ +/* + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable + proxy server for Application debugging, testing and development. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +*/ + +export class WebsocketApi { + private hostname: string = window.location.hostname ? window.location.hostname : 'localhost'; + private port: number = window.location.port ? Number(window.location.port) : 8899; + // TODO: Must map to route registered by dashboard.py, don't hardcode + private wsPrefix: string = '/dashboard'; + private wsScheme: string = window.location.protocol === 'http:' ? 'ws' : 'wss'; + private ws: WebSocket; + private wsPath: string = this.wsScheme + '://' + this.hostname + ':' + this.port + this.wsPrefix; + + private mid: number = 0; + private lastPingId: number; + private lastPingTime: number; + + private readonly schedulePingEveryMs: number = 1000; + private readonly scheduleReconnectEveryMs: number = 5000; + + private serverPingTimer: number; + private serverConnectTimer: number; + + private inspectionEnabled: boolean; + + constructor () { + this.scheduleServerConnect(0) + } + + public static getTime () { + const date = new Date() + return date.getTime() + } + + public enableInspection () { + // TODO: Set flag to true only once response has been received from the server + this.inspectionEnabled = true + this.ws.send(JSON.stringify({ id: this.mid, method: 'enable_inspection' })) + this.mid++ + } + + public disableInspection () { + this.inspectionEnabled = false + this.ws.send(JSON.stringify({ id: this.mid, method: 'disable_inspection' })) + this.mid++ + } + + private scheduleServerConnect (after_ms: number = this.scheduleReconnectEveryMs) { + this.clearServerConnectTimer() + this.serverConnectTimer = window.setTimeout( + this.connectToServer.bind(this), after_ms) + } + + private connectToServer () { + this.ws = new WebSocket(this.wsPath) + this.ws.onopen = this.onServerWSOpen.bind(this) + this.ws.onmessage = this.onServerWSMessage.bind(this) + this.ws.onerror = this.onServerWSError.bind(this) + this.ws.onclose = this.onServerWSClose.bind(this) + } + + private clearServerConnectTimer () { + if (this.serverConnectTimer == null) { + return + } + window.clearTimeout(this.serverConnectTimer) + this.serverConnectTimer = null + } + + private scheduleServerPing (after_ms: number = this.schedulePingEveryMs) { + this.clearServerPingTimer() + this.serverPingTimer = window.setTimeout( + this.pingServer.bind(this), after_ms) + } + + private pingServer () { + this.lastPingId = this.mid + this.lastPingTime = WebsocketApi.getTime() + this.mid++ + // console.log('Pinging server with id:%d', this.last_ping_id); + this.ws.send(JSON.stringify({ id: this.lastPingId, method: 'ping' })) + } + + private clearServerPingTimer () { + if (this.serverPingTimer != null) { + window.clearTimeout(this.serverPingTimer) + this.serverPingTimer = null + } + this.lastPingTime = null + this.lastPingId = null + } + + private onServerWSOpen (ev: MessageEvent) { + this.clearServerConnectTimer() + WebsocketApi.setServerStatusSuccess('Connected...') + this.scheduleServerPing(0) + } + + private onServerWSMessage (ev: MessageEvent) { + const message = JSON.parse(ev.data) + if (message.id === this.lastPingId) { + WebsocketApi.setServerStatusSuccess( + String((WebsocketApi.getTime() - this.lastPingTime) + ' ms')) + this.clearServerPingTimer() + this.scheduleServerPing() + } else { + console.log(message) + } + } + + private onServerWSError (ev: MessageEvent) { + WebsocketApi.setServerStatusDanger() + } + + private onServerWSClose (ev: MessageEvent) { + this.clearServerPingTimer() + this.scheduleServerConnect() + WebsocketApi.setServerStatusDanger() + } + + public static setServerStatusDanger () { + $('#proxyServerStatus').parent('div') + .removeClass('text-success') + .addClass('text-danger') + $('#proxyServerStatusSummary').text('') + } + + public static setServerStatusSuccess (summary: string) { + $('#proxyServerStatus').parent('div') + .removeClass('text-danger') + .addClass('text-success') + $('#proxyServerStatusSummary').text( + '(' + summary + ')') + } +} diff --git a/dashboard/src/plugins/mock_rest_api.ts b/dashboard/src/plugins/mock_rest_api.ts new file mode 100644 index 0000000000..cf2b43b18f --- /dev/null +++ b/dashboard/src/plugins/mock_rest_api.ts @@ -0,0 +1,178 @@ +/* + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable + proxy server for Application debugging, testing and development. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +*/ +import { DashboardPlugin } from '../core/plugin' +import { WebsocketApi } from '../core/ws' + +export class MockRestApiPlugin extends DashboardPlugin { + public name: string = 'api_development'; + + private specs: Map>; + + constructor (websocketApi: WebsocketApi) { + super(websocketApi) + this.specs = new Map() + this.fetchExistingSpecs() + } + + public initializeTab () : JQuery { + return this.makeTab('API Development', 'fa-connectdevelop') + } + + public initializeSkeleton (): JQuery { + return $('
') + .attr('id', 'app-header') + .append( + $('
') + .addClass('container-fluid') + .append( + $('
') + .addClass('row') + .append( + $('
') + .addClass('col-6') + .append( + $('

') + .addClass('h3') + .text('API Development') + ) + ) + .append( + $('
') + .addClass('col-6') + .addClass('text-right') + .append( + $('') + .attr('type', 'button') + .addClass('btn') + .addClass('btn-primary') + .text('Create New API') + .prepend( + $('') + .addClass('fa') + .addClass('fa-fw') + .addClass('fa-plus-circle') + ) + ) + ) + ) + ) + .add( + $('
') + .attr('id', 'app-body') + .append( + $('
') + .addClass('list-group') + .addClass('position-relative') + .append( + $('
') + .attr('href', '#') + .addClass('list-group-item default text-decoration-none bg-light') + .attr('data-toggle', 'collapse') + .attr('data-target', '#api-example-com-path-specs') + .attr('data-parent', '#proxyDashboard') + .text('api.example.com') + .append( + $('') + .addClass('badge badge-info') + .text('3 Resources') + ) + ) + .append( + $('') + .addClass('position-absolute fa fa-close ml-auto btn btn-danger remove-api-spec') + .attr('title', 'Delete api.example.com') + ) + .append( + $('
') + .addClass('collapse api-path-spec') + .attr('id', 'api-example-com-path-specs') + .append( + $('
') + .addClass('list-group-item bg-light') + .text('/v1/users/') + ) + .append( + $('
') + .addClass('list-group-item bg-light') + .text('/v1/groups/') + ) + .append( + $('
') + .addClass('list-group-item bg-light') + .text('/v1/messages/') + ) + ) + ) + .append( + $('
') + .addClass('list-group') + .addClass('position-relative') + .append( + $('') + .attr('href', '#') + .addClass('list-group-item default text-decoration-none bg-light') + .attr('data-toggle', 'collapse') + .attr('data-target', '#my-api') + .attr('data-parent', '#proxyDashboard') + .text('my.api') + .append( + $('') + .addClass('badge badge-info') + .text('1 Resource') + ) + ) + .append( + $('') + .addClass('position-absolute fa fa-close ml-auto btn btn-danger remove-api-spec') + .attr('title', 'Delete my.api') + ) + .append( + $('
') + .addClass('collapse api-path-spec') + .attr('id', 'my-api') + .append( + $('
') + .addClass('list-group-item bg-light') + .text('/api/') + ) + ) + ) + ) + } + + public activated (): void {} + + public deactivated (): void {} + + private fetchExistingSpecs () { + // TODO: Fetch list of currently configured APIs from the backend + const apiExampleOrgSpec = new Map() + apiExampleOrgSpec.set('/v1/users/', { + count: 2, + next: null, + previous: null, + results: [ + { + email: 'you@example.com', + groups: [], + url: 'api.example.org/v1/users/1/', + username: 'admin' + }, + { + email: 'someone@example.com', + groups: [], + url: 'api.example.org/v1/users/2/', + username: 'someone' + } + ] + }) + this.specs.set('api.example.org', apiExampleOrgSpec) + } +} diff --git a/dashboard/src/plugins/shortlink.ts b/dashboard/src/plugins/shortlink.ts new file mode 100644 index 0000000000..ab01d33fdd --- /dev/null +++ b/dashboard/src/plugins/shortlink.ts @@ -0,0 +1,26 @@ +/* + proxy.py + ~~~~~~~~ + ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable + proxy server for Application debugging, testing and development. + + :copyright: (c) 2013-present by Abhinav Singh and contributors. + :license: BSD, see LICENSE for more details. +*/ +import { DashboardPlugin } from '../core/plugin' + +export class ShortlinkPlugin extends DashboardPlugin { + public name: string = 'shortlink'; + + public initializeTab () : JQuery { + return this.makeTab('Short Links', 'fa-bolt') + } + + public initializeSkeleton (): JQuery { + return $('
') + } + + public activated (): void {} + + public deactivated (): void {} +} diff --git a/dashboard/src/proxy.css b/dashboard/src/proxy.css index af99872d24..955c1144e3 100644 --- a/dashboard/src/proxy.css +++ b/dashboard/src/proxy.css @@ -7,11 +7,11 @@ :copyright: (c) 2013-present by Abhinav Singh and contributors. :license: BSD, see LICENSE for more details. */ -#app { +#proxyDashboard { background-color: #eeeeee; height: 100%; } -#app .remove-api-spec { +#proxyDashboard .remove-api-spec { top: 10px; right: 15px; } diff --git a/dashboard/src/proxy.html b/dashboard/src/proxy.html index d7338c5e4a..9142b3f63c 100644 --- a/dashboard/src/proxy.html +++ b/dashboard/src/proxy.html @@ -27,105 +27,11 @@ -
-
-
-
-
-
-
-

API Development

-
-
- -
-
-
-
-
-
- - api.example.com 3 Resources - - -
-
- /v1/users/ -
-
- /v1/groups/ -
-
- /v1/messages/ -
-
-
-
- - my.api 1 Resource - - -
-
- /api/ -
-
-
-
-
-
- -
-
-
+