@@ -33,10 +33,7 @@ import ChrisAPIClient from "../../api/chrisapiclient";
33
33
import { ThemeContext } from "../DarkTheme/useTheme" ;
34
34
35
35
// -------------- Actions & Slices --------------
36
- import {
37
- getSelectedPlugin ,
38
- setPluginInstancesAndSelectedPlugin ,
39
- } from "../../store/pluginInstance/pluginInstanceSlice" ;
36
+ import { getSelectedPlugin } from "../../store/pluginInstance/pluginInstanceSlice" ;
40
37
41
38
// -------------- Modals (AddNode, DeleteNode, Pipeline, etc.) --------------
42
39
import AddNodeConnect from "../AddNode/AddNode" ;
@@ -68,7 +65,7 @@ export interface FeedTreeProps {
68
65
changeLayout : ( ) => void ;
69
66
onNodeClick : ( node : TreeNodeDatum ) => void ;
70
67
addNodeLocally : ( instance : PluginInstance | PluginInstance [ ] ) => void ;
71
- removeNodeLocally : ( ids : number [ ] ) => void ; // <-- NEW
68
+ removeNodeLocally : ( ids : number [ ] ) => void ;
72
69
pluginInstances : PluginInstance [ ] ;
73
70
statuses : {
74
71
[ id : number ] : string ;
@@ -159,7 +156,7 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
159
156
onNodeClick,
160
157
addNodeLocally,
161
158
removeNodeLocally,
162
- pluginInstances ,
159
+
163
160
statuses,
164
161
feed,
165
162
} = props ;
@@ -177,6 +174,9 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
177
174
const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
178
175
const containerRef = useRef < HTMLDivElement > ( null ) ;
179
176
177
+ // Keep track if we've already centered the tree once
178
+ const initialRenderRef = useRef ( true ) ;
179
+
180
180
// Get container dimensions
181
181
const size = useSize ( containerRef ) ;
182
182
const width = size ?. width ;
@@ -196,15 +196,13 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
196
196
const orientation = state . switchState . orientation ;
197
197
198
198
// -------------- Redux: entire pluginInstances array & selected plugin --------------
199
-
200
199
const selectedPlugin = useAppSelector (
201
200
( store ) => store . instance . selectedPlugin ,
202
201
) ;
203
202
204
203
// -------------- 3) Pipeline creation mutation --------------
205
204
const [ api , contextHolder ] = notification . useNotification ( ) ;
206
205
207
- // 1) Remove onMutate, keep onSuccess/onError
208
206
const pipelineMutation = useMutation ( {
209
207
mutationFn : ( nodeToZip : PluginInstance ) => fetchPipeline ( nodeToZip ) ,
210
208
onSuccess : ( ) => {
@@ -219,30 +217,23 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
219
217
} ,
220
218
} ) ;
221
219
222
- // 2) Show "preparing" only if pipeline is found
223
220
const fetchPipeline = async ( pluginInst : PluginInstance ) => {
224
221
const client = ChrisAPIClient . getClient ( ) ;
225
-
226
222
// Attempt to find the pipeline
227
223
const pipelineList = await client . getPipelines ( { name : "zip v20240311" } ) ;
228
224
const pipelines = pipelineList . getItems ( ) ;
229
225
if ( ! pipelines || pipelines . length === 0 ) {
230
- // Throw error *before* showing the notification
231
226
throw new Error ( "The zip pipeline is not registered. Contact admin." ) ;
232
227
}
233
-
234
- // Only after confirming pipeline exists:
235
228
api . info ( {
236
229
message : "Preparing to initiate the zipping process..." ,
237
230
} ) ;
238
231
239
- // Then continue with normal logic
240
232
const pipeline = pipelines [ 0 ] ;
241
233
const { id : pipelineId } = pipeline . data ;
242
234
243
235
const workflow = await client . createWorkflow (
244
- pipelineId ,
245
- // @ts -ignore: ignoring the mismatch if any
236
+ pipelineId , // @ts -ignore
246
237
{
247
238
previous_plugin_inst_id : pluginInst . data . id ,
248
239
} ,
@@ -253,7 +244,6 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
253
244
} ) ;
254
245
const newItems = pluginInstancesResponse . getItems ( ) ;
255
246
256
- // Merge new items into local state
257
247
if ( newItems && newItems . length > 0 ) {
258
248
const firstInstance = newItems [ newItems . length - 1 ] ;
259
249
dispatch ( getSelectedPlugin ( firstInstance ) ) ;
@@ -264,15 +254,13 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
264
254
} ;
265
255
266
256
// -------------- 1) Build a D3 tree layout in memory --------------
267
- // We'll create a "hierarchy" and compute x,y for each node. Then optionally center root.
268
257
const d3 = React . useMemo ( ( ) => {
269
258
if ( ! data )
270
259
return {
271
260
nodes : [ ] as HierarchyPointNode < TreeNodeDatum > [ ] ,
272
261
links : [ ] as HierarchyPointLink < TreeNodeDatum > [ ] ,
273
262
} ;
274
263
275
- // Build a D3 tree with nodeSize & orientation
276
264
const d3Tree = tree < TreeNodeDatum > ( )
277
265
. nodeSize (
278
266
orientation === "horizontal"
@@ -285,21 +273,18 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
285
273
: SEPARATION . nonSiblings ,
286
274
) ;
287
275
288
- // Convert root data -> hierarchical layout
289
276
const root = hierarchy ( data , ( d ) => d . children ) ;
290
- const layoutRoot = d3Tree ( root ) ; // <-- sets x, y on each node
277
+ const layoutRoot = d3Tree ( root ) ;
291
278
const computedNodes = layoutRoot . descendants ( ) ;
292
279
const computedLinks = layoutRoot . links ( ) ;
293
280
294
- // 3) If you want to add "ts" cross- links (tsIds)
281
+ // Add any extra TS links if needed
295
282
const newLinks : HierarchyPointLink < TreeNodeDatum > [ ] = [ ] ;
296
283
if ( tsIds && Object . keys ( tsIds ) . length > 0 ) {
297
284
for ( const link of computedLinks ) {
298
285
const sourceId = link . source . data . id ;
299
286
const targetId = link . target . data . id ;
300
-
301
287
if ( tsIds [ targetId ] || tsIds [ sourceId ] ) {
302
- // We'll just do a naive approach to add dash links
303
288
const topologicalLink = tsIds [ targetId ] ? link . target : link . source ;
304
289
const parents = tsIds [ topologicalLink . data . id ] ;
305
290
if ( parents && parents . length > 0 ) {
@@ -330,10 +315,11 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
330
315
} ;
331
316
} , [ data , tsIds , orientation ] ) ;
332
317
333
- // -------------- 4 ) Bind d3-zoom to the canvas with the updated transform --------------
318
+ // -------------- 2 ) Bind d3-zoom, center only once on mount --------------
334
319
useEffect ( ( ) => {
335
320
if ( ! canvasRef . current || ! d3 . rootNode || ! width || ! height ) return ;
336
321
322
+ // We'll throttle the "zoom" event so we don't set state too often.
337
323
const handleZoom = throttle (
338
324
( event : D3ZoomEvent < HTMLCanvasElement , unknown > ) => {
339
325
setTransform ( {
@@ -352,24 +338,30 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
352
338
. scaleExtent ( [ SCALE_EXTENT . min , SCALE_EXTENT . max ] )
353
339
. on ( "zoom" , handleZoom ) ;
354
340
355
- const root = d3 . rootNode ; // HierarchyPointNode
356
- const centerX = width / 2 - root . x ;
357
- const centerY = height / 7 - root . y ;
341
+ // Attach the zoom behavior to the canvas
342
+ const selection = select ( canvasRef . current ) . call ( zoomBehavior ) ;
358
343
359
- select ( canvasRef . current )
360
- . call ( zoomBehavior )
361
- // Center the root node in the canvas
362
- . call (
344
+ // Only center on the *first* render
345
+ if ( initialRenderRef . current ) {
346
+ const root = d3 . rootNode ;
347
+ const centerX = width / 2 - root . x ;
348
+ const centerY = height / 7 - root . y ;
349
+
350
+ // Programmatically set the initial zoom/pan
351
+ selection . call (
363
352
zoomBehavior . transform ,
364
353
zoomIdentity . translate ( centerX , centerY ) . scale ( INITIAL_SCALE ) ,
365
354
) ;
366
355
356
+ initialRenderRef . current = false ;
357
+ }
358
+
367
359
return ( ) => {
368
- select ( canvasRef . current ) . on ( ".zoom" , null ) ;
360
+ selection . on ( ".zoom" , null ) ;
369
361
} ;
370
362
} , [ d3 . rootNode , width , height ] ) ;
371
363
372
- // -------------- 5 ) Draw the nodes/links onto the canvas --------------
364
+ // -------------- 3 ) Draw the tree on the canvas --------------
373
365
useEffect ( ( ) => {
374
366
const canvas = canvasRef . current ;
375
367
if ( ! canvas || ! width || ! height ) return ;
@@ -414,7 +406,6 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
414
406
overlayScale : state . overlayScale . enabled
415
407
? state . overlayScale . type
416
408
: undefined ,
417
-
418
409
selectedId : selectedPlugin ?. data . id ,
419
410
finalStatus,
420
411
} ) ;
@@ -436,7 +427,7 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
436
427
selectedPlugin ,
437
428
] ) ;
438
429
439
- // -------------- 6) Canvas click => left-click node selection --------------
430
+ // -------------- Canvas click => node hit test --------------
440
431
const handleCanvasClick = useCallback (
441
432
( evt : React . MouseEvent < HTMLCanvasElement > ) => {
442
433
if ( ! canvasRef . current ) return ;
@@ -451,7 +442,6 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
451
442
const zoomedY =
452
443
( mouseY * ratio - transform . y * ratio ) / ( transform . k * ratio ) ;
453
444
454
- // Circle hit test
455
445
for ( const node of d3 . nodes ) {
456
446
const dx = node . x - zoomedX ;
457
447
const dy = node . y - zoomedY ;
@@ -465,38 +455,29 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
465
455
[ transform , onNodeClick , d3 . nodes ] ,
466
456
) ;
467
457
458
+ // Utility to convert node coords -> screen coords for context menu
468
459
const getNodeScreenCoords = useCallback (
469
460
(
470
461
nodeX : number ,
471
462
nodeY : number ,
472
463
transform : { x : number ; y : number ; k : number } ,
473
464
containerRect : DOMRect ,
474
465
) => {
475
- // 1) Account for the zoom/pan transform
476
- // “canvasX” = transform.x + transform.k * nodeX
477
- // “canvasY” = transform.y + transform.k * nodeY
478
466
const canvasX = transform . x + transform . k * nodeX ;
479
467
const canvasY = transform . y + transform . k * nodeY ;
480
-
481
- // 2) Convert from “canvas space” to “page” coords
482
- // by adding the container’s bounding rect offsets
483
468
const screenX = containerRect . left + canvasX ;
484
469
const screenY = containerRect . top + canvasY ;
485
-
486
470
return { screenX, screenY } ;
487
471
} ,
488
472
[ ] ,
489
473
) ;
490
474
491
- // -------------- 7) Canvas contextmenu => open custom context menu --------------
475
+ // -------------- Canvas contextmenu => custom context menu --------------
492
476
const handleCanvasContextMenu = useCallback (
493
477
( evt : React . MouseEvent < HTMLCanvasElement > ) => {
494
478
evt . preventDefault ( ) ;
495
479
if ( ! canvasRef . current ) return ;
496
480
497
- // We'll still do the 'hit test' to see if user right-clicked a node
498
-
499
- // GEtting the mouse position relative to the canvas.
500
481
const rect = canvasRef . current . getBoundingClientRect ( ) ;
501
482
const mouseX = evt . clientX - rect . left ;
502
483
const mouseY = evt . clientY - rect . top ;
@@ -513,29 +494,18 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
513
494
const dy = node . y - zoomedY ;
514
495
const dist = Math . sqrt ( dx * dx + dy * dy ) ;
515
496
if ( dist <= DEFAULT_NODE_RADIUS ) {
516
- foundNode = node . data ; // node.data is your “TreeNodeDatum”
497
+ foundNode = node . data ;
517
498
break ;
518
499
}
519
500
}
520
501
521
502
if ( foundNode ) {
522
- // We have a node. Now, instead of using evt.clientX, we want to get the
523
- // node’s on-screen coords, so the menu is adjacent to the node circle.
524
-
525
503
const containerRect = containerRef . current ?. getBoundingClientRect ( ) ;
526
504
if ( ! containerRect ) return ;
527
505
528
- // The node’s “layout” coords are in “foundNode.x, foundNode.y” inside your d3 Node
529
- // but we stored “foundNode” as node.data. So we need the actual node’s x,y from d3
530
- // If you have that in “(node.x, node.y)”, store it. For example, if your “foundNode” had that:
531
- // For this snippet we assume “foundNode.x, foundNode.y” are available, or you could store them.
532
- // If you only kept node.data, you need to keep the entire HierarchyPointNode instead.
533
-
534
- // This is the tricky part: we need the “HierarchyPointNode”.
535
- // Let’s say you keep it as nodeOfInterest: HierarchyPointNode<TreeNodeDatum>.
536
- // Then nodeOfInterest.x, nodeOfInterest.y are the layout coords.
537
- // For the sake of example:
538
- const nodeOfInterest = d3 . nodes . find ( ( n ) => n . data . id === foundNode . id ) ;
506
+ const nodeOfInterest = d3 . nodes . find (
507
+ ( n ) => n . data . id === foundNode ! . id ,
508
+ ) ;
539
509
if ( ! nodeOfInterest ) return ;
540
510
541
511
const { screenX, screenY } = getNodeScreenCoords (
@@ -545,23 +515,21 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
545
515
containerRect ,
546
516
) ;
547
517
548
- // + some offset to not overlap the circle
549
518
setContextMenuPosition ( {
550
519
x : screenX + 20 ,
551
520
y : screenY + 10 ,
552
521
visible : true ,
553
522
} ) ;
554
523
setContextMenuNode ( foundNode ) ;
555
524
} else {
556
- // user right-clicked empty space => maybe hide menu
557
525
setContextMenuNode ( null ) ;
558
526
setContextMenuPosition ( { x : 0 , y : 0 , visible : false } ) ;
559
527
}
560
528
} ,
561
529
[ transform , d3 . nodes , getNodeScreenCoords ] ,
562
530
) ;
563
531
564
- // -------------- 8) Handle toggles & orientation --------------
532
+ // -------------- Toggle handlers --------------
565
533
const handleChange = useCallback (
566
534
( feature : Feature , payload ?: any ) => {
567
535
updateState ( ( draft ) => {
@@ -589,19 +557,15 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
589
557
[ updateState ] ,
590
558
) ;
591
559
592
- // -------------- 9) Render --------------
593
560
return (
594
561
< div ref = { containerRef } style = { { width : "100%" , height : "100%" } } >
595
- { /* Notification context holder for pipeline creation */ }
596
562
{ contextHolder }
597
-
598
- { /* Modals for AddNode, DeleteNode, Pipeline, etc. */ }
599
563
< Modals
600
564
feed = { feed }
601
565
addNodeLocally = { addNodeLocally }
602
- removeNodeLocally = { removeNodeLocally } // <-- pass down to Modals
566
+ removeNodeLocally = { removeNodeLocally }
603
567
/>
604
- { /* Context menu (conditionally rendered) */ }
568
+
605
569
{ contextMenuPosition . visible && contextMenuNode && (
606
570
< div
607
571
style = { {
@@ -625,7 +589,7 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
625
589
</ div >
626
590
) }
627
591
628
- { /* Controls for labels, orientation, layout switch, scaling, search */ }
592
+ { /* Controls */ }
629
593
< div
630
594
className = "feed-tree__controls"
631
595
style = { { display : "flex" , gap : 10 , margin : 10 } }
@@ -712,49 +676,34 @@ export default function FeedTreeCanvas(props: FeedTreeProps) {
712
676
) ;
713
677
}
714
678
715
- // -------------- DRAWING FUNCTIONS --------------
716
-
717
- // A) Draw Link
718
-
719
- /**
720
- * Draw a simple straight line from parent->child,
721
- * then draw a small arrowhead near the child.
722
- */
679
+ // -------------- DRAWING UTILS --------------
723
680
function drawLink (
724
681
ctx : CanvasRenderingContext2D ,
725
682
linkData : HierarchyPointLink < TreeNodeDatum > ,
726
683
isDarkTheme : boolean ,
727
684
) {
728
685
const { source, target } = linkData ;
729
686
const nodeRadius = DEFAULT_NODE_RADIUS ;
730
-
731
- // If target is a "ts" plugin => dashed line
732
687
const isTs = target . data . item . data . plugin_type === "ts" ;
733
688
734
- // offset line so it doesn’t overlap the circle radius
735
689
const dx = target . x - source . x ;
736
690
const dy = target . y - source . y ;
737
691
const dist = Math . sqrt ( dx * dx + dy * dy ) ;
738
692
if ( dist === 0 ) return ;
739
693
740
- // unit direction from parent->child
741
694
const nx = dx / dist ;
742
695
const ny = dy / dist ;
743
696
744
- // line start, offset by nodeRadius
745
697
const sourceX = source . x + nodeRadius * nx ;
746
698
const sourceY = source . y + nodeRadius * ny ;
747
-
748
- // line end, offset behind the child’s node
749
- const childOffset = nodeRadius + 4 ; // extra 4 so the arrow isn't too close
699
+ const childOffset = nodeRadius + 4 ;
750
700
const targetX = target . x - childOffset * nx ;
751
701
const targetY = target . y - childOffset * ny ;
752
702
753
- // 1) Draw the line
754
703
ctx . save ( ) ;
755
704
ctx . beginPath ( ) ;
756
705
ctx . strokeStyle = isDarkTheme ? "#F2F9F9" : "#6A6E73" ;
757
- ctx . lineWidth = 0.5 ; // thinner lines
706
+ ctx . lineWidth = 0.5 ;
758
707
if ( isTs ) {
759
708
ctx . setLineDash ( [ 4 , 2 ] ) ;
760
709
} else {
@@ -764,17 +713,10 @@ function drawLink(
764
713
ctx . lineTo ( targetX , targetY ) ;
765
714
ctx . stroke ( ) ;
766
715
767
- // 2) Draw an arrowhead at the child end
768
- // We'll define a small helper to do so:
769
716
drawArrowHead ( ctx , sourceX , sourceY , targetX , targetY ) ;
770
-
771
717
ctx . restore ( ) ;
772
718
}
773
719
774
- /**
775
- * Draw a small arrowhead pointing from (x1,y1) -> (x2,y2).
776
- * We'll place the arrow tip exactly at (x2,y2).
777
- */
778
720
function drawArrowHead (
779
721
ctx : CanvasRenderingContext2D ,
780
722
x1 : number ,
@@ -783,14 +725,9 @@ function drawArrowHead(
783
725
y2 : number ,
784
726
arrowSize = 8 ,
785
727
) {
786
- // angle from parent->child
787
728
const angle = Math . atan2 ( y2 - y1 , x2 - x1 ) ;
788
-
789
729
ctx . beginPath ( ) ;
790
- // Move to the arrow tip
791
730
ctx . moveTo ( x2 , y2 ) ;
792
-
793
- // "wings" at angle ± some spread
794
731
ctx . lineTo (
795
732
x2 - arrowSize * Math . cos ( angle - Math . PI / 7 ) ,
796
733
y2 - arrowSize * Math . sin ( angle - Math . PI / 7 ) ,
@@ -799,13 +736,11 @@ function drawArrowHead(
799
736
x2 - arrowSize * Math . cos ( angle + Math . PI / 7 ) ,
800
737
y2 - arrowSize * Math . sin ( angle + Math . PI / 7 ) ,
801
738
) ;
802
-
803
739
ctx . closePath ( ) ;
804
- // fill using the same color as the link stroke
805
- ctx . fillStyle = ctx . strokeStyle ;
740
+ ctx . fillStyle = ctx . strokeStyle as string ;
806
741
ctx . fill ( ) ;
807
742
}
808
- // B) Draw Node
743
+
809
744
interface DrawNodeOptions {
810
745
ctx : CanvasRenderingContext2D ;
811
746
node : HierarchyPointNode < TreeNodeDatum > ;
@@ -817,10 +752,6 @@ interface DrawNodeOptions {
817
752
finalStatus : string | undefined ;
818
753
}
819
754
820
- /**
821
- * Replicates Node logic: color by status, highlight if selected or if search hits,
822
- * optional overlay scaling, label toggles, parent error => notExecuted
823
- */
824
755
function drawNode ( {
825
756
ctx,
826
757
node,
@@ -835,13 +766,9 @@ function drawNode({
835
766
const data = node . data ;
836
767
const itemData = data . item . data ;
837
768
838
- // 1) Node color by status
839
769
const color = getStatusColor ( finalStatus , data , searchFilter ) ;
840
-
841
- // 2) If node is selected => highlight ring
842
770
const isSelected = selectedId === itemData . id ;
843
771
844
- // 3) overlay scale factor (time, CPU, memory, etc.)
845
772
let factor = 1 ;
846
773
if ( overlayScale === "time" && itemData . start_date && itemData . end_date ) {
847
774
const start = new Date ( itemData . start_date ) . getTime ( ) ;
@@ -851,7 +778,6 @@ function drawNode({
851
778
if ( factor < 1 ) factor = 1 ;
852
779
}
853
780
854
- // 4) Main circle
855
781
ctx . save ( ) ;
856
782
ctx . beginPath ( ) ;
857
783
ctx . arc ( node . x , node . y , baseRadius , 0 , 2 * Math . PI ) ;
@@ -864,7 +790,6 @@ function drawNode({
864
790
ctx . stroke ( ) ;
865
791
}
866
792
867
- // 5) If factor > 1, draw outer ring
868
793
if ( factor > 1 ) {
869
794
ctx . beginPath ( ) ;
870
795
ctx . arc ( node . x , node . y , baseRadius * factor , 0 , 2 * Math . PI ) ;
@@ -873,7 +798,6 @@ function drawNode({
873
798
ctx . stroke ( ) ;
874
799
}
875
800
876
- // 6) Label if toggled or search highlight
877
801
const isSearchHit = color === "red" && searchFilter ;
878
802
if ( toggleLabel || isSearchHit ) {
879
803
ctx . fillStyle = isDarkTheme ? "#fff" : "#000" ;
@@ -885,17 +809,11 @@ function drawNode({
885
809
ctx . restore ( ) ;
886
810
}
887
811
888
- // -------------- Helper for node color --------------
889
812
function getStatusColor (
890
813
status : string | undefined ,
891
814
data : TreeNodeDatum ,
892
-
893
815
searchFilter : string ,
894
816
) : string {
895
- // Default color
896
- let color = "#F0AB00" ;
897
-
898
- // If searchFilter is present, highlight matching name/title in red
899
817
if ( searchFilter ) {
900
818
const term = searchFilter . toLowerCase ( ) ;
901
819
const pluginName = data . item . data . plugin_name ?. toLowerCase ( ) || "" ;
@@ -910,22 +828,15 @@ function getStatusColor(
910
828
case "scheduled" :
911
829
case "registeringFiles" :
912
830
case "created" :
913
- color = "#bee1f4" ;
914
- break ;
831
+ return "#bee1f4" ;
915
832
case "waiting" :
916
- color = "#aaa" ;
917
- break ;
833
+ return "#aaa" ;
918
834
case "finishedSuccessfully" :
919
- color = "#004080" ;
920
- break ;
835
+ return "#004080" ;
921
836
case "finishedWithError" :
922
837
case "cancelled" :
923
- color = "#c9190b" ;
924
- break ;
838
+ return "#c9190b" ;
925
839
default :
926
- color = "#004080" ; // fallback
927
- break ;
840
+ return "#F0AB00" ;
928
841
}
929
-
930
- return color ;
931
842
}
0 commit comments