]>
Commit | Line | Data |
---|---|---|
e306af50 TL |
1 | import { AbstractControl } from '@angular/forms'; |
2 | ||
f67539c2 | 3 | import _ from 'lodash'; |
e306af50 TL |
4 | |
5 | import { CrushNode } from '../models/crush-node'; | |
6 | ||
7 | export class CrushNodeSelectionClass { | |
8 | private nodes: CrushNode[] = []; | |
f6b5b4d7 | 9 | private idTree: { [id: number]: CrushNode } = {}; |
e306af50 TL |
10 | private allDevices: string[] = []; |
11 | private controls: { | |
12 | root: AbstractControl; | |
13 | failure: AbstractControl; | |
14 | device: AbstractControl; | |
15 | }; | |
16 | ||
17 | buckets: CrushNode[] = []; | |
18 | failureDomains: { [type: string]: CrushNode[] } = {}; | |
19 | failureDomainKeys: string[] = []; | |
20 | devices: string[] = []; | |
21 | deviceCount = 0; | |
22 | ||
f6b5b4d7 TL |
23 | static searchFailureDomains( |
24 | nodes: CrushNode[], | |
25 | s: string | |
26 | ): { [failureDomain: string]: CrushNode[] } { | |
27 | return this.getFailureDomains(this.search(nodes, s)); | |
28 | } | |
29 | ||
30 | /** | |
31 | * Filters crush map for a node and it's tree. | |
32 | * The node name as provided in crush rules attribute item_name is supported. | |
33 | * This means that '$name~$deviceType' can be used and will result in a crush map | |
34 | * that only include buckets with the specified device in use as their leaf. | |
35 | */ | |
36 | static search(nodes: CrushNode[], s: string): CrushNode[] { | |
37 | const [search, deviceType] = s.split('~'); // Used inside item_name in crush rules | |
38 | const node = nodes.find((n) => ['name', 'id', 'type'].some((attr) => n[attr] === search)); | |
39 | if (!node) { | |
40 | return []; | |
41 | } | |
42 | nodes = this.getSubNodes(node, this.createIdTreeFromNodes(nodes)); | |
43 | if (deviceType) { | |
44 | nodes = this.filterNodesByDeviceType(nodes, deviceType); | |
45 | } | |
46 | return nodes; | |
47 | } | |
48 | ||
49 | static createIdTreeFromNodes(nodes: CrushNode[]): { [id: number]: CrushNode } { | |
50 | const idTree = {}; | |
51 | nodes.forEach((node) => { | |
52 | idTree[node.id] = node; | |
53 | }); | |
54 | return idTree; | |
55 | } | |
56 | ||
57 | static getSubNodes(node: CrushNode, idTree: { [id: number]: CrushNode }): CrushNode[] { | |
58 | let subNodes = [node]; // Includes parent node | |
59 | if (!node.children) { | |
60 | return subNodes; | |
61 | } | |
62 | node.children.forEach((id) => { | |
63 | const childNode = idTree[id]; | |
64 | subNodes = subNodes.concat(this.getSubNodes(childNode, idTree)); | |
65 | }); | |
66 | return subNodes; | |
67 | } | |
68 | ||
69 | static filterNodesByDeviceType(nodes: CrushNode[], deviceType: string): any { | |
70 | let doNotInclude = nodes | |
71 | .filter((n) => n.device_class && n.device_class !== deviceType) | |
72 | .map((n) => n.id); | |
73 | let foundNewNode: boolean; | |
74 | let childrenToRemove = doNotInclude; | |
75 | ||
76 | // Filters out all unwanted nodes | |
77 | do { | |
78 | foundNewNode = false; | |
79 | nodes = nodes.filter((n) => !doNotInclude.includes(n.id)); // Unwanted nodes | |
80 | // Find nodes where all children were filtered | |
81 | const toRemoveNext: number[] = []; | |
82 | nodes.forEach((n) => { | |
83 | if (n.children && n.children.every((id) => doNotInclude.includes(id))) { | |
84 | toRemoveNext.push(n.id); | |
85 | foundNewNode = true; | |
86 | } | |
87 | }); | |
88 | if (foundNewNode) { | |
89 | doNotInclude = toRemoveNext; // Reduces array length | |
90 | childrenToRemove = childrenToRemove.concat(toRemoveNext); | |
91 | } | |
92 | } while (foundNewNode); | |
93 | ||
94 | // Removes filtered out children in all left nodes with children | |
95 | nodes = _.cloneDeep(nodes); // Clone objects to not change original objects | |
96 | nodes = nodes.map((n) => { | |
97 | if (!n.children) { | |
98 | return n; | |
99 | } | |
100 | n.children = n.children.filter((id) => !childrenToRemove.includes(id)); | |
101 | return n; | |
102 | }); | |
103 | ||
104 | return nodes; | |
105 | } | |
106 | ||
107 | static getFailureDomains(nodes: CrushNode[]): { [failureDomain: string]: CrushNode[] } { | |
108 | const domains = {}; | |
109 | nodes.forEach((node) => { | |
110 | const type = node.type; | |
111 | if (!domains[type]) { | |
112 | domains[type] = []; | |
113 | } | |
114 | domains[type].push(node); | |
115 | }); | |
116 | return domains; | |
117 | } | |
118 | ||
e306af50 TL |
119 | initCrushNodeSelection( |
120 | nodes: CrushNode[], | |
121 | rootControl: AbstractControl, | |
122 | failureControl: AbstractControl, | |
123 | deviceControl: AbstractControl | |
124 | ) { | |
125 | this.nodes = nodes; | |
f6b5b4d7 | 126 | this.idTree = CrushNodeSelectionClass.createIdTreeFromNodes(nodes); |
e306af50 | 127 | nodes.forEach((node) => { |
f6b5b4d7 | 128 | this.idTree[node.id] = node; |
e306af50 TL |
129 | }); |
130 | this.buckets = _.sortBy( | |
131 | nodes.filter((n) => n.children), | |
132 | 'name' | |
133 | ); | |
134 | this.controls = { | |
135 | root: rootControl, | |
136 | failure: failureControl, | |
137 | device: deviceControl | |
138 | }; | |
139 | this.preSelectRoot(); | |
140 | this.controls.root.valueChanges.subscribe(() => this.onRootChange()); | |
141 | this.controls.failure.valueChanges.subscribe(() => this.onFailureDomainChange()); | |
142 | this.controls.device.valueChanges.subscribe(() => this.onDeviceChange()); | |
143 | } | |
144 | ||
145 | private preSelectRoot() { | |
146 | const rootNode = this.nodes.find((node) => node.type === 'root'); | |
147 | this.silentSet(this.controls.root, rootNode); | |
148 | this.onRootChange(); | |
149 | } | |
150 | ||
151 | private silentSet(control: AbstractControl, value: any) { | |
152 | control.setValue(value, { emitEvent: false }); | |
153 | } | |
154 | ||
155 | private onRootChange() { | |
f6b5b4d7 TL |
156 | const nodes = CrushNodeSelectionClass.getSubNodes(this.controls.root.value, this.idTree); |
157 | const domains = CrushNodeSelectionClass.getFailureDomains(nodes); | |
e306af50 TL |
158 | Object.keys(domains).forEach((type) => { |
159 | if (domains[type].length <= 1) { | |
160 | delete domains[type]; | |
161 | } | |
162 | }); | |
163 | this.failureDomains = domains; | |
164 | this.failureDomainKeys = Object.keys(domains).sort(); | |
165 | this.updateFailureDomain(); | |
166 | } | |
167 | ||
e306af50 TL |
168 | private updateFailureDomain() { |
169 | let failureDomain = this.getIncludedCustomValue( | |
170 | this.controls.failure, | |
171 | Object.keys(this.failureDomains) | |
172 | ); | |
173 | if (failureDomain === '') { | |
174 | failureDomain = this.setMostCommonDomain(this.controls.failure); | |
175 | } | |
176 | this.updateDevices(failureDomain); | |
177 | } | |
178 | ||
179 | private getIncludedCustomValue(control: AbstractControl, includedIn: string[]) { | |
180 | return control.dirty && includedIn.includes(control.value) ? control.value : ''; | |
181 | } | |
182 | ||
183 | private setMostCommonDomain(failureControl: AbstractControl): string { | |
184 | let winner = { n: 0, type: '' }; | |
185 | Object.keys(this.failureDomains).forEach((type) => { | |
186 | const n = this.failureDomains[type].length; | |
187 | if (winner.n < n) { | |
188 | winner = { n, type }; | |
189 | } | |
190 | }); | |
191 | this.silentSet(failureControl, winner.type); | |
192 | return winner.type; | |
193 | } | |
194 | ||
195 | private onFailureDomainChange() { | |
196 | this.updateDevices(); | |
197 | } | |
198 | ||
199 | private updateDevices(failureDomain: string = this.controls.failure.value) { | |
200 | const subNodes = _.flatten( | |
f6b5b4d7 TL |
201 | this.failureDomains[failureDomain].map((node) => |
202 | CrushNodeSelectionClass.getSubNodes(node, this.idTree) | |
203 | ) | |
e306af50 TL |
204 | ); |
205 | this.allDevices = subNodes.filter((n) => n.device_class).map((n) => n.device_class); | |
206 | this.devices = _.uniq(this.allDevices).sort(); | |
207 | const device = | |
208 | this.devices.length === 1 | |
209 | ? this.devices[0] | |
210 | : this.getIncludedCustomValue(this.controls.device, this.devices); | |
211 | this.silentSet(this.controls.device, device); | |
212 | this.onDeviceChange(device); | |
213 | } | |
214 | ||
215 | private onDeviceChange(deviceType: string = this.controls.device.value) { | |
216 | this.deviceCount = | |
217 | deviceType === '' | |
218 | ? this.allDevices.length | |
219 | : this.allDevices.filter((type) => type === deviceType).length; | |
220 | } | |
221 | } |