Skip to content

Commit 2aee3cc

Browse files
feat(editors/publisher): add read only gse-control-element-editor (openscd#917)
1 parent 21732c9 commit 2aee3cc

File tree

5 files changed

+471
-7
lines changed

5 files changed

+471
-7
lines changed

src/editors/publisher/gse-control-editor.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
customElement,
44
html,
55
LitElement,
6-
property as state,
6+
property,
7+
state,
78
query,
89
TemplateResult,
910
} from 'lit-element';
@@ -15,6 +16,7 @@ import { Button } from '@material/mwc-button';
1516
import { ListItem } from '@material/mwc-list/mwc-list-item';
1617

1718
import './data-set-element-editor.js';
19+
import './gse-control-element-editor.js';
1820
import '../../filtered-list.js';
1921
import { FilteredList } from '../../filtered-list.js';
2022

@@ -25,21 +27,39 @@ import { styles } from './foundation.js';
2527
@customElement('gse-control-editor')
2628
export class GseControlEditor extends LitElement {
2729
/** The document being edited as provided to plugins by [[`OpenSCD`]]. */
28-
@state({ attribute: false })
29-
doc!: XMLDocument;
30+
@property({ attribute: false })
31+
set doc(newDoc: XMLDocument) {
32+
if (this._doc === newDoc) return;
3033

34+
this.selectedDataSet = undefined;
35+
this.selectedGseControl = undefined;
36+
37+
this._doc = newDoc;
38+
39+
this.requestUpdate();
40+
}
41+
get doc(): XMLDocument {
42+
return this._doc;
43+
}
44+
private _doc!: XMLDocument;
45+
46+
@state()
47+
selectedGseControl?: Element;
3148
@state()
32-
selectedGseControl?: Element | null;
49+
selectedDataSet?: Element | null;
3350

3451
@query('.selectionlist') selectionList!: FilteredList;
3552
@query('mwc-button') selectGSEControlButton!: Button;
3653

3754
private selectGSEControl(evt: Event): void {
3855
const id = ((evt.target as FilteredList).selected as ListItem).value;
3956
const gseControl = this.doc.querySelector(selector('GSEControl', id));
57+
if (!gseControl) return;
58+
59+
this.selectedGseControl = gseControl;
4060

4161
if (gseControl) {
42-
this.selectedGseControl = gseControl.parentElement?.querySelector(
62+
this.selectedDataSet = gseControl.parentElement?.querySelector(
4363
`DataSet[name="${gseControl.getAttribute('datSet')}"]`
4464
);
4565
(evt.target as FilteredList).classList.add('hidden');
@@ -51,8 +71,12 @@ export class GseControlEditor extends LitElement {
5171
if (this.selectedGseControl !== undefined)
5272
return html`<div class="elementeditorcontainer">
5373
<data-set-element-editor
54-
.element=${this.selectedGseControl}
74+
.element=${this.selectedDataSet!}
5575
></data-set-element-editor>
76+
<gse-control-element-editor
77+
.doc=${this.doc}
78+
.element=${this.selectedGseControl}
79+
></gse-control-element-editor>
5680
</div>`;
5781

5882
return html``;
@@ -122,8 +146,29 @@ export class GseControlEditor extends LitElement {
122146
static styles = css`
123147
${styles}
124148
149+
.elementeditorcontainer {
150+
flex: 65%;
151+
margin: 4px 8px 4px 4px;
152+
background-color: var(--mdc-theme-surface);
153+
overflow-y: scroll;
154+
display: grid;
155+
grid-gap: 12px;
156+
padding: 8px 12px 16px;
157+
grid-template-columns: repeat(3, 1fr);
158+
}
159+
125160
data-set-element-editor {
126-
flex: auto;
161+
grid-column: 1 / 2;
162+
}
163+
164+
gse-control-element-editor {
165+
grid-column: 2 / 4;
166+
}
167+
168+
@media (max-width: 950px) {
169+
.elementeditorcontainer {
170+
display: block;
171+
}
127172
}
128173
`;
129174
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import {
2+
css,
3+
customElement,
4+
html,
5+
LitElement,
6+
property,
7+
TemplateResult,
8+
} from 'lit-element';
9+
import { translate } from 'lit-translate';
10+
11+
import '@material/mwc-formfield';
12+
import '@material/mwc-checkbox';
13+
14+
import '../../wizard-checkbox.js';
15+
import '../../wizard-select.js';
16+
import '../../wizard-textfield.js';
17+
18+
import { identity } from '../../foundation.js';
19+
import { maxLength, patterns } from '../../wizards/foundation/limits.js';
20+
import { typeNullable, typePattern } from '../../wizards/foundation/p-types.js';
21+
import { ifDefined } from 'lit-html/directives/if-defined.js';
22+
23+
@customElement('gse-control-element-editor')
24+
export class GseControlElementEditor extends LitElement {
25+
/** The document being edited as provided to plugins by [[`OpenSCD`]]. */
26+
@property({ attribute: false })
27+
doc!: XMLDocument;
28+
/** The element being edited as provided to plugins by [[`OpenSCD`]]. */
29+
@property({ attribute: false })
30+
element!: Element;
31+
@property({ attribute: false })
32+
get gSE(): Element | null | undefined {
33+
const cbName = this.element.getAttribute('name');
34+
const iedName = this.element.closest('IED')?.getAttribute('name');
35+
const apName = this.element.closest('AccessPoint')?.getAttribute('name');
36+
const ldInst = this.element.closest('LDevice')?.getAttribute('inst');
37+
38+
return this.element.ownerDocument.querySelector(
39+
`:root > Communication > SubNetwork > ` +
40+
`ConnectedAP[iedName="${iedName}"][apName="${apName}"] > ` +
41+
`GSE[ldInst="${ldInst}"][cbName="${cbName}"]`
42+
);
43+
}
44+
45+
private renderGseContent(): TemplateResult {
46+
const gSE = this.gSE;
47+
if (!gSE)
48+
return html`<div class="content">
49+
<h3>
50+
<div>Communication Settings (GSE)</div>
51+
<div class="headersubtitle">No connection to SubNetwork</div>
52+
</h3>
53+
</div>`;
54+
55+
const minTime = gSE.querySelector('MinTime')?.innerHTML.trim() ?? null;
56+
const maxTime = gSE.querySelector('MaxTime')?.innerHTML.trim() ?? null;
57+
58+
const hasInstType = Array.from(gSE.querySelectorAll('Address > P')).some(
59+
pType => pType.getAttribute('xsi:type')
60+
);
61+
62+
const attributes: Record<string, string | null> = {};
63+
64+
['MAC-Address', 'APPID', 'VLAN-ID', 'VLAN-PRIORITY'].forEach(key => {
65+
if (!attributes[key])
66+
attributes[key] =
67+
gSE.querySelector(`Address > P[type="${key}"]`)?.innerHTML.trim() ??
68+
null;
69+
});
70+
71+
return html`<div class="content">
72+
<h3>Communication Settings (GSE)</h3>
73+
<mwc-formfield
74+
label="${translate('connectedap.wizard.addschemainsttype')}"
75+
><mwc-checkbox
76+
id="instType"
77+
?checked="${hasInstType}"
78+
></mwc-checkbox></mwc-formfield
79+
>${Object.entries(attributes).map(
80+
([key, value]) =>
81+
html`<wizard-textfield
82+
label="${key}"
83+
?nullable=${typeNullable[key]}
84+
.maybeValue=${value}
85+
pattern="${ifDefined(typePattern[key])}"
86+
required
87+
></wizard-textfield>`
88+
)}<wizard-textfield
89+
label="MinTime"
90+
.maybeValue=${minTime}
91+
nullable
92+
suffix="ms"
93+
type="number"
94+
></wizard-textfield
95+
><wizard-textfield
96+
label="MaxTime"
97+
.maybeValue=${maxTime}
98+
nullable
99+
suffix="ms"
100+
type="number"
101+
></wizard-textfield>
102+
</div>`;
103+
}
104+
105+
private renderGseControlContent(): TemplateResult {
106+
const [name, desc, type, appID, fixedOffs, securityEnabled] = [
107+
'name',
108+
'desc',
109+
'type',
110+
'appID',
111+
'fixedOffs',
112+
'securityEnabled',
113+
].map(attr => this.element?.getAttribute(attr));
114+
115+
return html`<div class="content">
116+
<wizard-textfield
117+
label="name"
118+
.maybeValue=${name}
119+
helper="${translate('scl.name')}"
120+
required
121+
validationMessage="${translate('textfield.required')}"
122+
pattern="${patterns.asciName}"
123+
maxLength="${maxLength.cbName}"
124+
dialogInitialFocus
125+
></wizard-textfield>
126+
<wizard-textfield
127+
label="desc"
128+
.maybeValue=${desc}
129+
nullable
130+
helper="${translate('scl.desc')}"
131+
></wizard-textfield>
132+
<wizard-select
133+
label="type"
134+
.maybeValue=${type}
135+
helper="${translate('scl.type')}"
136+
nullable
137+
required
138+
>${['GOOSE', 'GSSE'].map(
139+
type => html`<mwc-list-item value="${type}">${type}</mwc-list-item>`
140+
)}</wizard-select
141+
>
142+
<wizard-textfield
143+
label="appID"
144+
.maybeValue=${appID}
145+
helper="${translate('scl.id')}"
146+
required
147+
validationMessage="${translate('textfield.nonempty')}"
148+
></wizard-textfield>
149+
<wizard-checkbox
150+
label="fixedOffs"
151+
.maybeValue=${fixedOffs}
152+
nullable
153+
helper="${translate('scl.fixedOffs')}"
154+
></wizard-checkbox>
155+
<wizard-select
156+
label="securityEnabled"
157+
.maybeValue=${securityEnabled}
158+
nullable
159+
required
160+
helper="${translate('scl.securityEnable')}"
161+
>${['None', 'Signature', 'SignatureAndEncryption'].map(
162+
type => html`<mwc-list-item value="${type}">${type}</mwc-list-item>`
163+
)}</wizard-select
164+
>
165+
</div>`;
166+
}
167+
168+
render(): TemplateResult {
169+
return html`<h2 style="display: flex;">
170+
<div style="flex:auto">
171+
<div>GSEControl</div>
172+
<div class="headersubtitle">${identity(this.element)}</div>
173+
</div>
174+
</h2>
175+
<div class="parentcontent">
176+
${this.renderGseControlContent()}${this.renderGseContent()}
177+
</div>`;
178+
}
179+
180+
static styles = css`
181+
.parentcontent {
182+
display: grid;
183+
grid-gap: 12px;
184+
box-sizing: border-box;
185+
grid-template-columns: repeat(auto-fit, minmax(316px, auto));
186+
}
187+
188+
.content {
189+
border-left: thick solid var(--mdc-theme-on-primary);
190+
}
191+
192+
.content > * {
193+
display: block;
194+
margin: 4px 8px 16px;
195+
}
196+
197+
h2,
198+
h3 {
199+
color: var(--mdc-theme-on-surface);
200+
font-family: 'Roboto', sans-serif;
201+
font-weight: 300;
202+
margin: 4px 8px 16px;
203+
padding-left: 0.3em;
204+
}
205+
206+
.headersubtitle {
207+
font-size: 16px;
208+
font-weight: 200;
209+
overflow: hidden;
210+
white-space: nowrap;
211+
text-overflow: ellipsis;
212+
}
213+
214+
*[iconTrailing='search'] {
215+
--mdc-shape-small: 28px;
216+
}
217+
218+
@media (max-width: 950px) {
219+
.content {
220+
border-left: 0px solid var(--mdc-theme-on-primary);
221+
}
222+
}
223+
`;
224+
}

test/unit/editors/publisher/__snapshots__/gse-control-editor.test.snap.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ snapshots["Editor for GSEControl element with a selected GSEControl looks like t
257257
<div class="elementeditorcontainer">
258258
<data-set-element-editor>
259259
</data-set-element-editor>
260+
<gse-control-element-editor>
261+
</gse-control-element-editor>
260262
</div>
261263
</div>
262264
`;

0 commit comments

Comments
 (0)