diff --git a/src/db/migrations/20181021144645-AddGrafanaWidget.ts b/src/db/migrations/20181021144645-AddGrafanaWidget.ts index a2caa60..6ebfc73 100644 --- a/src/db/migrations/20181021144645-AddGrafanaWidget.ts +++ b/src/db/migrations/20181021144645-AddGrafanaWidget.ts @@ -1,5 +1,4 @@ import { QueryInterface } from "sequelize"; -import { DataType } from "sequelize-typescript"; export default { up: (queryInterface: QueryInterface) => { diff --git a/src/db/migrations/20181021152145-AddTradingViewWidget.ts b/src/db/migrations/20181021152145-AddTradingViewWidget.ts new file mode 100644 index 0000000..328893b --- /dev/null +++ b/src/db/migrations/20181021152145-AddTradingViewWidget.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkInsert("dimension_widgets", [ + { + type: "tradingview", + name: "TradingView", + avatarUrl: "/img/avatars/tradingview.png", + isEnabled: true, + isPublic: true, + description: "Monitor your favourite cryptocurrencies", + } + ])); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkDelete("dimension_widgets", { + type: "tradingview", + })); + } +} \ No newline at end of file diff --git a/web/app/app.module.ts b/web/app/app.module.ts index cc683d4..c341a25 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -97,6 +97,8 @@ import { GitterBridgeConfigComponent } from "./configs/bridge/gitter/gitter.brid import { GitterApiService } from "./shared/services/integrations/gitter-api.service"; import { GenericFullscreenWidgetWrapperComponent } from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component"; import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.widget.component"; +import { TradingViewWidgetConfigComponent } from "./configs/widget/tradingview/tradingview.widget.component"; +import { TradingViewWidgetWrapperComponent } from "./widget-wrappers/tradingview/tradingview.component"; @NgModule({ imports: [ @@ -177,6 +179,8 @@ import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.w GitterBridgeConfigComponent, GenericFullscreenWidgetWrapperComponent, GrafanaWidgetConfigComponent, + TradingViewWidgetConfigComponent, + TradingViewWidgetWrapperComponent, // Vendor ], diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 1215f2a..e0c926b 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -35,6 +35,8 @@ import { AdminGitterBridgeComponent } from "./admin/bridges/gitter/gitter.compon import { GitterBridgeConfigComponent } from "./configs/bridge/gitter/gitter.bridge.component"; import { GenericFullscreenWidgetWrapperComponent } from "./widget-wrappers/generic-fullscreen/generic-fullscreen.component"; import { GrafanaWidgetConfigComponent } from "./configs/widget/grafana/grafana.widget.component"; +import { TradingViewWidgetConfigComponent } from "./configs/widget/tradingview/tradingview.widget.component"; +import { TradingViewWidgetWrapperComponent } from "./widget-wrappers/tradingview/tradingview.component"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -167,6 +169,11 @@ const routes: Routes = [ component: GrafanaWidgetConfigComponent, data: {breadcrumb: "Grafana Widgets", name: "Grafana Widgets"}, }, + { + path: "tradingview", + component: TradingViewWidgetConfigComponent, + data: {breadcrumb: "TradingView Widgets", name: "TradingView Widgets"}, + }, ], }, { @@ -225,6 +232,7 @@ const routes: Routes = [ {path: "gcal", component: GCalWidgetWrapperComponent}, {path: "stickerpicker", component: StickerPickerWidgetWrapperComponent}, {path: "generic-fullscreen", component: GenericFullscreenWidgetWrapperComponent}, + {path: "tradingview", component: TradingViewWidgetWrapperComponent}, ] }, ]; diff --git a/web/app/configs/widget/google-calendar/gcal.widget.component.ts b/web/app/configs/widget/google-calendar/gcal.widget.component.ts index 3b1e370..7d88dda 100644 --- a/web/app/configs/widget/google-calendar/gcal.widget.component.ts +++ b/web/app/configs/widget/google-calendar/gcal.widget.component.ts @@ -38,6 +38,6 @@ export class GoogleCalendarWidgetConfigComponent extends WidgetComponent { } const encodedId = encodeURIComponent(widget.dimension.newData.src); - widget.dimension.newUrl = window.location.origin + "/widget/gcal?calendarId=" + encodedId; + widget.dimension.newUrl = window.location.origin + "/widgets/gcal?calendarId=" + encodedId; } } \ No newline at end of file diff --git a/web/app/configs/widget/tradingview/tradingview.widget.component.html b/web/app/configs/widget/tradingview/tradingview.widget.component.html new file mode 100644 index 0000000..c72e682 --- /dev/null +++ b/web/app/configs/widget/tradingview/tradingview.widget.component.html @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/web/app/configs/widget/tradingview/tradingview.widget.component.scss b/web/app/configs/widget/tradingview/tradingview.widget.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/configs/widget/tradingview/tradingview.widget.component.ts b/web/app/configs/widget/tradingview/tradingview.widget.component.ts new file mode 100644 index 0000000..397d57d --- /dev/null +++ b/web/app/configs/widget/tradingview/tradingview.widget.component.ts @@ -0,0 +1,98 @@ +import { DISABLE_AUTOMATIC_WRAPPING, WidgetComponent } from "../widget.component"; +import { EditableWidget, WIDGET_TRADINGVIEW } from "../../../shared/models/widget"; +import { Component } from "@angular/core"; + +@Component({ + templateUrl: "tradingview.widget.component.html", + styleUrls: ["tradingview.widget.component.scss"], +}) +export class TradingViewWidgetConfigComponent extends WidgetComponent { + + public readonly intervals = [ + {value: '1', label: '1 Minute'}, + {value: '3', label: '3 Minutes'}, + {value: '5', label: '5 Minutes'}, + {value: '15', label: '15 Minutes'}, + {value: '30', label: '30 Minutes'}, + {value: '60', label: '1 Hour'}, + {value: '120', label: '2 Hours'}, + {value: '180', label: '3 Hours'}, + {value: '240', label: '4 Hours'}, + {value: 'D', label: '1 Day'}, + {value: 'W', label: '1 Week'}, + ]; + + public readonly pairs = [ + // USD + {value: 'COINBASE:BTCUSD', label: 'Bitcoin / US Dollar'}, + {value: 'COINBASE:ETHUSD', label: 'Ethereum / US Dollar'}, + {value: 'COINBASE:LTCUSD', label: 'Litecoin / US Dollar'}, + {value: 'BITTREX:SNTUSD', label: 'Status Network Token / US Dollar'}, + {value: 'BITTREX:ETCUSD', label: 'Ethereum Classic / US Dollar'}, + {value: 'BITFINEX:BTGUSD', label: 'BTG / US Dollar'}, + {value: 'BITTREX:DASHUSD', label: 'Dash / US Dollar'}, + {value: 'BITFINEX:EOSUSD', label: 'EOS / US Dollar'}, + {value: 'BITFINEX:IOTUSD', label: 'IOTA / US Dollar'}, + {value: 'BITTREX:LSKUSD', label: 'Lisk / US Dollar'}, + {value: 'BITTREX:OMGUSD', label: 'OmiseGo / US Dollar'}, + {value: 'BITTREX:NEOUSD', label: 'NEO / US Dollar'}, + {value: 'BITTREX:XRPUSD', label: 'Ripple / US Dollar'}, + {value: 'BITFINEX:ZECUSD', label: 'Zcash / US Dollar'}, + {value: 'BITFINEX:XMRUSD', label: 'Monero / US Dollar'}, + + // Euro / GBP + {value: 'COINBASE:BTCEUR', label: 'Bitcoin / Euro'}, + {value: 'COINBASE:ETHEUR', label: 'Ethereum / Euro'}, + {value: 'COINBASE:LTCEUR', label: 'Litecoin / Euro'}, + {value: 'COINBASE:BTCGBP', label: 'Bitcoin / GBP'}, + + // Bitcoin + {value: 'COINBASE:ETHBTC', label: 'Ethereum / Bitcoin'}, + {value: 'COINBASE:LTCBTC', label: 'Litecoin / Bitcoin'}, + {value: 'BITTREX:SNTBTC', label: 'Status Network Token / Bitcoin'}, + {value: 'BITTREX:BCCBTC', label: 'Bitcoin Cash / Bitcoin'}, + {value: 'BITTREX:ADABTC', label: 'Ada / Bitcoin'}, + {value: 'BITTREX:ARKBTC', label: 'Ark / Bitcoin'}, + {value: 'BITTREX:EMC2BTC', label: 'Einsteinium / Bitcoin'}, + {value: 'BITFINEX:IOTBTC', label: 'IOTA / Bitcoin'}, + {value: 'BITTREX:LSKBTC', label: 'Lisk / Bitcoin'}, + {value: 'BITTREX:NEOBTC', label: 'Neo / Bitcoin'}, + {value: 'BITTREX:OMGBTC', label: 'OmiseGO / Bitcoin'}, + {value: 'BITTREX:POWRBTC', label: 'PowerLedger / Bitcoin'}, + {value: 'BITTREX:STRATBTC', label: 'Stratis / Bitcoin'}, + {value: 'BITTREX:TRIGBTC', label: 'TRIG Token / Bitcoin'}, + {value: 'BITTREX:VTCBTC', label: 'Vertcoin / Bitcoin'}, + {value: 'BITTREX:XLMBTC', label: 'Lumen / Bitcoin'}, + {value: 'BITTREX:XRPBTC', label: 'Ripple / Bitcoin'}, + + // Misc + {value: 'BITTREX:BTCUSDT', label: 'Bitcoin / Tether USD'}, + {value: 'BITTREX:ETHUSDT', label: 'Ethereum / Tether USD'}, + {value: 'BITTREX:SNTETH', label: 'Status Network Token / Ethereum'}, + {value: 'BITTREX:BCCUSDT', label: 'Bitcoin Cash / Tether USD'}, + {value: 'BITTREX:NEOUSDT', label: 'Neo / Tether'}, + ]; + + constructor() { + super(WIDGET_TRADINGVIEW, "TradingView Chart", DISABLE_AUTOMATIC_WRAPPING, "tradingView"); + } + + protected OnNewWidgetPrepared(widget: EditableWidget): void { + widget.dimension.newData.interval = "D"; // 1 day + widget.dimension.newData.pair = this.pairs[0].value; + } + + protected OnWidgetBeforeAdd(widget: EditableWidget): void { + this.setViewUrl(widget); + } + + protected OnWidgetBeforeEdit(widget: EditableWidget) { + this.setViewUrl(widget); + } + + private setViewUrl(widget: EditableWidget) { + const pair = this.pairs.find(p => p.value === widget.dimension.newData.pair); + widget.dimension.newTitle = pair ? pair.label : null; + widget.dimension.newUrl = window.location.origin + "/widgets/tradingview?pair=$pair&interval=$interval"; + } +} \ No newline at end of file diff --git a/web/app/home/home.component.html b/web/app/home/home.component.html index dfd1185..9f42ff9 100644 --- a/web/app/home/home.component.html +++ b/web/app/home/home.component.html @@ -40,8 +40,8 @@ Etherpad
- - Grafana + + TradingView
@@ -51,6 +51,10 @@ Twitch Livestream
+
+ + Grafana +
Google Docs diff --git a/web/app/shared/models/widget.ts b/web/app/shared/models/widget.ts index 65f7ec8..828834f 100644 --- a/web/app/shared/models/widget.ts +++ b/web/app/shared/models/widget.ts @@ -9,6 +9,7 @@ export const WIDGET_YOUTUBE = ["youtube", "dimension-youtube"]; export const WIDGET_GRAFANA = ["grafana", "dimension-grafana"]; export const WIDGET_TWITCH = ["twitch", "dimension-twitch"]; export const WIDGET_STICKER_PICKER = ["m.stickerpicker"]; +export const WIDGET_TRADINGVIEW = ["tradingview", "dimension-tradingview"]; export interface EditableWidget { /** diff --git a/web/app/shared/registry/integrations.registry.ts b/web/app/shared/registry/integrations.registry.ts index 09de201..27b12ba 100644 --- a/web/app/shared/registry/integrations.registry.ts +++ b/web/app/shared/registry/integrations.registry.ts @@ -6,6 +6,7 @@ import { WIDGET_GOOGLE_DOCS, WIDGET_GRAFANA, WIDGET_JITSI, + WIDGET_TRADINGVIEW, WIDGET_TWITCH, WIDGET_YOUTUBE } from "../models/widget"; @@ -51,7 +52,10 @@ export class IntegrationsRegistry { }, "grafana": { types: WIDGET_GRAFANA, - } + }, + "tradingview": { + types: WIDGET_TRADINGVIEW, + }, }, }; diff --git a/web/app/widget-wrappers/tradingview/tradingview.component.html b/web/app/widget-wrappers/tradingview/tradingview.component.html new file mode 100644 index 0000000..4a8002a --- /dev/null +++ b/web/app/widget-wrappers/tradingview/tradingview.component.html @@ -0,0 +1,2 @@ +
+
\ No newline at end of file diff --git a/web/app/widget-wrappers/tradingview/tradingview.component.scss b/web/app/widget-wrappers/tradingview/tradingview.component.scss new file mode 100644 index 0000000..4e9c7e5 --- /dev/null +++ b/web/app/widget-wrappers/tradingview/tradingview.component.scss @@ -0,0 +1,8 @@ +// component styles are encapsulated and only applied to their components +#tradingviewContainer { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; +} \ No newline at end of file diff --git a/web/app/widget-wrappers/tradingview/tradingview.component.ts b/web/app/widget-wrappers/tradingview/tradingview.component.ts new file mode 100644 index 0000000..c1335fd --- /dev/null +++ b/web/app/widget-wrappers/tradingview/tradingview.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import * as $ from "jquery"; + +declare var TradingView: any; + +@Component({ + selector: "my-tradingview-widget-wrapper", + templateUrl: "tradingview.component.html", + styleUrls: ["tradingview.component.scss"], +}) +export class TradingViewWidgetWrapperComponent implements OnInit { + + private symbol: string; + private interval: string; + + constructor(activatedRoute: ActivatedRoute) { + let params: any = activatedRoute.snapshot.queryParams; + + this.symbol = params.pair; + this.interval = params.interval; + } + + public ngOnInit() { + $.getScript("https://s3.tradingview.com/tv.js", () => { + new TradingView.widget({ + "autosize": true, + "symbol": this.symbol, + "interval": this.interval, + "timezone": "Etc/UTC", + "theme": "Light", + "style": "1", + "locale": "en", + "toolbar_bg": "#f1f3f6", + "enable_publishing": false, + "hide_top_toolbar": true, + "hide_legend": true, + "container_id": "tradingviewContainer", + }); + }); + } +} diff --git a/web/public/img/avatars/tradingview.png b/web/public/img/avatars/tradingview.png new file mode 100644 index 0000000..517de82 Binary files /dev/null and b/web/public/img/avatars/tradingview.png differ