diff --git a/src/editors/publisher/gse-control-editor.ts b/src/editors/publisher/gse-control-editor.ts index 5fb4c27acc..00db841002 100644 --- a/src/editors/publisher/gse-control-editor.ts +++ b/src/editors/publisher/gse-control-editor.ts @@ -3,7 +3,8 @@ import { customElement, html, LitElement, - property as state, + property, + state, query, TemplateResult, } from 'lit-element'; @@ -15,6 +16,7 @@ import { Button } from '@material/mwc-button'; import { ListItem } from '@material/mwc-list/mwc-list-item'; import './data-set-element-editor.js'; +import './gse-control-element-editor.js'; import '../../filtered-list.js'; import { FilteredList } from '../../filtered-list.js'; @@ -25,11 +27,26 @@ import { styles } from './foundation.js'; @customElement('gse-control-editor') export class GseControlEditor extends LitElement { /** The document being edited as provided to plugins by [[`OpenSCD`]]. */ - @state({ attribute: false }) - doc!: XMLDocument; + @property({ attribute: false }) + set doc(newDoc: XMLDocument) { + if (this._doc === newDoc) return; + this.selectedDataSet = undefined; + this.selectedGseControl = undefined; + + this._doc = newDoc; + + this.requestUpdate(); + } + get doc(): XMLDocument { + return this._doc; + } + private _doc!: XMLDocument; + + @state() + selectedGseControl?: Element; @state() - selectedGseControl?: Element | null; + selectedDataSet?: Element | null; @query('.selectionlist') selectionList!: FilteredList; @query('mwc-button') selectGSEControlButton!: Button; @@ -37,9 +54,12 @@ export class GseControlEditor extends LitElement { private selectGSEControl(evt: Event): void { const id = ((evt.target as FilteredList).selected as ListItem).value; const gseControl = this.doc.querySelector(selector('GSEControl', id)); + if (!gseControl) return; + + this.selectedGseControl = gseControl; if (gseControl) { - this.selectedGseControl = gseControl.parentElement?.querySelector( + this.selectedDataSet = gseControl.parentElement?.querySelector( `DataSet[name="${gseControl.getAttribute('datSet')}"]` ); (evt.target as FilteredList).classList.add('hidden'); @@ -51,8 +71,12 @@ export class GseControlEditor extends LitElement { if (this.selectedGseControl !== undefined) return html`
+
`; return html``; @@ -122,8 +146,29 @@ export class GseControlEditor extends LitElement { static styles = css` ${styles} + .elementeditorcontainer { + flex: 65%; + margin: 4px 8px 4px 4px; + background-color: var(--mdc-theme-surface); + overflow-y: scroll; + display: grid; + grid-gap: 12px; + padding: 8px 12px 16px; + grid-template-columns: repeat(3, 1fr); + } + data-set-element-editor { - flex: auto; + grid-column: 1 / 2; + } + + gse-control-element-editor { + grid-column: 2 / 4; + } + + @media (max-width: 950px) { + .elementeditorcontainer { + display: block; + } } `; } diff --git a/src/editors/publisher/gse-control-element-editor.ts b/src/editors/publisher/gse-control-element-editor.ts new file mode 100644 index 0000000000..a4b9f79f45 --- /dev/null +++ b/src/editors/publisher/gse-control-element-editor.ts @@ -0,0 +1,224 @@ +import { + css, + customElement, + html, + LitElement, + property, + TemplateResult, +} from 'lit-element'; +import { translate } from 'lit-translate'; + +import '@material/mwc-formfield'; +import '@material/mwc-checkbox'; + +import '../../wizard-checkbox.js'; +import '../../wizard-select.js'; +import '../../wizard-textfield.js'; + +import { identity } from '../../foundation.js'; +import { maxLength, patterns } from '../../wizards/foundation/limits.js'; +import { typeNullable, typePattern } from '../../wizards/foundation/p-types.js'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; + +@customElement('gse-control-element-editor') +export class GseControlElementEditor extends LitElement { + /** The document being edited as provided to plugins by [[`OpenSCD`]]. */ + @property({ attribute: false }) + doc!: XMLDocument; + /** The element being edited as provided to plugins by [[`OpenSCD`]]. */ + @property({ attribute: false }) + element!: Element; + @property({ attribute: false }) + get gSE(): Element | null | undefined { + const cbName = this.element.getAttribute('name'); + const iedName = this.element.closest('IED')?.getAttribute('name'); + const apName = this.element.closest('AccessPoint')?.getAttribute('name'); + const ldInst = this.element.closest('LDevice')?.getAttribute('inst'); + + return this.element.ownerDocument.querySelector( + `:root > Communication > SubNetwork > ` + + `ConnectedAP[iedName="${iedName}"][apName="${apName}"] > ` + + `GSE[ldInst="${ldInst}"][cbName="${cbName}"]` + ); + } + + private renderGseContent(): TemplateResult { + const gSE = this.gSE; + if (!gSE) + return html`
+

+
Communication Settings (GSE)
+
No connection to SubNetwork
+

+
`; + + const minTime = gSE.querySelector('MinTime')?.innerHTML.trim() ?? null; + const maxTime = gSE.querySelector('MaxTime')?.innerHTML.trim() ?? null; + + const hasInstType = Array.from(gSE.querySelectorAll('Address > P')).some( + pType => pType.getAttribute('xsi:type') + ); + + const attributes: Record = {}; + + ['MAC-Address', 'APPID', 'VLAN-ID', 'VLAN-PRIORITY'].forEach(key => { + if (!attributes[key]) + attributes[key] = + gSE.querySelector(`Address > P[type="${key}"]`)?.innerHTML.trim() ?? + null; + }); + + return html`
+

Communication Settings (GSE)

+ ${Object.entries(attributes).map( + ([key, value]) => + html`` + )} +
`; + } + + private renderGseControlContent(): TemplateResult { + const [name, desc, type, appID, fixedOffs, securityEnabled] = [ + 'name', + 'desc', + 'type', + 'appID', + 'fixedOffs', + 'securityEnabled', + ].map(attr => this.element?.getAttribute(attr)); + + return html`
+ + + ${['GOOSE', 'GSSE'].map( + type => html`${type}` + )} + + + ${['None', 'Signature', 'SignatureAndEncryption'].map( + type => html`${type}` + )} +
`; + } + + render(): TemplateResult { + return html`

+
+
GSEControl
+
${identity(this.element)}
+
+

+
+ ${this.renderGseControlContent()}${this.renderGseContent()} +
`; + } + + static styles = css` + .parentcontent { + display: grid; + grid-gap: 12px; + box-sizing: border-box; + grid-template-columns: repeat(auto-fit, minmax(316px, auto)); + } + + .content { + border-left: thick solid var(--mdc-theme-on-primary); + } + + .content > * { + display: block; + margin: 4px 8px 16px; + } + + h2, + h3 { + color: var(--mdc-theme-on-surface); + font-family: 'Roboto', sans-serif; + font-weight: 300; + margin: 4px 8px 16px; + padding-left: 0.3em; + } + + .headersubtitle { + font-size: 16px; + font-weight: 200; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + *[iconTrailing='search'] { + --mdc-shape-small: 28px; + } + + @media (max-width: 950px) { + .content { + border-left: 0px solid var(--mdc-theme-on-primary); + } + } + `; +} diff --git a/test/unit/editors/publisher/__snapshots__/gse-control-editor.test.snap.js b/test/unit/editors/publisher/__snapshots__/gse-control-editor.test.snap.js index d11d9805e7..ef3616d498 100644 --- a/test/unit/editors/publisher/__snapshots__/gse-control-editor.test.snap.js +++ b/test/unit/editors/publisher/__snapshots__/gse-control-editor.test.snap.js @@ -257,6 +257,8 @@ snapshots["Editor for GSEControl element with a selected GSEControl looks like t
+ +
`; diff --git a/test/unit/editors/publisher/__snapshots__/gse-control-element-editor.test.snap.js b/test/unit/editors/publisher/__snapshots__/gse-control-element-editor.test.snap.js new file mode 100644 index 0000000000..e33e72b769 --- /dev/null +++ b/test/unit/editors/publisher/__snapshots__/gse-control-element-editor.test.snap.js @@ -0,0 +1,163 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["Editor for GSEControl element and its direct children with valid GSEControl looks like the latest snapshot"] = +`

+
+
+ GSEControl +
+
+ IED1>>CircuitBreaker_CB1>GCB +
+
+

+
+
+ + + + + + + GOOSE + + + GSSE + + + + + + + + + None + + + Signature + + + SignatureAndEncryption + + +
+
+

+ Communication Settings (GSE) +

+ + + + + + + + + + + + + + + + +
+
+`; +/* end snapshot Editor for GSEControl element and its direct children with valid GSEControl looks like the latest snapshot */ + diff --git a/test/unit/editors/publisher/gse-control-element-editor.test.ts b/test/unit/editors/publisher/gse-control-element-editor.test.ts new file mode 100644 index 0000000000..f81ddad991 --- /dev/null +++ b/test/unit/editors/publisher/gse-control-element-editor.test.ts @@ -0,0 +1,30 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import '../../../../src/editors/publisher/gse-control-element-editor.js'; +import { GseControlElementEditor } from '../../../../src/editors/publisher/gse-control-element-editor.js'; + +describe('Editor for GSEControl element and its direct children', () => { + let doc: XMLDocument; + let element: GseControlElementEditor; + + beforeEach(async () => { + doc = await fetch('/test/testfiles/wizards/gsecontrol.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + }); + + describe('with valid GSEControl', () => { + beforeEach(async () => { + const gseControl = doc.querySelector('GSEControl')!; + + element = await fixture( + html`` + ); + }); + + it('looks like the latest snapshot', async () => + await expect(element).shadowDom.to.equalSnapshot()); + }); +});