Skip to content

Commit 51c3662

Browse files
feat(Logging): show Hitem from SCL (#315)
1 parent b5e2621 commit 51c3662

File tree

6 files changed

+195
-13
lines changed

6 files changed

+195
-13
lines changed

__snapshots__/open-scd.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,8 @@
395395
on=""
396396
>
397397
</mwc-icon-button-toggle>
398+
<mwc-icon-button-toggle id="sclhistoryfilter">
399+
</mwc-icon-button-toggle>
398400
<mwc-list
399401
id="content"
400402
wrapfocus=""

src/Logging.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
LogEvent,
2323
Mixin,
2424
newActionEvent,
25+
OpenDocEvent,
26+
SclhistoryEntry,
2527
} from './foundation.js';
2628
import { get, translate } from 'lit-translate';
2729
import { getFilterIcon, iconColors } from './icons.js';
@@ -32,6 +34,7 @@ const icons = {
3234
warning: 'warning',
3335
error: 'report',
3436
action: 'history',
37+
sclhistory: 'history_toggle_off',
3538
};
3639

3740
function getPluginName(src: string): string {
@@ -101,6 +104,58 @@ export function Logging<TBase extends LitElementConstructor>(Base: TBase) {
101104
return index;
102105
}
103106

107+
private convertToDate(when: string | null): Date | null {
108+
const convertedTime = new Date(when ?? '');
109+
if (!isNaN(convertedTime.getTime())) {
110+
return convertedTime;
111+
}
112+
return null;
113+
}
114+
115+
private createMessage(
116+
who: string | null,
117+
why: string | null
118+
): string | undefined {
119+
let message = who;
120+
if (message !== null && why !== null) {
121+
message += ' : ' + why;
122+
} else if (why !== null) {
123+
message = why;
124+
}
125+
return message ?? undefined;
126+
}
127+
128+
private createSclHistoryEntry(
129+
who: string | null,
130+
what: string | null,
131+
why: string | null,
132+
when: string | null
133+
): SclhistoryEntry {
134+
return {
135+
kind: 'sclhistory',
136+
title: what ?? 'UNDEFINED',
137+
message: this.createMessage(who, why),
138+
time: this.convertToDate(when),
139+
};
140+
}
141+
142+
private async onLoadHistoryFromDoc(event: OpenDocEvent) {
143+
const doc = event.detail.doc;
144+
145+
Array.from(
146+
doc.querySelectorAll(':root > Header > History > Hitem')
147+
).forEach(historyItem => {
148+
this.history.push(
149+
this.createSclHistoryEntry(
150+
historyItem.getAttribute('who'),
151+
historyItem.getAttribute('what'),
152+
historyItem.getAttribute('why'),
153+
historyItem.getAttribute('when')
154+
)
155+
);
156+
});
157+
}
158+
104159
private onIssue(de: IssueEvent): void {
105160
const issues = this.diagnoses.get(de.detail.validatorId);
106161
if (issues && issues[0].statusNumber > de.detail.statusNumber) return;
@@ -189,6 +244,7 @@ export function Logging<TBase extends LitElementConstructor>(Base: TBase) {
189244
this.onLog = this.onLog.bind(this);
190245
this.addEventListener('log', this.onLog);
191246
this.addEventListener('issue', this.onIssue);
247+
this.addEventListener('open-doc', this.onLoadHistoryFromDoc);
192248
}
193249

194250
renderLogEntry(
@@ -205,7 +261,7 @@ export function Logging<TBase extends LitElementConstructor>(Base: TBase) {
205261
>
206262
<span>
207263
<!-- FIXME: replace tt with mwc-chip asap -->
208-
<tt>${entry.time.toLocaleTimeString()}</tt>
264+
<tt>${entry.time?.toLocaleString()}</tt>
209265
${entry.title}</span
210266
>
211267
<span slot="secondary">${entry.message}</span>
@@ -273,7 +329,9 @@ export function Logging<TBase extends LitElementConstructor>(Base: TBase) {
273329

274330
private renderFilterButtons() {
275331
return (<LogEntryType[]>Object.keys(icons)).map(
276-
kind => html`<mwc-icon-button-toggle id="${kind}filter" on
332+
kind => html`<mwc-icon-button-toggle
333+
id="${kind}filter"
334+
?on=${kind !== 'sclhistory'}
277335
>${getFilterIcon(kind, false)}
278336
${getFilterIcon(kind, true)}</mwc-icon-button-toggle
279337
>`
@@ -297,10 +355,14 @@ export function Logging<TBase extends LitElementConstructor>(Base: TBase) {
297355
#log > mwc-icon-button-toggle:nth-child(4) {
298356
right: 158px;
299357
}
358+
#log > mwc-icon-button-toggle:nth-child(5) {
359+
right: 206px;
360+
}
300361
#content mwc-list-item.info,
301362
#content mwc-list-item.warning,
302363
#content mwc-list-item.error,
303-
#content mwc-list-item.action {
364+
#content mwc-list-item.action,
365+
#content mwc-list-item.sclhistory {
304366
display: none;
305367
}
306368
#infofilter[on] ~ #content mwc-list-item.info {
@@ -315,6 +377,9 @@ export function Logging<TBase extends LitElementConstructor>(Base: TBase) {
315377
#actionfilter[on] ~ #content mwc-list-item.action {
316378
display: flex;
317379
}
380+
#sclhistoryfilter[on] ~ #content mwc-list-item.sclhistory {
381+
display: flex;
382+
}
318383
319384
#log {
320385
--mdc-dialog-min-width: 92vw;

src/foundation.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,13 @@ export function newWizardEvent(
228228

229229
type InfoEntryKind = 'info' | 'warning' | 'error';
230230

231-
export type LogEntryType = 'info' | 'warning' | 'error' | 'action' | 'reset';
231+
export type LogEntryType =
232+
| 'info'
233+
| 'warning'
234+
| 'error'
235+
| 'action'
236+
| 'reset'
237+
| 'sclhistory';
232238

233239
/** The basic information contained in each [[`LogEntry`]]. */
234240
export interface LogDetailBase {
@@ -245,6 +251,10 @@ export interface InfoDetail extends LogDetailBase {
245251
kind: InfoEntryKind;
246252
cause?: LogEntry;
247253
}
254+
/** A [[`LogEntry`]] create from the HItem Line (History) of the SCD File */
255+
export interface SclhistoryDetail extends LogDetailBase {
256+
kind: 'sclhistory';
257+
}
248258

249259
export interface ResetDetail {
250260
kind: 'reset';
@@ -283,13 +293,14 @@ export function newIssueEvent(
283293

284294
/** [[`LogEntry`]]s are timestamped upon being committed to the `history`. */
285295
interface Timestamped {
286-
time: Date;
296+
time: Date | null;
287297
}
288298

289299
export type CommitEntry = Timestamped & CommitDetail;
290300
export type InfoEntry = Timestamped & InfoDetail;
301+
export type SclhistoryEntry = Timestamped & SclhistoryDetail;
291302

292-
export type LogEntry = InfoEntry | CommitEntry;
303+
export type LogEntry = InfoEntry | CommitEntry | SclhistoryEntry;
293304

294305
/** Represents some work pending completion, upon which `promise` resolves. */
295306
export interface PendingStateDetail {

src/icons.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,19 @@ const pathsSVG = {
4444
warning: svg`<path d="M0 0h24v24H0z" fill="none"></path><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" fill="currentColor"></path>`,
4545
error: svg`<path d="M0 0h24v24H0V0z" fill="none"></path>
4646
<path d="M15.73 3H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27L15.73 3zM19 14.9L14.9 19H9.1L5 14.9V9.1L9.1 5h5.8L19 9.1v5.8z" fill="currentColor"></path><path d="M11 7h2v7h-2z" fill="currentColor"></path><circle cx="12" cy="16" r="1" fill="currentColor"></circle>`,
47+
sclhistory: svg`<rect xmlns="http://www.w3.org/2000/svg" fill="none"/><path xmlns="http://www.w3.org/2000/svg" d="M15.1,19.37l1,1.74c-0.96,0.44-2.01,0.73-3.1,0.84v-2.02C13.74,19.84,14.44,19.65,15.1,19.37z M4.07,13H2.05 c0.11,1.1,0.4,2.14,0.84,3.1l1.74-1C4.35,14.44,4.16,13.74,4.07,13z M15.1,4.63l1-1.74C15.14,2.45,14.1,2.16,13,2.05v2.02 C13.74,4.16,14.44,4.35,15.1,4.63z M19.93,11h2.02c-0.11-1.1-0.4-2.14-0.84-3.1l-1.74,1C19.65,9.56,19.84,10.26,19.93,11z M8.9,19.37l-1,1.74c0.96,0.44,2.01,0.73,3.1,0.84v-2.02C10.26,19.84,9.56,19.65,8.9,19.37z M11,4.07V2.05 c-1.1,0.11-2.14,0.4-3.1,0.84l1,1.74C9.56,4.35,10.26,4.16,11,4.07z M18.36,7.17l1.74-1.01c-0.63-0.87-1.4-1.64-2.27-2.27 l-1.01,1.74C17.41,6.08,17.92,6.59,18.36,7.17z M4.63,8.9l-1.74-1C2.45,8.86,2.16,9.9,2.05,11h2.02C4.16,10.26,4.35,9.56,4.63,8.9z M19.93,13c-0.09,0.74-0.28,1.44-0.56,2.1l1.74,1c0.44-0.96,0.73-2.01,0.84-3.1H19.93z M16.83,18.36l1.01,1.74 c0.87-0.63,1.64-1.4,2.27-2.27l-1.74-1.01C17.92,17.41,17.41,17.92,16.83,18.36z M7.17,5.64L6.17,3.89 C5.29,4.53,4.53,5.29,3.9,6.17l1.74,1.01C6.08,6.59,6.59,6.08,7.17,5.64z M5.64,16.83L3.9,17.83c0.63,0.87,1.4,1.64,2.27,2.27 l1.01-1.74C6.59,17.92,6.08,17.41,5.64,16.83z M13,7h-2v5.41l4.29,4.29l1.41-1.41L13,11.59V7z"/>`,
4748
};
4849

4950
export const iconColors = {
5051
info: '--cyan',
5152
warning: '--yellow',
5253
error: '--red',
5354
action: '--blue',
55+
sclhistory: '--cyan',
5456
};
5557

5658
export function getFilterIcon(
57-
type: 'action' | 'info' | 'warning' | 'error' | 'reset',
59+
type: 'action' | 'info' | 'warning' | 'error' | 'reset' | 'sclhistory',
5860
state: boolean
5961
): TemplateResult {
6062
if (type === 'reset') return html``;

test/testfiles/history.scd

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<SCL xmlns="http://www.iec.ch/61850/2003/SCL" release="4" revision="B" version="2007">
3+
<Header id="40cf9d4d-19b1-4d24-9faa-0afa26ded001" revision="" version="1.0.0">
4+
<History>
5+
<Hitem revision="" version="0.0.1" what="SCD created from CIM File(s): some-cim-file.xml" when="2021-09-14T10:51:38Z" who="Test User 1"/>
6+
<Hitem revision="" version="1.0.0" what="SCD created, Nice history test 1" when="2021-09-14T10:52:28Z" who="Test User 1"/>
7+
<Hitem revision="" version="1.1.0" what="SCD updated, Nice history test 2" when="" who="Test User 1"/>
8+
<Hitem revision="" version="1.2.0" what="SCD updated, Nice history test 3" when="invalid" who="Test User 1" why="Small correction in substation"/>
9+
<Hitem revision="" version="1.2.0" what="SCD updated, Nice history test 4" why="Small correction in substation"/>
10+
<Hitem revision="" version="1.2.0" what="SCD updated, Nice history test 5" />
11+
<Hitem revision="" version="1.2.0" who="Test User 1" why="Small correction in substation"/>
12+
</History>
13+
</Header>
14+
</SCL>

test/unit/Logging.test.ts

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
CommitEntry,
66
newIssueEvent,
77
newLogEvent,
8+
newOpenDocEvent,
89
} from '../../src/foundation.js';
910

1011
import { MockAction } from './mock-actions.js';
@@ -218,7 +219,94 @@ describe('LoggingElement', () => {
218219
});
219220
});
220221

221-
describe('with an issue incomming', () => {
222+
describe('when loading file with history items', () => {
223+
beforeEach(async () => {
224+
const doc = await fetch('/base/test/testfiles/history.scd')
225+
.then(response => response.text())
226+
.then(str => new DOMParser().parseFromString(str, 'application/xml'));
227+
228+
element.dispatchEvent(newOpenDocEvent(doc, 'history.scd'));
229+
await element.requestUpdate();
230+
});
231+
232+
it('has 7 items in the history list', () => {
233+
expect(element.history.length).to.be.equal(7);
234+
});
235+
236+
it('expect the correct values of the first line (with valid when and no why)', () => {
237+
expect(element.history.length).to.be.equal(7);
238+
expect(element.history[0].kind).to.be.equal('sclhistory');
239+
expect(element.history[0].title).to.satisfy((msg: string) =>
240+
msg.startsWith('SCD created from CIM File(s):')
241+
);
242+
expect(element.history[0].message).to.be.equal('Test User 1');
243+
expect(element.history[0].time).to.be.not.null;
244+
});
245+
246+
it('expect the correct values of the third line (with no when and no why)', () => {
247+
expect(element.history.length).to.be.equal(7);
248+
expect(element.history[2].kind).to.be.equal('sclhistory');
249+
expect(element.history[2].title).to.satisfy((msg: string) =>
250+
msg.startsWith('SCD updated, ')
251+
);
252+
expect(element.history[2].message).to.be.equal('Test User 1');
253+
expect(element.history[2].time).to.be.null;
254+
});
255+
256+
it('expect the correct values of the forth line (with invalid when, but valid why)', () => {
257+
expect(element.history.length).to.be.equal(7);
258+
expect(element.history[3].kind).to.be.equal('sclhistory');
259+
expect(element.history[3].title).to.satisfy((msg: string) =>
260+
msg.startsWith('SCD updated, ')
261+
);
262+
expect(element.history[3].message).to.be.equal(
263+
'Test User 1 : Small correction in substation'
264+
);
265+
expect(element.history[3].time).to.be.null;
266+
});
267+
268+
it('expect the correct message values (with missing who)', () => {
269+
expect(element.history.length).to.be.equal(7);
270+
expect(element.history[4].kind).to.be.equal('sclhistory');
271+
expect(element.history[4].title).to.satisfy((msg: string) =>
272+
msg.startsWith('SCD updated, ')
273+
);
274+
expect(element.history[4].message).to.be.equal(
275+
'Small correction in substation'
276+
);
277+
expect(element.history[4].time).to.be.null;
278+
});
279+
280+
it('expect undefined message (with missing who and why)', () => {
281+
expect(element.history.length).to.be.equal(7);
282+
expect(element.history[5].kind).to.be.equal('sclhistory');
283+
expect(element.history[5].title).to.satisfy((msg: string) =>
284+
msg.startsWith('SCD updated, ')
285+
);
286+
expect(element.history[5].message).to.be.undefined;
287+
expect(element.history[5].time).to.be.null;
288+
});
289+
290+
it('expect undefined title (with invalid what)', () => {
291+
expect(element.history.length).to.be.equal(7);
292+
expect(element.history[6].kind).to.be.equal('sclhistory');
293+
expect(element.history[6].title).to.satisfy((msg: string) =>
294+
msg.startsWith('UNDEFINED')
295+
);
296+
expect(element.history[6].message).to.be.equal(
297+
'Test User 1 : Small correction in substation'
298+
);
299+
expect(element.history[6].time).to.be.null;
300+
});
301+
302+
it('can reset its history', async () => {
303+
element.dispatchEvent(newLogEvent({ kind: 'reset' }));
304+
await element.requestUpdate();
305+
expect(element).property('history').to.be.empty;
306+
});
307+
});
308+
309+
describe('with an issue incoming', () => {
222310
beforeEach(async () => {
223311
element.dispatchEvent(
224312
newIssueEvent({
@@ -246,7 +334,7 @@ describe('LoggingElement', () => {
246334
expect(element.diagnoses.has('/src/validators/ValidateTemplates.js')).to
247335
.be.false);
248336

249-
describe('with a second issue comming in - new statusNumber, same validator', () => {
337+
describe('with a second issue coming in - new statusNumber, same validator', () => {
250338
beforeEach(() => {
251339
element.dispatchEvent(
252340
newIssueEvent({
@@ -284,7 +372,7 @@ describe('LoggingElement', () => {
284372
});
285373
});
286374

287-
describe('with a second issue comming in - same statusNumber, same validator', () => {
375+
describe('with a second issue coming in - same statusNumber, same validator', () => {
288376
beforeEach(() => {
289377
element.dispatchEvent(
290378
newIssueEvent({
@@ -314,7 +402,7 @@ describe('LoggingElement', () => {
314402
});
315403
});
316404

317-
describe('with a second issue comming in - outdated statusNumber, same validator', () => {
405+
describe('with a second issue coming in - outdated statusNumber, same validator', () => {
318406
beforeEach(() => {
319407
element.dispatchEvent(
320408
newIssueEvent({
@@ -325,7 +413,7 @@ describe('LoggingElement', () => {
325413
);
326414
});
327415

328-
it('ignores incomming the issue', () => {
416+
it('ignores incoming the issue', () => {
329417
expect(element.diagnoses.get('/src/validators/ValidateSchema.js')).to
330418
.exist;
331419
expect(
@@ -339,7 +427,7 @@ describe('LoggingElement', () => {
339427
});
340428
});
341429

342-
describe('with another issue comming in - new validator', () => {
430+
describe('with another issue coming in - new validator', () => {
343431
beforeEach(() => {
344432
element.dispatchEvent(
345433
newIssueEvent({

0 commit comments

Comments
 (0)