Skip to content

Commit 8cf5c34

Browse files
author
Dennis Labordus
committed
Implemented Session Timeout Panels.
Signed-off-by: Dennis Labordus <[email protected]>
1 parent 6817056 commit 8cf5c34

File tree

10 files changed

+300
-54
lines changed

10 files changed

+300
-54
lines changed

public/js/plugins.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export const officialPlugins = [
1212
icon: 'settings_ethernet',
1313
default: true,
1414
kind: 'editor',
15-
1615
},
1716
{
1817
name: 'Templates',

src/compas-services/CompasUserInfoService.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ export function CompasUserInfoService() {
1414
.catch(handleError)
1515
.then(handleResponse)
1616
.then(parseXml);
17+
},
18+
19+
ping(): Promise<string> {
20+
const pingUrl = getCompasSettings().sclDataServiceUrl + '/q/health/ready';
21+
return fetch(pingUrl)
22+
.catch(handleError)
23+
.then(handleResponse);
1724
}
1825
}
1926
}

src/compas/CompasSession.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import {css, customElement, html, LitElement, property, TemplateResult} from "lit-element";
2+
import {translate, translateUnsafeHTML} from "lit-translate";
3+
import {Dialog} from "@material/mwc-dialog";
4+
5+
import {saveDocumentToFile} from "../file.js";
6+
import {getOpenScdElement} from "./foundation.js";
7+
import {CompasUserInfoService} from "../compas-services/CompasUserInfoService";
8+
9+
@customElement('compas-session-expiring-dialog')
10+
export class CompasSessionExpiringDialogElement extends LitElement {
11+
@property({ type: Number })
12+
expiringSessionWarning: number = 10 * 60 * 1000;
13+
@property({ type: Number })
14+
expiredSessionMessage: number = 15 * 60 * 1000;
15+
16+
private expiringSessionWarningTimer: NodeJS.Timeout | null = null;
17+
18+
static getElement(): CompasSessionExpiringDialogElement {
19+
return (<CompasSessionExpiringDialogElement>getOpenScdElement()
20+
.shadowRoot!.querySelector('compas-session-expiring-dialog'));
21+
}
22+
23+
resetTimer(): void {
24+
if (this.expiringSessionWarningTimer) {
25+
clearTimeout(this.expiringSessionWarningTimer);
26+
}
27+
this.expiringSessionWarningTimer = setTimeout(showExpiringSessionWarning, this.expiringSessionWarning);
28+
}
29+
30+
private getDialog(): Dialog {
31+
return <Dialog>this.shadowRoot!.querySelector('mwc-dialog[id="compasSessionExpiringDialog"]');
32+
}
33+
34+
show(): void {
35+
const expiringDialog = this.getDialog();
36+
if (expiringDialog && !expiringDialog.open) {
37+
expiringDialog.show();
38+
}
39+
}
40+
41+
close(): void {
42+
const expiringDialog = this.getDialog();
43+
if (expiringDialog && expiringDialog.open) {
44+
expiringDialog.close();
45+
}
46+
}
47+
48+
render(): TemplateResult {
49+
return html`
50+
<mwc-dialog id="compasSessionExpiringDialog"
51+
heading="${translate('compas.session.headingExpiring')}"
52+
scrimClickAction="">
53+
<div>${translateUnsafeHTML('compas.session.explainExpiring',
54+
{timeTillExpire: ((this.expiredSessionMessage - this.expiringSessionWarning) / 60 / 1000),
55+
expiringSessionWarning: (this.expiringSessionWarning / 60 / 1000)})}</div>
56+
<mwc-button slot="primaryAction" dialogAction="close">
57+
${translate('compas.session.continue')}
58+
</mwc-button>
59+
</mwc-dialog>
60+
`;
61+
}
62+
63+
static styles = css`
64+
#compasSessionExpiringDialog {
65+
--mdc-dialog-max-width: 800px;
66+
}
67+
`
68+
}
69+
70+
@customElement('compas-session-expired-dialog')
71+
export class CompasSessionExpiredDialogElement extends LitElement {
72+
@property({ type: Document })
73+
doc: XMLDocument | null = null;
74+
@property({ type: String })
75+
docName = '';
76+
@property({ type: Number })
77+
expiredSessionMessage: number = 15 * 60 * 1000;
78+
79+
private expiredSessionMessageTimer: NodeJS.Timeout | null = null;
80+
81+
static getElement(): CompasSessionExpiredDialogElement {
82+
return (<CompasSessionExpiredDialogElement>getOpenScdElement()
83+
.shadowRoot!.querySelector('compas-session-expired-dialog'));
84+
}
85+
86+
resetTimer(): void {
87+
if (this.expiredSessionMessageTimer) {
88+
clearTimeout(this.expiredSessionMessageTimer);
89+
}
90+
this.expiredSessionMessageTimer = setTimeout(showExpiredSessionMessage, this.expiredSessionMessage);
91+
}
92+
93+
private getDialog(): Dialog {
94+
return <Dialog>this.shadowRoot!.querySelector('mwc-dialog[id="compasSessionExpiredDialog"]');
95+
}
96+
97+
show(): void {
98+
const expiringDialog = this.getDialog();
99+
if (expiringDialog && !expiringDialog.open) {
100+
expiringDialog.show();
101+
}
102+
}
103+
104+
close(): void {
105+
const expiringDialog = this.getDialog();
106+
if (expiringDialog && expiringDialog.open) {
107+
expiringDialog.close();
108+
}
109+
}
110+
111+
save(): void {
112+
saveDocumentToFile(this.doc, this.docName);
113+
}
114+
115+
render(): TemplateResult {
116+
const expiredSessionMessage = (this.expiredSessionMessage / 60 / 1000);
117+
return html`
118+
<mwc-dialog id="compasSessionExpiredDialog"
119+
heading="${translate('compas.session.headingExpired')}"
120+
scrimClickAction=""
121+
escapeKeyAction=""">
122+
<div>${(this.doc == null) ?
123+
translateUnsafeHTML('compas.session.explainExpiredWithoutProject',
124+
{expiredSessionMessage: expiredSessionMessage}) :
125+
translateUnsafeHTML('compas.session.explainExpiredWithProject',
126+
{expiredSessionMessage: expiredSessionMessage}) }
127+
</div>
128+
${(this.doc != null) ?
129+
html `<mwc-button slot="primaryAction"
130+
@click=${() => this.save()}
131+
?disabled=${this.doc == null}>
132+
${translate('compas.session.saveProject')}
133+
</mwc-button>` :
134+
html `` }
135+
</mwc-dialog>
136+
`;
137+
}
138+
139+
static styles = css`
140+
#compasSessionExpiredDialog {
141+
--mdc-dialog-max-width: 1024px;
142+
}
143+
`
144+
}
145+
146+
export function renderCompasSessionDialogs(doc: Document | null, docName: string): TemplateResult {
147+
return html `
148+
<compas-session-expiring-dialog></compas-session-expiring-dialog>
149+
<compas-session-expired-dialog .doc="${doc}" .docName="${docName}"></compas-session-expired-dialog>
150+
`;
151+
}
152+
153+
let pingTimer: NodeJS.Timeout | null = null;
154+
let pingScheduled = false;
155+
156+
async function executeKeepAlivePing() {
157+
await CompasUserInfoService().ping()
158+
.finally(() => pingScheduled = false)
159+
}
160+
161+
function schedulePing() {
162+
if (!pingTimer || !pingScheduled) {
163+
// Every minute we will send a Ping to the CoMPAS Services while the user is still active.
164+
// This to keep the connection alive so long the user is working.
165+
pingTimer = setTimeout(executeKeepAlivePing, (60 * 1000));
166+
pingScheduled = true;
167+
}
168+
}
169+
170+
function showExpiringSessionWarning() {
171+
CompasSessionExpiringDialogElement.getElement().show();
172+
}
173+
174+
function showExpiredSessionMessage() {
175+
CompasSessionExpiringDialogElement.getElement().close();
176+
CompasSessionExpiredDialogElement.getElement().show();
177+
unregisterEvents();
178+
}
179+
180+
function resetTimer() {
181+
CompasSessionExpiringDialogElement.getElement().resetTimer();
182+
CompasSessionExpiredDialogElement.getElement().resetTimer();
183+
schedulePing();
184+
}
185+
186+
function unregisterEvents() {
187+
window.removeEventListener('click', resetTimer);
188+
window.removeEventListener('keydown', resetTimer);
189+
}
190+
191+
function registerEvents() {
192+
window.addEventListener('click', resetTimer);
193+
window.addEventListener('keydown', resetTimer);
194+
resetTimer();
195+
}
196+
197+
export function setSessionTimeouts(sessionWarning: number, sessionExpires: number): void {
198+
const expiringSessionWarning = sessionWarning * 60 * 1000;
199+
const expiredSessionMessage = sessionExpires * 60 * 1000;
200+
201+
CompasSessionExpiringDialogElement.getElement().expiringSessionWarning = expiringSessionWarning;
202+
CompasSessionExpiringDialogElement.getElement().expiredSessionMessage = expiredSessionMessage;
203+
CompasSessionExpiredDialogElement.getElement().expiredSessionMessage = expiredSessionMessage;
204+
registerEvents();
205+
}

src/compas/CompasUploadVersion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {get, translate} from "lit-translate";
44
import {newLogEvent, newPendingStateEvent, newWizardEvent, Wizard, WizardInput} from "../foundation.js";
55

66
import {CompasExistsIn} from "./CompasExistsIn.js";
7-
import {ChangeSet, CompasSclDataService} from "../compas-services/CompasSclDataService.js";
7+
import {CompasSclDataService} from "../compas-services/CompasSclDataService.js";
88
import {createLogEvent} from "../compas-services/foundation.js";
99
import {getOpenScdElement, getTypeFromDocName, reloadSclDocument} from "./foundation.js";
1010
import {CompasChangeSetRadiogroup} from "./CompasChangeSetRadiogroup.js";

src/compas/foundation.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {OpenSCD} from "../open-scd.js";
66
import {CompasSclDataService} from "../compas-services/CompasSclDataService.js";
77
import {CompasUserInfoService} from "../compas-services/CompasUserInfoService.js";
88
import {createLogEvent} from "../compas-services/foundation.js";
9+
import {setSessionTimeouts} from "./CompasSession.js";
910

1011
const FILE_EXTENSION_LENGTH = 3;
1112

@@ -43,7 +44,6 @@ export async function reloadSclDocument(type: string, id: string): Promise<void>
4344
.catch(createLogEvent);
4445
}
4546

46-
4747
export function updateDocumentInOpenSCD(doc: Document): void {
4848
const id = (doc.querySelectorAll(':root > Header') ?? []).item(0).getAttribute('id') ?? '';
4949
const version = (doc.querySelectorAll(':root > Header') ?? []).item(0).getAttribute('version') ?? '';
@@ -59,13 +59,21 @@ export function updateDocumentInOpenSCD(doc: Document): void {
5959
getOpenScdElement().dispatchEvent(newOpenDocEvent(doc, docName, {detail: {docId: id}}));
6060
}
6161

62-
export async function showOptionalUserInfo(): Promise<void> {
62+
export async function retrieveUserInfo(): Promise<void> {
6363
await CompasUserInfoService().getCompasUserInfo()
6464
.then(response => {
6565
const name = response.querySelectorAll("Name").item(0)?.textContent;
66-
if (name != null)
66+
if (name != null) {
6767
getOpenScdElement().dispatchEvent(newUserInfoEvent(name));
68+
}
69+
70+
const sessionWarning = response.querySelectorAll("SessionWarning").item(0)?.textContent??"15";
71+
const sessionExpires = response.querySelectorAll("SessionExpires").item(0)?.textContent??"10";
72+
setSessionTimeouts(parseInt(sessionWarning), parseInt(sessionExpires));
6873
})
69-
.catch(createLogEvent);
74+
.catch(reason => {
75+
createLogEvent(reason);
76+
setSessionTimeouts(10, 15);
77+
});
7078
}
71-
showOptionalUserInfo();
79+
retrieveUserInfo();

src/file.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
function formatXml(xml: string, tab?: string): string {
2+
let formatted = '',
3+
indent = '';
4+
5+
if (!tab) tab = '\t';
6+
xml.split(/>\s*</).forEach(function (node) {
7+
if (node.match(/^\/\w/)) indent = indent.substring(tab!.length);
8+
formatted += indent + '<' + node + '>\r\n';
9+
if (node.match(/^<?\w[^>]*[^/]$/)) indent += tab;
10+
});
11+
return formatted.substring(1, formatted.length - 3);
12+
}
13+
14+
export function saveDocumentToFile(doc: Document | null, docName: string) {
15+
if (doc) {
16+
const blob = new Blob(
17+
[formatXml(new XMLSerializer().serializeToString(doc))],
18+
{
19+
type: 'application/xml',
20+
}
21+
);
22+
23+
const a = document.createElement('a');
24+
a.download = docName;
25+
a.href = URL.createObjectURL(blob);
26+
a.dataset.downloadurl = ['application/xml', a.download, a.href].join(':');
27+
a.style.display = 'none';
28+
document.body.appendChild(a);
29+
a.click();
30+
document.body.removeChild(a);
31+
setTimeout(function () {
32+
URL.revokeObjectURL(a.href);
33+
}, 5000);
34+
}
35+
}

src/menu/SaveProject.ts

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,12 @@
11
import { LitElement, property } from 'lit-element';
22

3-
function formatXml(xml: string, tab?: string) {
4-
let formatted = '',
5-
indent = '';
6-
7-
if (!tab) tab = '\t';
8-
xml.split(/>\s*</).forEach(function (node) {
9-
if (node.match(/^\/\w/)) indent = indent.substring(tab!.length);
10-
formatted += indent + '<' + node + '>\r\n';
11-
if (node.match(/^<?\w[^>]*[^/]$/)) indent += tab;
12-
});
13-
return formatted.substring(1, formatted.length - 3);
14-
}
3+
import {saveDocumentToFile} from "../file.js";
154

165
export default class SaveProjectPlugin extends LitElement {
176
@property() doc!: XMLDocument;
187
@property() docName!: string;
198

209
async run(): Promise<void> {
21-
if (this.doc) {
22-
const blob = new Blob(
23-
[formatXml(new XMLSerializer().serializeToString(this.doc))],
24-
{
25-
type: 'application/xml',
26-
}
27-
);
28-
29-
const a = document.createElement('a');
30-
a.download = this.docName;
31-
a.href = URL.createObjectURL(blob);
32-
a.dataset.downloadurl = ['application/xml', a.download, a.href].join(':');
33-
a.style.display = 'none';
34-
document.body.appendChild(a);
35-
a.click();
36-
document.body.removeChild(a);
37-
setTimeout(function () {
38-
URL.revokeObjectURL(a.href);
39-
}, 5000);
40-
}
10+
saveDocumentToFile(this.doc, this.docName);
4111
}
4212
}

src/open-scd.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import './compas/CompasChangeSetRadiogroup.js';
4141
import './compas/CompasComment.js';
4242
import './compas/CompasLoading.js';
4343
import './compas/CompasSclTypeList.js';
44+
import './compas/CompasSession.js';
4445

4546
import { newOpenDocEvent, newPendingStateEvent } from './foundation.js';
4647
import { getTheme } from './themes.js';
@@ -53,6 +54,8 @@ import { Setting } from './Setting.js';
5354
import { Waiting } from './Waiting.js';
5455
import { Wizarding } from './Wizarding.js';
5556

57+
import {renderCompasSessionDialogs} from "./compas/CompasSession.js";
58+
5659
import { ListItem } from '@material/mwc-list/mwc-list-item';
5760

5861
/** The `<open-scd>` custom element is the main entry point of the
@@ -121,7 +124,7 @@ export class OpenSCD extends Hosting(Setting(Wizarding(Waiting(Plugging(Editing(
121124
}
122125

123126
render(): TemplateResult {
124-
return html` ${super.render()} ${getTheme(this.settings.theme)} `;
127+
return html` ${super.render()} ${getTheme(this.settings.theme)} ${renderCompasSessionDialogs(this.doc, this.docName)}`;
125128
}
126129

127130
static styles = css`

0 commit comments

Comments
 (0)