1
0
Fork 0
mirror of synced 2024-06-26 18:20:43 +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) { } catch (JWTException $error) {
throw new Exception('Failed to verify JWT. '.$error->getMessage(), 401); throw new Exception('Failed to verify JWT. '.$error->getMessage(), 401);
} }
$jwtUserId = $payload['userId'] ?? ''; $jwtUserId = $payload['userId'] ?? '';
$jwtSessionId = $payload['sessionId'] ?? ''; $jwtSessionId = $payload['sessionId'] ?? '';

View file

@ -454,9 +454,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$server->close($connection, $th->getCode()); $server->close($connection, $th->getCode());
if (App::isDevelopment()) { if (App::isDevelopment()) {
Console::error("[Error] Connection Error"); Console::error('[Error] Connection Error');
Console::error("[Error] Code: " . $response['data']['code']); Console::error('[Error] Code: ' . $response['data']['code']);
Console::error("[Error] Message: " . $response['data']['message']); Console::error('[Error] Message: ' . $response['data']['message']);
} }
if ($th instanceof PDOException) { if ($th instanceof PDOException) {
@ -506,6 +506,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
} }
switch ($message['type']) { switch ($message['type']) {
/**
* This type is used to authenticate.
*/
case 'authentication': case 'authentication':
if (!array_key_exists('session', $message['data'])) { if (!array_key_exists('session', $message['data'])) {
throw new Exception('Payload is not valid.', 1003); 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-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-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="{}" /> <input type="hidden" id="collection-rules" name="rules" required data-cast-to="json" value="{}" />
<hr /> <hr />
<button type="submit">Create</button> &nbsp; <button data-ui-modal-close="" type="button" class="reverse">Cancel</button> <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="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" <h1 class="margin-bottom-small"
data-service="projects.get" data-service="projects.get"
data-event="load,project.update,projects.createPlatform,projects.updatePlatform,projects.deletePlatform" data-event="load,project.update,projects.createPlatform,projects.updatePlatform,projects.deletePlatform"
@ -39,7 +39,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
data-event="submit" data-event="submit"
data-name="usage" data-name="usage"
data-param-project-id="{{router.params.project}}" data-param-project-id="{{router.params.project}}"
data-param-range="24h"> data-param-range="24h"
data-scope="console">
<button class="tick">24h</button> <button class="tick">24h</button>
</form> </form>
@ -53,7 +54,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
data-service="projects.getUsage" data-service="projects.getUsage"
data-event="submit" data-event="submit"
data-name="usage" 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> <button class="tick">30d</button>
</form> </form>
@ -68,7 +70,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
data-event="submit" data-event="submit"
data-name="usage" data-name="usage"
data-param-project-id="{{router.params.project}}" data-param-project-id="{{router.params.project}}"
data-param-range="90d"> data-param-range="90d"
data-scope="console">
<button class="tick">90d</button> <button class="tick">90d</button>
</form> </form>
@ -83,8 +86,7 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
data-name="usage" data-name="usage"
data-param-project-id="{{router.params.project}}" data-param-project-id="{{router.params.project}}"
data-param-range="30d"> data-param-range="30d">
<?php if (!$graph && $usageStatsEnabled): ?>
<?php if (!$graph && $usageStatsEnabled): ?>
<div class="box dashboard"> <div class="box dashboard">
<div class="row responsive"> <div class="row responsive">
<div class="col span-9"> <div class="col span-9">
@ -94,50 +96,46 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled',true);
<div class="chart-metric"> <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="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> </div>
<div class="col span-3"> <div class="col span-3">
<div class="value margin-bottom-small"> <div class="value margin-bottom-small">
<span class="sum" data-ls-bind="{{usage.network.total|humanFileSize}}" data-default="0">0</span> <span class="sum" data-ls-bind="{{realtime.current|accessProject}}" 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>
</div> </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> </div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div class="box dashboard">
<div class="box dashboard"> <div class="row responsive">
<div class="row responsive"> <div class="col span-3">
<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="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 class="margin-top-small"><b class="text-size-small unit">Documents</b></div> </div>
</div> <div class="col span-3">
<div class="col span-3"> <div class="value">
<div class="value"> <span class="sum" data-ls-bind="{{usage.storage.total|humanFileSize}}" data-default="0">0</span>
<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>
<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>
<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> </div>
</div>
</div> </div>
<div class="zone xl margin-top-xl clear" data-ls-if="({{console-project}})"> <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", "name": "composer/package-versions-deprecated",
"version": "1.11.99.2", "version": "1.11.99.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/package-versions-deprecated.git", "url": "https://github.com/composer/package-versions-deprecated.git",
"reference": "c6522afe5540d5fc46675043d3ed5a45a740b27c" "reference": "fff576ac850c045158a250e7e27666e146e78d18"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/c6522afe5540d5fc46675043d3ed5a45a740b27c", "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/fff576ac850c045158a250e7e27666e146e78d18",
"reference": "c6522afe5540d5fc46675043d3ed5a45a740b27c", "reference": "fff576ac850c045158a250e7e27666e146e78d18",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2563,7 +2563,7 @@
"description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
"support": { "support": {
"issues": "https://github.com/composer/package-versions-deprecated/issues", "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": [ "funding": [
{ {
@ -2579,7 +2579,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2021-05-24T07:46:03+00:00" "time": "2021-08-17T13:49:14+00:00"
}, },
{ {
"name": "composer/semver", "name": "composer/semver",

View file

@ -27,6 +27,7 @@ const configApp = {
'public/scripts/services/sdk.js', 'public/scripts/services/sdk.js',
'public/scripts/services/search.js', 'public/scripts/services/search.js',
'public/scripts/services/timezone.js', 'public/scripts/services/timezone.js',
'public/scripts/services/realtime.js',
'public/scripts/routes.js', 'public/scripts/routes.js',
'public/scripts/filters.js', 'public/scripts/filters.js',
@ -41,6 +42,7 @@ const configApp = {
'public/scripts/views/forms/clone.js', 'public/scripts/views/forms/clone.js',
'public/scripts/views/forms/add.js', 'public/scripts/views/forms/add.js',
'public/scripts/views/forms/chart.js', 'public/scripts/views/forms/chart.js',
'public/scripts/views/forms/chart-bar.js',
'public/scripts/views/forms/code.js', 'public/scripts/views/forms/code.js',
'public/scripts/views/forms/color.js', 'public/scripts/views/forms/color.js',
'public/scripts/views/forms/copy.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);} function step(result){result.done?resolve(result.value):adopt(result.value).then(fulfilled,rejected);}
step((generator=generator.apply(thisArg,_arguments||[])).next());});} 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 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"');} if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/account';let payload={};if(typeof email!=='undefined'){payload['email']=email;} let path='/account';let payload={};if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;} 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"');} 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;} 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);})};} 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;} 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;} 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;} 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;} 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;} 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:"";} 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);}} 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(','));} 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() { constructor() {
this.config = { this.config = {
endpoint: 'https://appwrite.io/v1', endpoint: 'https://appwrite.io/v1',
endpointRealtime: '',
project: '', project: '',
key: '', key: '',
jwt: '', jwt: '',
@ -49,6 +50,76 @@
'x-sdk-version': 'appwrite:web:2.1.0', 'x-sdk-version': 'appwrite:web:2.1.0',
'X-Appwrite-Response-Format': '0.9.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 = { this.account = {
/** /**
* Get Account * Get Account
@ -4117,6 +4188,18 @@
*/ */
setEndpoint(endpoint) { setEndpoint(endpoint) {
this.config.endpoint = 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; return this;
} }
/** /**
@ -4185,6 +4268,55 @@
this.config.mode = value; this.config.mode = value;
return this; 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 = {}) { call(method, url, headers = {}, params = {}) {
var _a, _b; var _a, _b;
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {

View file

@ -255,6 +255,9 @@ window.ls.filter
return ''; return '';
}) })
.add("accessProject", function($value, router) {
return ($value && $value.hasOwnProperty(router.params.project)) ? $value[router.params.project] : 0;
})
; ;
function abbreviate(number, maxPlaces, forcePlaces, forceLetter) { 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); let promise = sdk.account.createSession(form.email, form.password);
container.set("serviceForm", {}, true, true); // Remove sensetive data when not needed container.set("serviceForm", {}, true, true); // Remove sensetive data when not needed
promise.then(function () { promise.then(function () {
var subscribe = document.getElementById('newsletter').checked; var subscribe = document.getElementById('newsletter').checked;
if (subscribe) { if (subscribe) {
@ -51,4 +51,75 @@ document.addEventListener("account.create", function () {
}, function (error) { }, function (error) {
window.location = '/auth/signup?failure=1'; 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; position: relative;
background: var(--config-color-background-fade); background: var(--config-color-background-fade);
border-radius: 10px; border-radius: 10px;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.05);
padding: 30px;
display: block;
border-bottom: none; border-bottom: none;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.05);
display: block;
padding: 30px;
&.padding-tiny { &.padding-tiny {
padding: 5px; padding: 5px;

View file

@ -466,7 +466,7 @@ fieldset {
white-space: nowrap; white-space: nowrap;
background: var(--config-color-tooltip-background); background: var(--config-color-tooltip-background);
border-radius: 5px; border-radius: 5px;
bottom: 26px; bottom: calc(100% + 6px);
color: var(--config-color-tooltip-text); color: var(--config-color-tooltip-text);
content: attr(data-tooltip); content: attr(data-tooltip);
padding: 5px 15px; padding: 5px 15px;
@ -483,22 +483,23 @@ fieldset {
border: solid; border: solid;
border-color: var(--config-color-tooltip-background) transparent; border-color: var(--config-color-tooltip-background) transparent;
border-width: 6px 6px 0 6px; border-width: 6px 6px 0 6px;
bottom: 20px; bottom: 100%;
content: ""; content: "";
position: absolute; position: absolute;
z-index: 99; z-index: 99;
.func-start(5px); .func-start(3px);
} }
&.down:hover:after { &.down:hover {
top: 26px; &:after {
bottom: inherit; top: calc(100% + 6px);
} bottom: inherit;
}
&.down:hover:before { &:before {
top: 20px; top: 100%;
border-width: 0 6px 6px 6px; border-width: 0 6px 6px 6px;
bottom: inherit; bottom: inherit;
}
} }
} }

View file

@ -408,7 +408,7 @@
.dashboard { .dashboard {
padding: 20px; padding: 20px;
overflow: hidden; overflow: visible;
position: relative; position: relative;
z-index: 1; z-index: 1;
margin-bottom: 2px; margin-bottom: 2px;
@ -522,6 +522,44 @@
padding: 0; padding: 0;
border: none; 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 { .chart-metric {
@ -587,10 +625,10 @@
} }
&:nth-child(1), &.blue { &:nth-child(1), &.blue {
color: #29b5d9; color: var(--config-color-chart);
&::before { &::before {
background: #29b5d9; background: var(--config-color-chart);
} }
} }

View file

@ -23,12 +23,15 @@
--config-color-normal: #40404c; --config-color-normal: #40404c;
--config-color-dark: #313131; --config-color-dark: #313131;
--config-color-fade: #8f8f8f; --config-color-fade: #8f8f8f;
--config-color-fade-dark: #6e6e6e;
--config-color-fade-light: #e2e2e2; --config-color-fade-light: #e2e2e2;
--config-color-fade-super: #f1f3f5; --config-color-fade-super: #f1f3f5;
--config-color-danger: #f53d3d; --config-color-danger: #f53d3d;
--config-color-success: #1bbf61; --config-color-success: #1bbf61;
--config-color-warning: #fffbdd; --config-color-warning: #fffbdd;
--config-color-info: #386fd2; --config-color-info: #386fd2;
--config-color-chart: #29b5d9;
--config-color-chart-fade: #d4f0f7;
--config-border-color: #f3f3f3; --config-border-color: #f3f3f3;
--config-border-fade: #e0e3e4; --config-border-fade: #e0e3e4;
--config-border-radius: 10px; --config-border-radius: 10px;
@ -105,12 +108,15 @@
--config-color-normal: #c7d8eb; --config-color-normal: #c7d8eb;
--config-color-dark: #c7d8eb; --config-color-dark: #c7d8eb;
--config-color-fade: #bec3e0; --config-color-fade: #bec3e0;
--config-color-fade-dark: #81859b;
--config-color-fade-light: #181818; --config-color-fade-light: #181818;
--config-color-fade-super: #262D50; --config-color-fade-super: #262D50;
--config-color-danger: #d84a4a; --config-color-danger: #d84a4a;
--config-color-success: #34b86d; --config-color-success: #34b86d;
--config-color-warning: #e0d56d; --config-color-warning: #e0d56d;
--config-color-info: #386fd2; --config-color-info: #386fd2;
--config-color-chart: #29b5d9;
--config-color-chart-fade: #c7d8eb;
--config-border-color: #262D50; --config-border-color: #262D50;
--config-border-fade: #19203a; --config-border-fade: #19203a;
--config-prism-background: #1F253F; --config-prism-background: #1F253F;

View file

@ -95,6 +95,10 @@ small {
font-size: 13px; font-size: 13px;
} }
.text-size-smaller {
font-size: 11.5px;
}
.text-size-xs { .text-size-xs {
font-size: 10px; font-size: 10px;
} }
@ -154,6 +158,10 @@ small {
color: var(--config-color-fade); color: var(--config-color-fade);
} }
.text-fade-dark {
color: var(--config-color-fade-dark);
}
&.text-green { &.text-green {
color: var(--config-color-success); color: var(--config-color-success);
} }