Skip to content

Commit ecfeb5d

Browse files
feat(editors/substation): add read-only l-node-editor (openscd#730)
* feat(editors/substation): add read-only l-node-editor * feat(editors/subtation): add l-node-editor to ConductingEquipment * feat(editors/subtation): add l-node-editor to PowerTransformer * feat(editors/subtation): add l-node-editor to EqSubFunction * feat(editors/subtation): add l-node-editor to EqFunction * feat(editors/subtation): add l-node-editor to SubFunction * feat(editors/subtation): add l-node-editor to Function * feat(editors/subtation): add l-node-editor to Bay * feat(editors/subtation): add l-node-editor to VoltageLevel * feat(editors/subtation): add l-node-editor to Substation * merge with first * style(action-icon): propper highlight of focused secondary items * fix(editors/substation): unify rendering of LNode container * feat(action-icon): add hideActions property (openscd#752) * feat(action-icon): add noaction property * test(editors/substation/l-node-editor): update snapshot * refactor(action-icon): resolve review comments
1 parent 8f89f27 commit ecfeb5d

33 files changed

+1663
-42
lines changed

src/action-icon.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ export class ActionIcon extends LitElement {
3131
/** highlight pane with dotted outline */
3232
@property({ type: Boolean })
3333
highlighted = false;
34+
/** disables CSS adoption to action buttons */
35+
@property({ type: Boolean })
36+
hideActions = false;
3437

3538
async firstUpdated(): Promise<void> {
3639
this.tabIndex = 0;
@@ -75,16 +78,6 @@ export class ActionIcon extends LitElement {
7578
--mdc-icon-size: 64px;
7679
}
7780
78-
:host(:focus-within) ::slotted([slot='icon']),
79-
:host(:focus-within) mwc-icon {
80-
outline-style: solid;
81-
outline-width: 4px;
82-
transform: scale(0.8);
83-
transition: all 250ms linear;
84-
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
85-
0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2);
86-
}
87-
8881
:host([secondary]) ::slotted([slot='icon']),
8982
:host([secondary]) mwc-icon {
9083
outline-color: var(--mdc-theme-secondary);
@@ -96,6 +89,20 @@ export class ActionIcon extends LitElement {
9689
outline-width: 2px;
9790
}
9891
92+
:host(:focus-within) ::slotted([slot='icon']),
93+
:host(:focus-within) mwc-icon {
94+
outline-style: solid;
95+
outline-width: 4px;
96+
}
97+
98+
:host(:focus-within:not([hideActions])) ::slotted([slot='icon']),
99+
:host(:focus-within:not([hideActions])) mwc-icon {
100+
transform: scale(0.8);
101+
transition: all 250ms linear;
102+
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
103+
0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2);
104+
}
105+
99106
::slotted([slot='icon']:hover),
100107
mwc-icon:hover {
101108
outline-style: dashed;
@@ -178,6 +185,10 @@ export class ActionIcon extends LitElement {
178185
opacity 200ms linear;
179186
}
180187
188+
:host([secondary]) header {
189+
background-color: var(--mdc-theme-secondary);
190+
}
191+
181192
:host(:hover) header {
182193
position: absolute;
183194
opacity: 1;
@@ -191,11 +202,18 @@ export class ActionIcon extends LitElement {
191202
:host(:focus-within) header {
192203
position: absolute;
193204
opacity: 1;
194-
transform: translate(0, -80px);
195205
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
196206
0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2);
197207
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1),
198208
opacity 250ms linear;
199209
}
210+
211+
:host(:focus-within:not([hideActions])) header {
212+
transform: translate(0, -80px);
213+
}
214+
215+
:host(:focus-within[hideActions]) header {
216+
transform: translate(0, -40px);
217+
}
200218
`;
201219
}

src/editors/substation/bay-editor.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,20 @@ export class BayEditor extends LitElement {
9999
this.addMenu.anchor = <HTMLElement>this.addButton;
100100
}
101101

102+
private renderLNodes(): TemplateResult {
103+
if (!this.showfunctions) return html``;
104+
105+
const lNodes = getChildElementsByTagName(this.element, 'LNode');
106+
107+
return lNodes.length
108+
? html`<div class="container lnode">
109+
${lNodes.map(
110+
lNode => html`<l-node-editor .element=${lNode}></l-node-editor>`
111+
)}
112+
</div>`
113+
: html``;
114+
}
115+
102116
renderFunctions(): TemplateResult {
103117
if (!this.showfunctions) return html``;
104118

@@ -177,7 +191,7 @@ export class BayEditor extends LitElement {
177191
>${this.renderAddButtons()}</mwc-menu
178192
>
179193
</abbr>
180-
${this.renderIedContainer()} ${this.renderFunctions()}
194+
${this.renderIedContainer()}${this.renderLNodes()}${this.renderFunctions()}
181195
<div
182196
class="${classMap({
183197
content: true,

src/editors/substation/conducting-equipment-editor.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import '@material/mwc-icon-button';
1515
import '../../action-icon.js';
1616
import '../../action-pane.js';
1717
import './eq-function-editor.js';
18-
import { startMove, getIcon } from './foundation.js';
18+
import './l-node-editor.js';
19+
import { startMove, getIcon, styles } from './foundation.js';
1920
import {
2021
getChildElementsByTagName,
2122
newActionEvent,
@@ -62,6 +63,18 @@ export class ConductingEquipmentEditor extends LitElement {
6263
);
6364
}
6465

66+
private renderLNodes(): TemplateResult {
67+
const lNodes = getChildElementsByTagName(this.element, 'LNode');
68+
69+
return lNodes.length
70+
? html`<div class="container lnode">
71+
${lNodes.map(
72+
lNode => html`<l-node-editor .element=${lNode}></l-node-editor>`
73+
)}
74+
</div>`
75+
: html``;
76+
}
77+
6578
renderEqFunctions(): TemplateResult {
6679
if (!this.showfunctions) return html``;
6780

@@ -143,7 +156,7 @@ export class ConductingEquipmentEditor extends LitElement {
143156
render(): TemplateResult {
144157
if (this.showfunctions)
145158
return html`<action-pane label="${this.name}"
146-
>${this.renderContentPane()}${this.renderEqFunctions()}</action-pane
159+
>${this.renderContentPane()}${this.renderLNodes()}${this.renderEqFunctions()}</action-pane
147160
>`;
148161
149162
return html`<action-icon label="${this.name}"
@@ -152,6 +165,8 @@ export class ConductingEquipmentEditor extends LitElement {
152165
}
153166

154167
static styles = css`
168+
${styles}
169+
155170
:host(.moving) {
156171
opacity: 0.3;
157172
}

src/editors/substation/eq-function-editor.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
property,
66
customElement,
77
state,
8+
css,
89
} from 'lit-element';
910

1011
import '../../action-pane.js';
@@ -26,6 +27,18 @@ export class EqFunctionEditor extends LitElement {
2627
return `${name}${desc ? ` - ${desc}` : ''}${type ? ` (${type})` : ''}`;
2728
}
2829

30+
private renderLNodes(): TemplateResult {
31+
const lNodes = getChildElementsByTagName(this.element, 'LNode');
32+
33+
return lNodes.length
34+
? html`<div class="container lnode">
35+
${lNodes.map(
36+
lNode => html`<l-node-editor .element=${lNode}></l-node-editor>`
37+
)}
38+
</div>`
39+
: html``;
40+
}
41+
2942
private renderEqSubFunctions(): TemplateResult {
3043
const eqSubFunctions = getChildElementsByTagName(
3144
this.element,
@@ -45,7 +58,17 @@ export class EqFunctionEditor extends LitElement {
4558
icon="functions"
4659
secondary
4760
highlighted
48-
>${this.renderEqSubFunctions()}</action-pane
61+
>${this.renderLNodes()}${this.renderEqSubFunctions()}</action-pane
4962
>`;
5063
}
64+
65+
static styles = css`
66+
.container.lnode {
67+
display: grid;
68+
grid-gap: 12px;
69+
padding: 8px 12px 16px;
70+
box-sizing: border-box;
71+
grid-template-columns: repeat(auto-fit, minmax(64px, auto));
72+
}
73+
`;
5174
}

src/editors/substation/eq-sub-function-editor.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
property,
66
customElement,
77
state,
8+
css,
89
} from 'lit-element';
910

1011
import '../../action-pane.js';
@@ -38,9 +39,31 @@ export class EqSubFunctionEditor extends LitElement {
3839
)}`;
3940
}
4041

42+
private renderLNodes(): TemplateResult {
43+
const lNodes = getChildElementsByTagName(this.element, 'LNode');
44+
45+
return lNodes.length
46+
? html`<div class="container lnode">
47+
${lNodes.map(
48+
lNode => html`<l-node-editor .element=${lNode}></l-node-editor>`
49+
)}
50+
</div>`
51+
: html``;
52+
}
53+
4154
render(): TemplateResult {
4255
return html`<action-pane label="${this.header}" icon="functions" secondary
43-
>${this.renderEqSubFunctions()}</action-pane
56+
>${this.renderLNodes()}${this.renderEqSubFunctions()}</action-pane
4457
>`;
4558
}
59+
60+
static styles = css`
61+
.container.lnode {
62+
display: grid;
63+
grid-gap: 12px;
64+
padding: 8px 12px 16px;
65+
box-sizing: border-box;
66+
grid-template-columns: repeat(auto-fit, minmax(64px, auto));
67+
}
68+
`;
4669
}

src/editors/substation/foundation.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,14 @@ export const styles = css`
297297
grid-template-columns: repeat(auto-fit, minmax(64px, auto));
298298
}
299299
300+
.container.lnode {
301+
display: grid;
302+
grid-gap: 12px;
303+
padding: 8px 12px 16px;
304+
box-sizing: border-box;
305+
grid-template-columns: repeat(auto-fit, minmax(64px, auto));
306+
}
307+
300308
powertransformer-editor[showfunctions] {
301309
margin: 4px 8px 16px;
302310
}

src/editors/substation/function-editor.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
property,
66
customElement,
77
state,
8+
css,
89
} from 'lit-element';
910

1011
import '../../action-pane.js';
@@ -26,6 +27,18 @@ export class FunctionEditor extends LitElement {
2627
return `${name}${desc ? ` - ${desc}` : ''}${type ? ` (${type})` : ''}`;
2728
}
2829

30+
private renderLNodes(): TemplateResult {
31+
const lNodes = getChildElementsByTagName(this.element, 'LNode');
32+
33+
return lNodes.length
34+
? html`<div class="container lnode">
35+
${lNodes.map(
36+
lNode => html`<l-node-editor .element=${lNode}></l-node-editor>`
37+
)}
38+
</div>`
39+
: html``;
40+
}
41+
2942
private renderSubFunctions(): TemplateResult {
3043
const subfunctions = getChildElementsByTagName(this.element, 'SubFunction');
3144
return html` ${subfunctions.map(
@@ -42,7 +55,17 @@ export class FunctionEditor extends LitElement {
4255
icon="functions"
4356
secondary
4457
highlighted
45-
>${this.renderSubFunctions()}</action-pane
58+
>${this.renderLNodes()}${this.renderSubFunctions()}</action-pane
4659
>`;
4760
}
61+
62+
static styles = css`
63+
.container.lnode {
64+
display: grid;
65+
grid-gap: 12px;
66+
padding: 8px 12px 16px;
67+
box-sizing: border-box;
68+
grid-template-columns: repeat(auto-fit, minmax(64px, auto));
69+
}
70+
`;
4871
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {
2+
html,
3+
LitElement,
4+
TemplateResult,
5+
property,
6+
customElement,
7+
state,
8+
} from 'lit-element';
9+
10+
import '../../action-icon.js';
11+
import { identity } from '../../foundation.js';
12+
import {
13+
automationLogicalNode,
14+
controlLogicalNode,
15+
functionalLogicalNode,
16+
furtherPowerSystemEquipmentLogicalNode,
17+
generalLogicalNode,
18+
interfacingLogicalNode,
19+
measurementLogicalNode,
20+
nonElectricalLogicalNode,
21+
powerTransformerLogicalNode,
22+
protectionLogicalNode,
23+
protectionRelatedLogicalNode,
24+
qualityLogicalNode,
25+
supervisionLogicalNode,
26+
switchgearLogicalNode,
27+
systemLogicalNode,
28+
transformerLogicalNode,
29+
} from '../../icons/lnode.js';
30+
31+
export function getLNodeIcon(lNode: Element): TemplateResult {
32+
const lnClassGroup = lNode.getAttribute('lnClass')?.charAt(0) ?? '';
33+
return lnClassIcons[lnClassGroup] ?? systemLogicalNode;
34+
}
35+
36+
const lnClassIcons: Partial<Record<string, TemplateResult>> = {
37+
L: systemLogicalNode,
38+
A: automationLogicalNode,
39+
C: controlLogicalNode,
40+
F: functionalLogicalNode,
41+
G: generalLogicalNode,
42+
I: interfacingLogicalNode,
43+
K: nonElectricalLogicalNode,
44+
M: measurementLogicalNode,
45+
P: protectionLogicalNode,
46+
Q: qualityLogicalNode,
47+
R: protectionRelatedLogicalNode,
48+
S: supervisionLogicalNode,
49+
T: transformerLogicalNode,
50+
X: switchgearLogicalNode,
51+
Y: powerTransformerLogicalNode,
52+
Z: furtherPowerSystemEquipmentLogicalNode,
53+
};
54+
55+
/** Pane rendering `LNode` element with its children */
56+
@customElement('l-node-editor')
57+
export class LNodeEditor extends LitElement {
58+
/** The edited `LNode` element */
59+
@property({ attribute: false })
60+
element!: Element;
61+
@state()
62+
private get header(): string {
63+
const prefix = this.element.getAttribute('prefix') ?? '';
64+
const lnClass = this.element.getAttribute('lnClass');
65+
const lnInst = this.element.getAttribute('lnInst');
66+
67+
const header = this.missingIedReference
68+
? `${prefix} ${lnClass} ${lnInst}`
69+
: identity(this.element);
70+
71+
return typeof header === 'string' ? header : '';
72+
}
73+
@state()
74+
private get missingIedReference(): boolean {
75+
return this.element.getAttribute('iedName') === 'None' ?? false;
76+
}
77+
78+
render(): TemplateResult {
79+
return html`<action-icon
80+
label="${this.header}"
81+
?secondary=${this.missingIedReference}
82+
?highlighted=${this.missingIedReference}
83+
hideActions
84+
><mwc-icon slot="icon"
85+
>${getLNodeIcon(this.element)}</mwc-icon
86+
></action-icon
87+
>`;
88+
}
89+
}

0 commit comments

Comments
 (0)