@barchart/portfolio-client-js 1.4.7 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.releases/2.0.0.md +10 -0
- package/gulpfile.js +2 -11
- package/lib/common/Configuration.js +46 -23
- package/lib/gateway/PortfolioGateway.js +150 -127
- package/lib/index.js +2 -2
- package/lib/security/JwtProvider.js +188 -0
- package/lib/security/meta.js +14 -0
- package/package.json +6 -6
- package/example/example.css +0 -124
- package/example/example.html +0 -98
- package/example/example.js +0 -22074
- package/example/js/startup.js +0 -963
- package/lib/gateway/jwt/JwtGateway.js +0 -410
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const assert = require('@barchart/common-js/lang/assert'),
|
|
2
|
+
Disposable = require('@barchart/common-js/lang/Disposable'),
|
|
3
|
+
is = require('@barchart/common-js/lang/is'),
|
|
4
|
+
random = require('@barchart/common-js/lang/random'),
|
|
5
|
+
Scheduler = require('@barchart/common-js/timing/Scheduler');
|
|
6
|
+
|
|
7
|
+
const EndpointBuilder = require('@barchart/common-js/api/http/builders/EndpointBuilder'),
|
|
8
|
+
Gateway = require('@barchart/common-js/api/http/Gateway'),
|
|
9
|
+
ProtocolType = require('@barchart/common-js/api/http/definitions/ProtocolType'),
|
|
10
|
+
ResponseInterceptor = require('@barchart/common-js/api/http/interceptors/ResponseInterceptor'),
|
|
11
|
+
VerbType = require('@barchart/common-js/api/http/definitions/VerbType');
|
|
12
|
+
|
|
13
|
+
const Configuration = require('../common/Configuration');
|
|
14
|
+
|
|
15
|
+
module.exports = (() => {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_REFRESH_INTERVAL_MILLISECONDS = 5 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generates and caches a signed token (using a delegate). The cached token
|
|
22
|
+
* is refreshed periodically.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
* @exported
|
|
26
|
+
* @param {Callbacks.JwtTokenGenerator} tokenGenerator - An anonymous function which returns a signed JWT token.
|
|
27
|
+
* @param {Number=} refreshInterval - The number of milliseconds which must pass before a new JWT token is generated. A zero value means the token should never be refreshed. A null or undefined value means the token is not cached.
|
|
28
|
+
*/
|
|
29
|
+
class JwtProvider extends Disposable {
|
|
30
|
+
constructor(tokenGenerator, refreshInterval) {
|
|
31
|
+
super();
|
|
32
|
+
|
|
33
|
+
assert.argumentIsRequired(tokenGenerator, 'tokenGenerator', Function);
|
|
34
|
+
assert.argumentIsOptional(refreshInterval, 'refreshInterval', Number);
|
|
35
|
+
|
|
36
|
+
this._tokenGenerator = tokenGenerator;
|
|
37
|
+
|
|
38
|
+
this._tokenPromise = null;
|
|
39
|
+
|
|
40
|
+
this._refreshTimestamp = null;
|
|
41
|
+
this._refreshPending = false;
|
|
42
|
+
|
|
43
|
+
if (is.number(refreshInterval)) {
|
|
44
|
+
this._refreshInterval = Math.max(refreshInterval || 0, 0);
|
|
45
|
+
this._refreshJitter = random.range(0, Math.floor(this._refreshInterval / 10));
|
|
46
|
+
} else {
|
|
47
|
+
this._refreshInterval = null;
|
|
48
|
+
this._refreshJitter = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this._scheduler = new Scheduler();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reads the current token, refreshing if necessary.
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
* @returns {Promise<String>}
|
|
59
|
+
*/
|
|
60
|
+
getToken() {
|
|
61
|
+
return Promise.resolve()
|
|
62
|
+
.then(() => {
|
|
63
|
+
if (this._refreshPending) {
|
|
64
|
+
return this._tokenPromise;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (this._tokenPromise === null || this._refreshInterval === null || (this._refreshInterval > 0 && getTime() > (this._refreshTimestamp + this._refreshInterval + this._refreshJitter))) {
|
|
68
|
+
this._refreshPending = true;
|
|
69
|
+
|
|
70
|
+
this._tokenPromise = this._scheduler.backoff(() => this._tokenGenerator(), 100, 'Read JWT token', 3)
|
|
71
|
+
.then((token) => {
|
|
72
|
+
this._refreshTimestamp = getTime();
|
|
73
|
+
this._refreshPending = false;
|
|
74
|
+
|
|
75
|
+
return token;
|
|
76
|
+
}).catch((e) => {
|
|
77
|
+
this._tokenPromise = null;
|
|
78
|
+
|
|
79
|
+
this._refreshTimestamp = null;
|
|
80
|
+
this._refreshPending = false;
|
|
81
|
+
|
|
82
|
+
return Promise.reject(e);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return this._tokenPromise;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A factory for {@link JwtProvider} which is an alternative to the constructor.
|
|
92
|
+
*
|
|
93
|
+
* @public
|
|
94
|
+
* @static
|
|
95
|
+
* @exported
|
|
96
|
+
* @param {Callbacks.JwtTokenGenerator} tokenGenerator - An anonymous function which returns a signed JWT token.
|
|
97
|
+
* @param {Number=} refreshInterval - The number of milliseconds which must pass before a new JWT token is generated. A zero value means the token should never be refreshed. A null or undefined value means the token is not cached.
|
|
98
|
+
* @returns {JwtProvider}
|
|
99
|
+
*/
|
|
100
|
+
static fromTokenGenerator(tokenGenerator, refreshInterval) {
|
|
101
|
+
return new JwtProvider(tokenGenerator, refreshInterval);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Builds a {@link JwtProvider} which will generate tokens impersonating the specified
|
|
106
|
+
* user. These tokens will only work in the "test" environment.
|
|
107
|
+
*
|
|
108
|
+
* Recall, the "test" environment is not "secure" -- any data saved here can be accessed
|
|
109
|
+
* by anyone (using this feature). Furthermore, data is periodically purged from the
|
|
110
|
+
* test environment.
|
|
111
|
+
*
|
|
112
|
+
* @public
|
|
113
|
+
* @static
|
|
114
|
+
* @param {String} userId - The user identifier to impersonate.
|
|
115
|
+
* @param {String} contextId - The context identifier of the user to impersonate.
|
|
116
|
+
* @param {String=} permissions - The desired permission level.
|
|
117
|
+
* @returns {JwtProvider}
|
|
118
|
+
*/
|
|
119
|
+
static forTest(userId, contextId, permissions) {
|
|
120
|
+
return getJwtProviderForImpersonation(Configuration.getJwtImpersonationHost, 'test', userId, contextId, permissions);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Builds a {@link JwtProvider} which will generate tokens impersonating the specified
|
|
125
|
+
* user. The "development" environment is for Barchart use only and access is restricted
|
|
126
|
+
* to Barchart's internal network.
|
|
127
|
+
*
|
|
128
|
+
* @public
|
|
129
|
+
* @static
|
|
130
|
+
* @param {String} userId - The user identifier to impersonate.
|
|
131
|
+
* @param {String} contextId - The context identifier of the user to impersonate.
|
|
132
|
+
* @param {String=} permissions - The desired permission level.
|
|
133
|
+
* @returns {JwtProvider}
|
|
134
|
+
*/
|
|
135
|
+
static forDevelopment(userId, contextId, permissions) {
|
|
136
|
+
return getJwtProviderForImpersonation(Configuration.getJwtImpersonationHost, 'dev', userId, contextId, permissions);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_onDispose() {
|
|
140
|
+
this._scheduler.dispose();
|
|
141
|
+
this._scheduler = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
toString() {
|
|
145
|
+
return '[JwtProvider]';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getJwtProviderForImpersonation(host, environment, userId, contextId, permissions) {
|
|
150
|
+
assert.argumentIsRequired(host, 'host', String);
|
|
151
|
+
assert.argumentIsRequired(environment, 'environment', String);
|
|
152
|
+
assert.argumentIsRequired(userId, 'userId', String);
|
|
153
|
+
assert.argumentIsRequired(contextId, 'contextId', String);
|
|
154
|
+
assert.argumentIsOptional(permissions, 'permissions', String);
|
|
155
|
+
|
|
156
|
+
const tokenEndpoint = EndpointBuilder.for('generate-impersonation-jwt-for-test', 'generate JWT token for test environment')
|
|
157
|
+
.withVerb(VerbType.POST)
|
|
158
|
+
.withProtocol(ProtocolType.HTTPS)
|
|
159
|
+
.withHost(host)
|
|
160
|
+
.withPathBuilder((pb) =>
|
|
161
|
+
pb.withLiteralParameter('version', 'v1')
|
|
162
|
+
.withLiteralParameter('tokens', 'tokens')
|
|
163
|
+
.withLiteralParameter('impersonate', 'impersonate')
|
|
164
|
+
.withLiteralParameter('service', 'portfolio')
|
|
165
|
+
.withLiteralParameter('environment', environment)
|
|
166
|
+
)
|
|
167
|
+
.withBody()
|
|
168
|
+
.withResponseInterceptor(ResponseInterceptor.DATA)
|
|
169
|
+
.endpoint;
|
|
170
|
+
|
|
171
|
+
const payload = { };
|
|
172
|
+
|
|
173
|
+
payload.userId = userId;
|
|
174
|
+
payload.contextId = contextId;
|
|
175
|
+
|
|
176
|
+
if (permissions) {
|
|
177
|
+
payload.permissions = permissions;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return new JwtProvider(() => Gateway.invoke(tokenEndpoint, payload), DEFAULT_REFRESH_INTERVAL_MILLISECONDS);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getTime() {
|
|
184
|
+
return (new Date()).getTime();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return JwtProvider;
|
|
188
|
+
})();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A meta namespace containing signatures of anonymous functions.
|
|
3
|
+
*
|
|
4
|
+
* @namespace Callbacks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A function which returns a signed token.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
* @callback JwtTokenGenerator
|
|
12
|
+
* @memberOf Callbacks
|
|
13
|
+
* @returns {String|Promise<String>}
|
|
14
|
+
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barchart/portfolio-client-js",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "JavaScript library for interfacing with Barchart's Portfolio API",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bryan Ingle",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"SDK"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@barchart/common-js": "^3.
|
|
24
|
+
"@barchart/common-js": "^3.15.0",
|
|
25
25
|
"@barchart/portfolio-api-common": "^1.4.4"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@babel/core": "^7.
|
|
28
|
+
"@babel/core": "^7.11.1",
|
|
29
29
|
"babelify": "^10.0.0",
|
|
30
|
-
"browserify": "^16.5.
|
|
30
|
+
"browserify": "^16.5.2",
|
|
31
31
|
"git-get-status": "^1.0.5",
|
|
32
32
|
"glob": "^6.0.1",
|
|
33
33
|
"gulp": "^4.0.2",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"gulp-jasmine": "^2.2.1",
|
|
36
36
|
"gulp-jsdoc3": "^1.0.1",
|
|
37
37
|
"gulp-jshint": "~2.1.0",
|
|
38
|
-
"gulp-replace": "^0.5.4",
|
|
39
38
|
"gulp-prompt": "^1.2.0",
|
|
40
|
-
"
|
|
39
|
+
"gulp-replace": "^0.5.4",
|
|
40
|
+
"jshint": "^2.12.0",
|
|
41
41
|
"vinyl-buffer": "^1.0.1",
|
|
42
42
|
"vinyl-source-stream": "^2.0.0"
|
|
43
43
|
},
|
package/example/example.css
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
body {
|
|
2
|
-
font-family: "Gill Sans MT";
|
|
3
|
-
}
|
|
4
|
-
h4 {
|
|
5
|
-
margin: 0px;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.container-center-outer {
|
|
9
|
-
height: 100%;
|
|
10
|
-
width: 100%;
|
|
11
|
-
|
|
12
|
-
display: table;
|
|
13
|
-
}
|
|
14
|
-
.container-center-inner {
|
|
15
|
-
display: table-cell;
|
|
16
|
-
text-align: center;
|
|
17
|
-
vertical-align: middle;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.center {
|
|
21
|
-
text-align: center;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.header {
|
|
25
|
-
position: absolute;
|
|
26
|
-
z-index: 1000;
|
|
27
|
-
top: 0px;
|
|
28
|
-
left: 0px;
|
|
29
|
-
right: 0px;
|
|
30
|
-
height: 50px;
|
|
31
|
-
background-color: #29506D;
|
|
32
|
-
color: #FFFFFF;
|
|
33
|
-
padding-left: 20px;
|
|
34
|
-
padding-right: 20px;
|
|
35
|
-
}
|
|
36
|
-
.header h4 {
|
|
37
|
-
line-height: 50px;
|
|
38
|
-
margin-top: 0px;
|
|
39
|
-
margin-bottom: 0px;
|
|
40
|
-
}
|
|
41
|
-
.header .header-action-buttons {
|
|
42
|
-
margin-left: 20px;
|
|
43
|
-
}
|
|
44
|
-
.header .text-button {
|
|
45
|
-
line-height: 50px;
|
|
46
|
-
margin-right: 10px;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.header input {
|
|
50
|
-
position: relative;
|
|
51
|
-
top: -4px;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.header .input-group {
|
|
55
|
-
position: relative;
|
|
56
|
-
top: 6px;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.header .input-group input {
|
|
60
|
-
top: 1px;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.header .header-action-buttons .btn-group {
|
|
64
|
-
margin-top: 8px;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.main {
|
|
68
|
-
position: absolute;
|
|
69
|
-
height: auto;
|
|
70
|
-
top: 50px;
|
|
71
|
-
bottom: 40px;
|
|
72
|
-
left: 0px;
|
|
73
|
-
right: 0px;
|
|
74
|
-
overflow: auto;
|
|
75
|
-
padding-top: 20px;
|
|
76
|
-
padding-left: 20px;
|
|
77
|
-
padding-right: 20px;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.main .login {
|
|
81
|
-
margin: auto;
|
|
82
|
-
width: 300px;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.footer {
|
|
86
|
-
position: absolute;
|
|
87
|
-
bottom: 0px;
|
|
88
|
-
left: 0px;
|
|
89
|
-
right: 0px;
|
|
90
|
-
height: 40px;
|
|
91
|
-
background-color: #496D89;
|
|
92
|
-
opacity: 0.99;
|
|
93
|
-
z-index: 1;
|
|
94
|
-
padding-left: 20px;
|
|
95
|
-
padding-right: 20px;
|
|
96
|
-
}
|
|
97
|
-
.footer h4 {
|
|
98
|
-
font-size: 14px;
|
|
99
|
-
line-height: 40px;
|
|
100
|
-
margin-top: 0px;
|
|
101
|
-
margin-bottom: 0px;
|
|
102
|
-
color: white;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.watchlist-action-buttons .text-button {
|
|
106
|
-
margin-left: 10px;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
.text-button {
|
|
110
|
-
cursor: pointer;
|
|
111
|
-
}
|
|
112
|
-
.text-button:hover {
|
|
113
|
-
text-shadow: 0 0 1.2em #00FFFF;
|
|
114
|
-
}
|
|
115
|
-
.text-button.text-button-black {
|
|
116
|
-
color: #555555;
|
|
117
|
-
}
|
|
118
|
-
.text-button.text-button-black:hover {
|
|
119
|
-
text-shadow: 0 0 1.2em #FF0000;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
.btn {
|
|
123
|
-
text-shadow: none;
|
|
124
|
-
}
|
package/example/example.html
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8"/>
|
|
5
|
-
|
|
6
|
-
<title>Barchart Portfolio Client API</title>
|
|
7
|
-
|
|
8
|
-
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
|
9
|
-
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
|
10
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css" type="text/css">
|
|
11
|
-
<link rel="stylesheet" href="example.css" type="text/css">
|
|
12
|
-
|
|
13
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.23.0/polyfill.min.js"></script>
|
|
14
|
-
|
|
15
|
-
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
|
16
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
|
|
17
|
-
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
|
|
18
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
|
|
19
|
-
|
|
20
|
-
<script src="example.js"></script>
|
|
21
|
-
|
|
22
|
-
<script type="text/html" id="disconnected-template">
|
|
23
|
-
<div class="container-center-outer">
|
|
24
|
-
<div class="container-center-inner">
|
|
25
|
-
<form class="form-horizontal login">
|
|
26
|
-
<div class="form-group" data-bind="css: { 'has-error': user().length === 0 }, event: { keypress: handleLoginKeypress }">
|
|
27
|
-
<label class="pull-left">User</label>
|
|
28
|
-
<input class="form-control" data-bind="textInput: user" type="text" placeholder="User">
|
|
29
|
-
</div>
|
|
30
|
-
<div class="form-group" data-bind="css: { 'has-error': user().length === 0 }, event: { keypress: handleLoginKeypress }">
|
|
31
|
-
<label class="pull-left">Legacy User</label>
|
|
32
|
-
<input class="form-control" data-bind="textInput: userLegacy" type="text" placeholder="Legacy User">
|
|
33
|
-
</div>
|
|
34
|
-
<div class="form-group buttons">
|
|
35
|
-
<button class="form-control btn btn-primary" type="button" data-bind="click: connect, enable: canConnect">Connect</button>
|
|
36
|
-
</div>
|
|
37
|
-
</form>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
</script>
|
|
41
|
-
|
|
42
|
-
<script type="text/html" id="portfolio-header-template">
|
|
43
|
-
<div class="pull-left">
|
|
44
|
-
<h4 class="pull-left">Barchart Portfolio API <span data-bind="visible: connected"></span></h4>
|
|
45
|
-
|
|
46
|
-
<div class="pull-left header-action-buttons form-inline" style="margin-top: 12px;" data-bind="visible: connected() === true">
|
|
47
|
-
<input class="form-control" data-bind="value: portfolio" type="text" placeholder="Portfolio">
|
|
48
|
-
<input class="form-control" data-bind="value: position" type="text" placeholder="Position">
|
|
49
|
-
<input class="form-control" data-bind="value: transaction" type="text" placeholder="Transaction">
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<div class="pull-left">
|
|
54
|
-
<div class="pull-left header-action-buttons form-inline" data-bind="visible: connected() === true">
|
|
55
|
-
<div class="dropdown" style="margin-top: 8px;">
|
|
56
|
-
<button class="btn btn-default dropdown-toggle" style="width: 200px;" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
|
57
|
-
<span data-bind="text: mode().text"></span>
|
|
58
|
-
</button>
|
|
59
|
-
<ul class="dropdown-menu" data-bind="foreach: modes">
|
|
60
|
-
<li class="center"><a href="#" data-bind="text: $data.text, click: function() { $parent.setMode($data); }"></a></li>
|
|
61
|
-
</ul>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div class="pull-left header-action-buttons form-inline" data-bind="visible: connected() === true">
|
|
66
|
-
<span class="text-button glyphicon glyphicon-play" data-bind="click: execute"></span>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
<div class="pull-right">
|
|
71
|
-
<div class="header-action-buttons pull-right form-inline" data-bind="visible: connected() === true">
|
|
72
|
-
<span class="text-button glyphicon glyphicon-remove" data-bind="click: disconnect"></span>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
</script>
|
|
76
|
-
|
|
77
|
-
<script type="text/html" id="portfolio-console-template">
|
|
78
|
-
<div data-bind="foreach: console">
|
|
79
|
-
<pre data-bind="text: $data" style="font-size: 10px;"></pre>
|
|
80
|
-
</div>
|
|
81
|
-
</script>
|
|
82
|
-
|
|
83
|
-
<script type="text/html" id="footer-template">
|
|
84
|
-
<h4 class="pull-left" data-bind="visible: connected">
|
|
85
|
-
Client Version: <span data-bind="text: clientVersion"></span>
|
|
86
|
-
</h4>
|
|
87
|
-
<h4 class="pull-right">
|
|
88
|
-
User: <span data-bind="text: user"></span>
|
|
89
|
-
</h4>
|
|
90
|
-
</script>
|
|
91
|
-
</head>
|
|
92
|
-
<body>
|
|
93
|
-
<div class="header" data-bind="template: { name: 'portfolio-header-template' }"></div>
|
|
94
|
-
<div class="main" data-bind="template: { name: activeTemplate }"></div>
|
|
95
|
-
<div class="footer" data-bind="template: { name: 'footer-template' }">
|
|
96
|
-
</div>
|
|
97
|
-
</body>
|
|
98
|
-
</html>
|