1
0
Fork 0
mirror of synced 2024-06-17 10:14:50 +12:00

Merge pull request #1263 from TorstenDittmann/feat-265-realtime-console

feat(console): add realtime
This commit is contained in:
Torsten Dittmann 2021-08-31 17:36:47 +02:00 committed by GitHub
commit 56e714cf45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 445 additions and 85 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -468,7 +468,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response
} catch (JWTException $error) {
throw new Exception('Failed to verify JWT. '.$error->getMessage(), 401);
}
$jwtUserId = $payload['userId'] ?? '';
$jwtSessionId = $payload['sessionId'] ?? '';

View file

@ -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);

View file

@ -100,7 +100,7 @@
<input type="hidden" id="collection-read" name="read" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-write" name="write" required data-cast-to="json" value="<?php echo htmlentities(json_encode([])); ?>" />
<input type="hidden" id="collection-rules" name="rules" required data-cast-to="json" value="{}" />
<hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button>

View file

@ -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,8 +86,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
data-name="usage"
data-param-project-id="{{router.params.project}}"
data-param-range="30d">
<?php if (!$graph && $usageStatsEnabled): ?>
<?php if (!$graph && $usageStatsEnabled): ?>
<div class="box dashboard">
<div class="row responsive">
<div class="col span-9">
@ -94,50 +96,46 @@ $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">
<div class="value"><span class="sum" data-ls-bind="{{usage.documents.total|statsTotal}}" data-default="0">0</span></div>
<div class="margin-top-small"><b class="text-size-small unit">Documents</b></div>
</div>
<div class="col span-3">
<div class="value">
<span class="sum" data-ls-bind="{{usage.storage.total|humanFileSize}}" data-default="0">0</span>
<span data-ls-bind="{{usage.storage.total|humanFileUnit}}" class="text-size-small unit"></span>
<?php endif; ?>
<div class="box dashboard">
<div class="row responsive">
<div class="col span-3">
<div class="value"><span class="sum" data-ls-bind="{{usage.documents.total|statsTotal}}" data-default="0">0</span></div>
<div class="margin-top-small"><b class="text-size-small unit">Documents</b></div>
</div>
<div class="col span-3">
<div class="value">
<span class="sum" data-ls-bind="{{usage.storage.total|humanFileSize}}" data-default="0">0</span>
<span data-ls-bind="{{usage.storage.total|humanFileUnit}}" class="text-size-small unit"></span>
</div>
<div class="margin-top-small"><b class="text-size-small unit">Storage</b></div>
</div>
<div class="col span-3">
<div class="value"><span class="sum" data-ls-bind="{{usage.users.total}}" data-default="0">0</span></div>
<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.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 class="margin-top-small"><b class="text-size-small unit">Storage</b></div>
</div>
<div class="col span-3">
<div class="value"><span class="sum" data-ls-bind="{{usage.users.total}}" data-default="0">0</span></div>
<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>
</div>
</div>
</div>
</div>
</div>
<div class="zone xl margin-top-xl clear" data-ls-if="({{console-project}})">

12
composer.lock generated
View file

@ -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",

View file

@ -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',

File diff suppressed because one or more lines are too long

View file

@ -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(','));}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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* () {

View file

@ -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) {

View file

@ -26,7 +26,7 @@ document.addEventListener("account.create", function () {
let promise = sdk.account.createSession(form.email, form.password);
container.set("serviceForm", {}, true, true); // Remove sensetive data when not needed
promise.then(function () {
var subscribe = document.getElementById('newsletter').checked;
if (subscribe) {
@ -51,4 +51,75 @@ document.addEventListener("account.create", function () {
}, function (error) {
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);
}
});

View 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);

View 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);

View file

@ -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;

View file

@ -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,22 +483,23 @@ 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;
bottom: inherit;
}
&.down:hover:before {
top: 20px;
border-width: 0 6px 6px 6px;
bottom: inherit;
&.down:hover {
&:after {
top: calc(100% + 6px);
bottom: inherit;
}
&:before {
top: 100%;
border-width: 0 6px 6px 6px;
bottom: inherit;
}
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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);
}