Merge pull request #1263 from TorstenDittmann/feat-265-realtime-console
feat(console): add realtime
This commit is contained in:
commit
56e714cf45
|
@ -454,9 +454,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
|||
$server->close($connection, $th->getCode());
|
||||
|
||||
if (App::isDevelopment()) {
|
||||
Console::error("[Error] Connection Error");
|
||||
Console::error("[Error] Code: " . $response['data']['code']);
|
||||
Console::error("[Error] Message: " . $response['data']['message']);
|
||||
Console::error('[Error] Connection Error');
|
||||
Console::error('[Error] Code: ' . $response['data']['code']);
|
||||
Console::error('[Error] Message: ' . $response['data']['message']);
|
||||
}
|
||||
|
||||
if ($th instanceof PDOException) {
|
||||
|
@ -506,6 +506,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
|||
}
|
||||
|
||||
switch ($message['type']) {
|
||||
/**
|
||||
* This type is used to authenticate.
|
||||
*/
|
||||
case 'authentication':
|
||||
if (!array_key_exists('session', $message['data'])) {
|
||||
throw new Exception('Payload is not valid.', 1003);
|
||||
|
|
|
@ -4,7 +4,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
?>
|
||||
|
||||
<div class="cover margin-bottom-small">
|
||||
<div class="zone xl margin-bottom-xl margin-top-small">
|
||||
<div class="zone xxl margin-bottom-xl margin-top-small">
|
||||
<h1 class="margin-bottom-small"
|
||||
data-service="projects.get"
|
||||
data-event="load,project.update,projects.createPlatform,projects.updatePlatform,projects.deletePlatform"
|
||||
|
@ -39,7 +39,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-project-id="{{router.params.project}}"
|
||||
data-param-range="24h">
|
||||
data-param-range="24h"
|
||||
data-scope="console">
|
||||
<button class="tick">24h</button>
|
||||
</form>
|
||||
|
||||
|
@ -53,7 +54,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
data-service="projects.getUsage"
|
||||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-project-id="{{router.params.project}}">
|
||||
data-param-project-id="{{router.params.project}}"
|
||||
data-scope="console">
|
||||
<button class="tick">30d</button>
|
||||
</form>
|
||||
|
||||
|
@ -68,7 +70,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
data-event="submit"
|
||||
data-name="usage"
|
||||
data-param-project-id="{{router.params.project}}"
|
||||
data-param-range="90d">
|
||||
data-param-range="90d"
|
||||
data-scope="console">
|
||||
<button class="tick">90d</button>
|
||||
</form>
|
||||
|
||||
|
@ -83,7 +86,6 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
data-name="usage"
|
||||
data-param-project-id="{{router.params.project}}"
|
||||
data-param-range="30d">
|
||||
|
||||
<?php if (!$graph && $usageStatsEnabled): ?>
|
||||
<div class="box dashboard">
|
||||
<div class="row responsive">
|
||||
|
@ -94,26 +96,20 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
|
||||
<div class="chart-metric">
|
||||
<div class="value margin-bottom-small"><span class="sum" data-ls-bind="{{usage.requests.total|statsTotal}}">N/A</span></div>
|
||||
<div class="metric margin-bottom-small">Requests <span class="tooltip" data-tooltip="Total number of API requests"><i class="icon-info-circled"></i></span></div>
|
||||
<div class="unit margin-start-no margin-bottom-small">Requests</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<div class="value margin-bottom-small">
|
||||
<span class="sum" data-ls-bind="{{usage.network.total|humanFileSize}}" data-default="0">0</span>
|
||||
<span data-ls-bind="{{usage.network.total|humanFileUnit}}" class="text-size-small unit"></span>
|
||||
</div>
|
||||
<div class="metric margin-bottom-small">Bandwidth</div>
|
||||
|
||||
<div class="margin-top-large value small">
|
||||
<b class="text-size-small sum small" data-ls-bind="{{usage.functions.total|statsTotal}}" data-default="0"></b>
|
||||
<br />
|
||||
<b>Func. Executions</b>
|
||||
<span class="sum" data-ls-bind="{{realtime.current|accessProject}}" data-default="0">0</span>
|
||||
</div>
|
||||
<div class="unit margin-start-no margin-bottom-small">Connections</div>
|
||||
<div class="chart-bar margin-top-small margin-bottom-small" data-ls-attrs="data-history={{realtime.history|accessProject}}" data-forms-chart-bars="{{realtime.history|accessProject}}"></div>
|
||||
<div class="text-fade-dark text-size-small">Activity last 60 seconds</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="box dashboard">
|
||||
<div class="row responsive">
|
||||
<div class="col span-3">
|
||||
|
@ -132,8 +128,10 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
|
|||
<div class="margin-top-small"><b class="text-size-small unit">Users</b></div>
|
||||
</div>
|
||||
<div class="col span-3">
|
||||
<div class="value"><span class="sum" data-ls-bind="{{usage.tasks.total}}" data-default="0">0</span></div>
|
||||
<div class="margin-top-small"><b class="text-size-small unit">Tasks</b></div>
|
||||
<div class="value"><span class="sum" data-ls-bind="{{usage.functions.total|statsTotal}}" data-default="0">0</span></div>
|
||||
<div class="margin-top-small"><b class="text-size-small unit">Executions</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
12
composer.lock
generated
12
composer.lock
generated
|
@ -2510,16 +2510,16 @@
|
|||
},
|
||||
{
|
||||
"name": "composer/package-versions-deprecated",
|
||||
"version": "1.11.99.2",
|
||||
"version": "1.11.99.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/package-versions-deprecated.git",
|
||||
"reference": "c6522afe5540d5fc46675043d3ed5a45a740b27c"
|
||||
"reference": "fff576ac850c045158a250e7e27666e146e78d18"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/c6522afe5540d5fc46675043d3ed5a45a740b27c",
|
||||
"reference": "c6522afe5540d5fc46675043d3ed5a45a740b27c",
|
||||
"url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/fff576ac850c045158a250e7e27666e146e78d18",
|
||||
"reference": "fff576ac850c045158a250e7e27666e146e78d18",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2563,7 +2563,7 @@
|
|||
"description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
|
||||
"support": {
|
||||
"issues": "https://github.com/composer/package-versions-deprecated/issues",
|
||||
"source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.2"
|
||||
"source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2579,7 +2579,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2021-05-24T07:46:03+00:00"
|
||||
"time": "2021-08-17T13:49:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
|
|
|
@ -27,6 +27,7 @@ const configApp = {
|
|||
'public/scripts/services/sdk.js',
|
||||
'public/scripts/services/search.js',
|
||||
'public/scripts/services/timezone.js',
|
||||
'public/scripts/services/realtime.js',
|
||||
|
||||
'public/scripts/routes.js',
|
||||
'public/scripts/filters.js',
|
||||
|
@ -41,6 +42,7 @@ const configApp = {
|
|||
'public/scripts/views/forms/clone.js',
|
||||
'public/scripts/views/forms/add.js',
|
||||
'public/scripts/views/forms/chart.js',
|
||||
'public/scripts/views/forms/chart-bar.js',
|
||||
'public/scripts/views/forms/code.js',
|
||||
'public/scripts/views/forms/color.js',
|
||||
'public/scripts/views/forms/copy.js',
|
||||
|
|
30
public/dist/scripts/app-all.js
vendored
30
public/dist/scripts/app-all.js
vendored
File diff suppressed because one or more lines are too long
13
public/dist/scripts/app-dep.js
vendored
13
public/dist/scripts/app-dep.js
vendored
|
@ -5,7 +5,13 @@ function rejected(value){try{step(generator["throw"](value));}catch(e){reject(e)
|
|||
function step(result){result.done?resolve(result.value):adopt(result.value).then(fulfilled,rejected);}
|
||||
step((generator=generator.apply(thisArg,_arguments||[])).next());});}
|
||||
class AppwriteException extends Error{constructor(message,code=0,response=''){super(message);this.name='AppwriteException';this.message=message;this.code=code;this.response=response;}}
|
||||
class Appwrite{constructor(){this.config={endpoint:'https://appwrite.io/v1',project:'',key:'',jwt:'',locale:'',mode:'',};this.headers={'x-sdk-version':'appwrite:web:2.1.0','X-Appwrite-Response-Format':'0.9.0',};this.account={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/account';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(email,password,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
class Appwrite{constructor(){this.config={endpoint:'https://appwrite.io/v1',endpointRealtime:'',project:'',key:'',jwt:'',locale:'',mode:'',};this.headers={'x-sdk-version':'appwrite:web:2.1.0','X-Appwrite-Response-Format':'0.9.0',};this.realtime={socket:undefined,timeout:undefined,channels:{},lastMessage:undefined,createSocket:()=>{var _a,_b;const channels=new URLSearchParams();channels.set('project',this.config.project);for(const property in this.realtime.channels){channels.append('channels[]',property);}
|
||||
if(((_a=this.realtime.socket)===null||_a===void 0?void 0:_a.readyState)===WebSocket.OPEN){this.realtime.socket.close();}
|
||||
this.realtime.socket=new WebSocket(this.config.endpointRealtime+'/realtime?'+channels.toString());(_b=this.realtime.socket)===null||_b===void 0?void 0:_b.addEventListener('message',this.realtime.authenticate);for(const channel in this.realtime.channels){this.realtime.channels[channel].forEach(callback=>{var _a;(_a=this.realtime.socket)===null||_a===void 0?void 0:_a.addEventListener('message',callback);});}
|
||||
this.realtime.socket.addEventListener('close',event=>{var _a,_b,_c;if(((_b=(_a=this.realtime)===null||_a===void 0?void 0:_a.lastMessage)===null||_b===void 0?void 0:_b.type)==='error'&&((_c=this.realtime)===null||_c===void 0?void 0:_c.lastMessage.data).code===1008){return;}
|
||||
console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.',event.reason);setTimeout(()=>{this.realtime.createSocket();},1000);});},authenticate:(event)=>{var _a,_b;const message=JSON.parse(event.data);if(message.type==='connected'){const cookie=JSON.parse((_a=window.localStorage.getItem('cookieFallback'))!==null&&_a!==void 0?_a:"{}");const session=cookie===null||cookie===void 0?void 0:cookie[`a_session_${this.config.project}`];const data=message.data;if(session&&!data.user){(_b=this.realtime.socket)===null||_b===void 0?void 0:_b.send(JSON.stringify({type:"authentication",data:{session}}));}}},onMessage:(channel,callback)=>(event)=>{try{const message=JSON.parse(event.data);this.realtime.lastMessage=message;if(message.type==='event'){let data=message.data;if(data.channels&&data.channels.includes(channel)){callback(data);}}
|
||||
else if(message.type==='error'){throw message.data;}}
|
||||
catch(e){console.error(e);}}};this.account={get:()=>__awaiter(this,void 0,void 0,function*(){let path='/account';let payload={};const uri=new URL(this.config.endpoint+path);return yield this.call('get',uri,{'content-type':'application/json',},payload);}),create:(email,password,name)=>__awaiter(this,void 0,void 0,function*(){if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
|
||||
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
|
||||
let path='/account';let payload={};if(typeof email!=='undefined'){payload['email']=email;}
|
||||
if(typeof password!=='undefined'){payload['password']=password;}
|
||||
|
@ -449,12 +455,15 @@ const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,
|
|||
if(typeof emailVerification==='undefined'){throw new AppwriteException('Missing required parameter: "emailVerification"');}
|
||||
let path='/users/{userId}/verification'.replace('{userId}',userId);let payload={};if(typeof emailVerification!=='undefined'){payload['emailVerification']=emailVerification;}
|
||||
const uri=new URL(this.config.endpoint+path);return yield this.call('patch',uri,{'content-type':'application/json',},payload);})};}
|
||||
setEndpoint(endpoint){this.config.endpoint=endpoint;return this;}
|
||||
setEndpoint(endpoint){this.config.endpoint=endpoint;this.config.endpointRealtime=this.config.endpointRealtime||this.config.endpoint.replace("https://","wss://").replace("http://","ws://");return this;}
|
||||
setEndpointRealtime(endpointRealtime){this.config.endpointRealtime=endpointRealtime;return this;}
|
||||
setProject(value){this.headers['X-Appwrite-Project']=value;this.config.project=value;return this;}
|
||||
setKey(value){this.headers['X-Appwrite-Key']=value;this.config.key=value;return this;}
|
||||
setJWT(value){this.headers['X-Appwrite-JWT']=value;this.config.jwt=value;return this;}
|
||||
setLocale(value){this.headers['X-Appwrite-Locale']=value;this.config.locale=value;return this;}
|
||||
setMode(value){this.headers['X-Appwrite-Mode']=value;this.config.mode=value;return this;}
|
||||
subscribe(channels,callback){let channelArray=typeof channels==='string'?[channels]:channels;let savedChannels=[];channelArray.forEach((channel,index)=>{if(!(channel in this.realtime.channels)){this.realtime.channels[channel]=[];}
|
||||
savedChannels[index]={name:channel,index:(this.realtime.channels[channel].push(this.realtime.onMessage(channel,callback))-1)};clearTimeout(this.realtime.timeout);this.realtime.timeout=window===null||window===void 0?void 0:window.setTimeout(()=>{this.realtime.createSocket();},1);});return()=>{savedChannels.forEach(channel=>{var _a;(_a=this.realtime.socket)===null||_a===void 0?void 0:_a.removeEventListener('message',this.realtime.channels[channel.name][channel.index]);this.realtime.channels[channel.name].splice(channel.index,1);});};}
|
||||
call(method,url,headers={},params={}){var _a,_b;return __awaiter(this,void 0,void 0,function*(){method=method.toUpperCase();headers=Object.assign(Object.assign({},headers),this.headers);let options={method,headers,credentials:'include'};if(typeof window!=='undefined'&&window.localStorage){headers['X-Fallback-Cookies']=(_a=window.localStorage.getItem('cookieFallback'))!==null&&_a!==void 0?_a:"";}
|
||||
if(method==='GET'){for(const[key,value]of Object.entries(this.flatten(params))){url.searchParams.append(key,value);}}
|
||||
else{switch(headers['content-type']){case'application/json':options.body=JSON.stringify(params);break;case'multipart/form-data':let formData=new FormData();for(const key in params){if(Array.isArray(params[key])){formData.append(key+'[]',params[key].join(','));}
|
||||
|
|
17
public/dist/scripts/app.js
vendored
17
public/dist/scripts/app.js
vendored
File diff suppressed because one or more lines are too long
2
public/dist/styles/default-ltr.css
vendored
2
public/dist/styles/default-ltr.css
vendored
File diff suppressed because one or more lines are too long
2
public/dist/styles/default-rtl.css
vendored
2
public/dist/styles/default-rtl.css
vendored
File diff suppressed because one or more lines are too long
|
@ -39,6 +39,7 @@
|
|||
constructor() {
|
||||
this.config = {
|
||||
endpoint: 'https://appwrite.io/v1',
|
||||
endpointRealtime: '',
|
||||
project: '',
|
||||
key: '',
|
||||
jwt: '',
|
||||
|
@ -49,6 +50,76 @@
|
|||
'x-sdk-version': 'appwrite:web:2.1.0',
|
||||
'X-Appwrite-Response-Format': '0.9.0',
|
||||
};
|
||||
this.realtime = {
|
||||
socket: undefined,
|
||||
timeout: undefined,
|
||||
channels: {},
|
||||
lastMessage: undefined,
|
||||
createSocket: () => {
|
||||
var _a, _b;
|
||||
const channels = new URLSearchParams();
|
||||
channels.set('project', this.config.project);
|
||||
for (const property in this.realtime.channels) {
|
||||
channels.append('channels[]', property);
|
||||
}
|
||||
if (((_a = this.realtime.socket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
|
||||
this.realtime.socket.close();
|
||||
}
|
||||
this.realtime.socket = new WebSocket(this.config.endpointRealtime + '/realtime?' + channels.toString());
|
||||
(_b = this.realtime.socket) === null || _b === void 0 ? void 0 : _b.addEventListener('message', this.realtime.authenticate);
|
||||
for (const channel in this.realtime.channels) {
|
||||
this.realtime.channels[channel].forEach(callback => {
|
||||
var _a;
|
||||
(_a = this.realtime.socket) === null || _a === void 0 ? void 0 : _a.addEventListener('message', callback);
|
||||
});
|
||||
}
|
||||
this.realtime.socket.addEventListener('close', event => {
|
||||
var _a, _b, _c;
|
||||
if (((_b = (_a = this.realtime) === null || _a === void 0 ? void 0 : _a.lastMessage) === null || _b === void 0 ? void 0 : _b.type) === 'error' && ((_c = this.realtime) === null || _c === void 0 ? void 0 : _c.lastMessage.data).code === 1008) {
|
||||
return;
|
||||
}
|
||||
console.error('Realtime got disconnected. Reconnect will be attempted in 1 second.', event.reason);
|
||||
setTimeout(() => {
|
||||
this.realtime.createSocket();
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
authenticate: (event) => {
|
||||
var _a, _b;
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.type === 'connected') {
|
||||
const cookie = JSON.parse((_a = window.localStorage.getItem('cookieFallback')) !== null && _a !== void 0 ? _a : "{}");
|
||||
const session = cookie === null || cookie === void 0 ? void 0 : cookie[`a_session_${this.config.project}`];
|
||||
const data = message.data;
|
||||
if (session && !data.user) {
|
||||
(_b = this.realtime.socket) === null || _b === void 0 ? void 0 : _b.send(JSON.stringify({
|
||||
type: "authentication",
|
||||
data: {
|
||||
session
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
onMessage: (channel, callback) => (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
this.realtime.lastMessage = message;
|
||||
if (message.type === 'event') {
|
||||
let data = message.data;
|
||||
if (data.channels && data.channels.includes(channel)) {
|
||||
callback(data);
|
||||
}
|
||||
}
|
||||
else if (message.type === 'error') {
|
||||
throw message.data;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.account = {
|
||||
/**
|
||||
* Get Account
|
||||
|
@ -4117,6 +4188,18 @@
|
|||
*/
|
||||
setEndpoint(endpoint) {
|
||||
this.config.endpoint = endpoint;
|
||||
this.config.endpointRealtime = this.config.endpointRealtime || this.config.endpoint.replace("https://", "wss://").replace("http://", "ws://");
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Set Realtime Endpoint
|
||||
*
|
||||
* @param {string} endpointRealtime
|
||||
*
|
||||
* @returns {this}
|
||||
*/
|
||||
setEndpointRealtime(endpointRealtime) {
|
||||
this.config.endpointRealtime = endpointRealtime;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
|
@ -4185,6 +4268,55 @@
|
|||
this.config.mode = value;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Subscribes to Appwrite events and passes you the payload in realtime.
|
||||
*
|
||||
* @param {string|string[]} channels
|
||||
* Channel to subscribe - pass a single channel as a string or multiple with an array of strings.
|
||||
*
|
||||
* Possible channels are:
|
||||
* - account
|
||||
* - collections
|
||||
* - collections.[ID]
|
||||
* - collections.[ID].documents
|
||||
* - documents
|
||||
* - documents.[ID]
|
||||
* - files
|
||||
* - files.[ID]
|
||||
* - executions
|
||||
* - executions.[ID]
|
||||
* - functions.[ID]
|
||||
* - teams
|
||||
* - teams.[ID]
|
||||
* - memberships
|
||||
* - memberships.[ID]
|
||||
* @param {(payload: RealtimeMessage) => void} callback Is called on every realtime update.
|
||||
* @returns {() => void} Unsubscribes from events.
|
||||
*/
|
||||
subscribe(channels, callback) {
|
||||
let channelArray = typeof channels === 'string' ? [channels] : channels;
|
||||
let savedChannels = [];
|
||||
channelArray.forEach((channel, index) => {
|
||||
if (!(channel in this.realtime.channels)) {
|
||||
this.realtime.channels[channel] = [];
|
||||
}
|
||||
savedChannels[index] = {
|
||||
name: channel,
|
||||
index: (this.realtime.channels[channel].push(this.realtime.onMessage(channel, callback)) - 1)
|
||||
};
|
||||
clearTimeout(this.realtime.timeout);
|
||||
this.realtime.timeout = window === null || window === void 0 ? void 0 : window.setTimeout(() => {
|
||||
this.realtime.createSocket();
|
||||
}, 1);
|
||||
});
|
||||
return () => {
|
||||
savedChannels.forEach(channel => {
|
||||
var _a;
|
||||
(_a = this.realtime.socket) === null || _a === void 0 ? void 0 : _a.removeEventListener('message', this.realtime.channels[channel.name][channel.index]);
|
||||
this.realtime.channels[channel.name].splice(channel.index, 1);
|
||||
});
|
||||
};
|
||||
}
|
||||
call(method, url, headers = {}, params = {}) {
|
||||
var _a, _b;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
|
|
|
@ -255,6 +255,9 @@ window.ls.filter
|
|||
|
||||
return '';
|
||||
})
|
||||
.add("accessProject", function($value, router) {
|
||||
return ($value && $value.hasOwnProperty(router.params.project)) ? $value[router.params.project] : 0;
|
||||
})
|
||||
;
|
||||
|
||||
function abbreviate(number, maxPlaces, forcePlaces, forceLetter) {
|
||||
|
|
|
@ -52,3 +52,74 @@ document.addEventListener("account.create", function () {
|
|||
window.location = '/auth/signup?failure=1';
|
||||
});
|
||||
});
|
||||
window.addEventListener("load", async () => {
|
||||
const bars = 12;
|
||||
const realtime = window.ls.container.get('realtime');
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let current = {};
|
||||
window.ls.container.get('console').subscribe('project', event => {
|
||||
for (let project in event.payload) {
|
||||
current[project] = event.payload[project] ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
while (true) {
|
||||
let newHistory = {};
|
||||
let createdHistory = false;
|
||||
for (const project in current) {
|
||||
let history = realtime?.history ?? {};
|
||||
|
||||
if (!(project in history)) {
|
||||
history[project] = new Array(bars).fill({
|
||||
percentage: 0,
|
||||
value: 0
|
||||
});
|
||||
}
|
||||
|
||||
history = history[project];
|
||||
history.push({
|
||||
percentage: 0,
|
||||
value: current[project]
|
||||
});
|
||||
if (history.length >= bars) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
const highest = history.reduce((prev, curr) => {
|
||||
return (curr.value > prev) ? curr.value : prev;
|
||||
}, 0);
|
||||
|
||||
history = history.map(({ percentage, value }) => {
|
||||
createdHistory = true;
|
||||
percentage = value === 0 ? 0 : ((Math.round((value / highest) * 10) / 10) * 100);
|
||||
if (percentage > 100) percentage = 100;
|
||||
else if (percentage == 0 && value != 0) percentage = 5;
|
||||
|
||||
return {
|
||||
percentage: percentage,
|
||||
value: value
|
||||
};
|
||||
})
|
||||
newHistory[project] = history;
|
||||
}
|
||||
|
||||
let currentSnapshot = { ...current };
|
||||
for (let index = .1; index <= 1; index += .05) {
|
||||
let currentTransition = { ...currentSnapshot };
|
||||
for (const project in current) {
|
||||
if (project in newHistory) {
|
||||
let base = newHistory[project][bars - 2].value;
|
||||
let cur = currentSnapshot[project];
|
||||
let offset = (cur - base) * index;
|
||||
currentTransition[project] = base + Math.floor(offset);
|
||||
}
|
||||
}
|
||||
|
||||
realtime.setCurrent(currentTransition);
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
realtime.setHistory(newHistory);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
20
public/scripts/services/realtime.js
Normal file
20
public/scripts/services/realtime.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
window.ls.container.set('realtime', () => {
|
||||
return {
|
||||
current: null,
|
||||
history: null,
|
||||
setCurrent: function(currentConnections) {
|
||||
var scope = this;
|
||||
scope.current = currentConnections;
|
||||
return scope.current;
|
||||
},
|
||||
setHistory: function(history) {
|
||||
var scope = this;
|
||||
scope.history = history;
|
||||
return scope.history;
|
||||
}
|
||||
};
|
||||
}, true, true);
|
||||
})(window);
|
42
public/scripts/views/forms/chart-bar.js
Normal file
42
public/scripts/views/forms/chart-bar.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
window.ls.container.get("view").add({
|
||||
selector: "data-forms-chart-bars",
|
||||
controller: (element) => {
|
||||
let observer = null;
|
||||
let populateChart = () => {
|
||||
let history = element.dataset?.history;
|
||||
if (history == 0) {
|
||||
history = new Array(12).fill({
|
||||
percentage: 0,
|
||||
value: 0
|
||||
});
|
||||
} else {
|
||||
history = JSON.parse(history);
|
||||
}
|
||||
element.innerHTML = '';
|
||||
history.forEach(({ percentage, value }, index) => {
|
||||
const seconds = 60 - (index * 5);
|
||||
const bar = document.createElement('span');
|
||||
bar.classList.add('bar');
|
||||
bar.classList.add(`bar-${percentage}`);
|
||||
bar.classList.add('tooltip');
|
||||
bar.classList.add('down');
|
||||
bar.setAttribute('data-tooltip', `${value} (${seconds} seconds ago)`);
|
||||
element.appendChild(bar);
|
||||
})
|
||||
}
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
} else {
|
||||
observer = new MutationObserver(populateChart);
|
||||
observer.observe(element, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-history']
|
||||
});
|
||||
}
|
||||
populateChart();
|
||||
}
|
||||
});
|
||||
})(window);
|
|
@ -2,10 +2,10 @@
|
|||
position: relative;
|
||||
background: var(--config-color-background-fade);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.05);
|
||||
padding: 30px;
|
||||
display: block;
|
||||
border-bottom: none;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.05);
|
||||
display: block;
|
||||
padding: 30px;
|
||||
|
||||
&.padding-tiny {
|
||||
padding: 5px;
|
||||
|
|
|
@ -466,7 +466,7 @@ fieldset {
|
|||
white-space: nowrap;
|
||||
background: var(--config-color-tooltip-background);
|
||||
border-radius: 5px;
|
||||
bottom: 26px;
|
||||
bottom: calc(100% + 6px);
|
||||
color: var(--config-color-tooltip-text);
|
||||
content: attr(data-tooltip);
|
||||
padding: 5px 15px;
|
||||
|
@ -483,23 +483,24 @@ fieldset {
|
|||
border: solid;
|
||||
border-color: var(--config-color-tooltip-background) transparent;
|
||||
border-width: 6px 6px 0 6px;
|
||||
bottom: 20px;
|
||||
bottom: 100%;
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
.func-start(5px);
|
||||
.func-start(3px);
|
||||
}
|
||||
|
||||
&.down:hover:after {
|
||||
top: 26px;
|
||||
&.down:hover {
|
||||
&:after {
|
||||
top: calc(100% + 6px);
|
||||
bottom: inherit;
|
||||
}
|
||||
|
||||
&.down:hover:before {
|
||||
top: 20px;
|
||||
&:before {
|
||||
top: 100%;
|
||||
border-width: 0 6px 6px 6px;
|
||||
bottom: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
|
|
|
@ -408,7 +408,7 @@
|
|||
|
||||
.dashboard {
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: 2px;
|
||||
|
@ -522,6 +522,44 @@
|
|||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
height: 4rem;
|
||||
width: auto;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
@media @desktops {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 12.5%;
|
||||
background-color: var(--config-color-chart-fade);
|
||||
margin: 0 2px;
|
||||
border-top: 2px solid var(--config-color-chart);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--config-color-chart);
|
||||
}
|
||||
|
||||
.bar-loop (@i) when (@i > -1) {
|
||||
&.bar-@{i} {
|
||||
height: ~"@{i}%";
|
||||
}
|
||||
&.bar-@{i} when(@i = 0) {
|
||||
border-top: 1px solid var(--config-color-chart);
|
||||
}
|
||||
.bar-loop(@i - 10);
|
||||
}
|
||||
|
||||
.bar-loop (100);
|
||||
|
||||
&.bar-5 {
|
||||
height: 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-metric {
|
||||
|
@ -587,10 +625,10 @@
|
|||
}
|
||||
|
||||
&:nth-child(1), &.blue {
|
||||
color: #29b5d9;
|
||||
color: var(--config-color-chart);
|
||||
|
||||
&::before {
|
||||
background: #29b5d9;
|
||||
background: var(--config-color-chart);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,12 +23,15 @@
|
|||
--config-color-normal: #40404c;
|
||||
--config-color-dark: #313131;
|
||||
--config-color-fade: #8f8f8f;
|
||||
--config-color-fade-dark: #6e6e6e;
|
||||
--config-color-fade-light: #e2e2e2;
|
||||
--config-color-fade-super: #f1f3f5;
|
||||
--config-color-danger: #f53d3d;
|
||||
--config-color-success: #1bbf61;
|
||||
--config-color-warning: #fffbdd;
|
||||
--config-color-info: #386fd2;
|
||||
--config-color-chart: #29b5d9;
|
||||
--config-color-chart-fade: #d4f0f7;
|
||||
--config-border-color: #f3f3f3;
|
||||
--config-border-fade: #e0e3e4;
|
||||
--config-border-radius: 10px;
|
||||
|
@ -105,12 +108,15 @@
|
|||
--config-color-normal: #c7d8eb;
|
||||
--config-color-dark: #c7d8eb;
|
||||
--config-color-fade: #bec3e0;
|
||||
--config-color-fade-dark: #81859b;
|
||||
--config-color-fade-light: #181818;
|
||||
--config-color-fade-super: #262D50;
|
||||
--config-color-danger: #d84a4a;
|
||||
--config-color-success: #34b86d;
|
||||
--config-color-warning: #e0d56d;
|
||||
--config-color-info: #386fd2;
|
||||
--config-color-chart: #29b5d9;
|
||||
--config-color-chart-fade: #c7d8eb;
|
||||
--config-border-color: #262D50;
|
||||
--config-border-fade: #19203a;
|
||||
--config-prism-background: #1F253F;
|
||||
|
|
|
@ -95,6 +95,10 @@ small {
|
|||
font-size: 13px;
|
||||
}
|
||||
|
||||
.text-size-smaller {
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.text-size-xs {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
@ -154,6 +158,10 @@ small {
|
|||
color: var(--config-color-fade);
|
||||
}
|
||||
|
||||
.text-fade-dark {
|
||||
color: var(--config-color-fade-dark);
|
||||
}
|
||||
|
||||
&.text-green {
|
||||
color: var(--config-color-success);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue