Skip to content

Commit 98d8cf2

Browse files
feat: managed resource graph adjustments
1 parent c5c6302 commit 98d8cf2

File tree

13 files changed

+256
-210
lines changed

13 files changed

+256
-210
lines changed

public/locales/en.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -297,14 +297,12 @@
297297
"Graphs": {
298298
"colorsProvider": "Provider",
299299
"colorsProviderConfig": "Provider Config",
300-
"colorizedTitle": "Visualize: ",
300+
"colorizedTitle": "Group by: ",
301+
"colorsFlux": "Flux",
301302
"loadingError": "Error loading graph data",
302303
"loadingGraph": "Loading graph data...",
303304
"noResources": "No resources to display"
304305
},
305-
"GraphsLegend": {
306-
"title": "Legend"
307-
},
308306
"validationErrors": {
309307
"required": "This field is required!",
310308
"properFormatting": "Use A-Z, a-z, 0-9, hyphen (-), and period (.), but note that whitespace (spaces, tabs, etc.) is not allowed for proper compatibility.",
@@ -335,7 +333,8 @@
335333
"ready": "Ready",
336334
"synced": "Synced",
337335
"healthy": "Healthy",
338-
"installed": "Installed"
336+
"installed": "Installed",
337+
"none": "None"
339338
},
340339
"errors": {
341340
"installError": "Install error",

src/components/Graphs/CustomNode.module.css

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
position: relative;
1010
font-family: var(--sapFontFamily);
1111
pointer-events: auto;
12+
color: var(--sapTextColor, #222);
1213
}
1314

1415
.nodeContent {
@@ -32,7 +33,7 @@
3233

3334
.nodeType {
3435
font-size: 12px;
35-
color: #888;
36+
color: var(--sapContent_LabelColor, #888);
3637
margin-top: 2px;
3738
}
3839

@@ -48,4 +49,12 @@
4849

4950
.handleHidden {
5051
visibility: hidden;
52+
}
53+
54+
:global([data-theme='dark']) .nodeContainer {
55+
color: #fff;
56+
}
57+
58+
:global([data-theme='dark']) .nodeType {
59+
color: rgba(255, 255, 255, 0.75);
5160
}
Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,49 @@
11
import React from 'react';
22
import { Button, Icon } from '@ui5/webcomponents-react';
3-
import StatusIcon from './StatusIcon';
3+
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
44
import styles from './CustomNode.module.css';
55
import { Handle, Position } from '@xyflow/react';
6+
import { useTranslation } from 'react-i18next';
67

78
export interface CustomNodeProps {
89
label: string;
910
type?: string;
1011
status: string;
12+
transitionTime?: string;
13+
statusMessage?: string;
1114
onYamlClick: () => void;
1215
}
1316

14-
const CustomNode: React.FC<CustomNodeProps> = ({ label, type, status, onYamlClick }) => (
15-
<div className={styles.nodeContainer}>
16-
<Handle type="target" position={Position.Top} className={styles.handleHidden} />
17-
<Handle type="source" position={Position.Bottom} className={styles.handleHidden} />
18-
<div className={styles.nodeContent}>
19-
<div className={styles.statusIcon}>
20-
<StatusIcon isOk={status === 'OK'} />
21-
</div>
22-
<div className={styles.nodeTextContainer}>
23-
<div className={styles.nodeLabel} title={label}>
24-
{label}
17+
const CustomNode: React.FC<CustomNodeProps> = ({ label, type, status, transitionTime, statusMessage, onYamlClick }) => {
18+
const { t } = useTranslation();
19+
return (
20+
<div className={styles.nodeContainer}>
21+
<Handle type="target" position={Position.Top} className={styles.handleHidden} />
22+
<Handle type="source" position={Position.Bottom} className={styles.handleHidden} />
23+
<div className={styles.nodeContent}>
24+
<div className={styles.statusIcon}>
25+
<ResourceStatusCell
26+
isOk={status === 'OK'}
27+
transitionTime={transitionTime ?? ''}
28+
positiveText={t('common.healthy')}
29+
negativeText={t('errors.notHealthy')}
30+
message={statusMessage}
31+
/>
32+
</div>
33+
<div className={styles.nodeTextContainer}>
34+
<div className={styles.nodeLabel} title={label}>
35+
{label}
36+
</div>
37+
{type && <div className={styles.nodeType}>{type}</div>}
2538
</div>
26-
{type && <div className={styles.nodeType}>{type}</div>}
39+
</div>
40+
<div className={styles.yamlButtonWrapper}>
41+
<Button design="Transparent" aria-label="YAML" title="YAML" onClick={onYamlClick}>
42+
<Icon name="document" design="Information" />
43+
</Button>
2744
</div>
2845
</div>
29-
<div className={styles.yamlButtonWrapper}>
30-
<Button design="Transparent" aria-label="YAML" title="YAML" onClick={onYamlClick}>
31-
<Icon name="document" design="Information" />
32-
</Button>
33-
</div>
34-
</div>
35-
);
46+
);
47+
};
3648

3749
export default CustomNode;

src/components/Graphs/Graph.module.css

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
.graphContainer {
22
display: flex;
33
height: 600px;
4-
border: 1px solid #ddd;
4+
border: 1px solid var(--sapList_BorderColor, #ddd);
55
border-radius: 8px;
66
overflow: hidden;
7-
background-color: #fafafa;
7+
background-color: var(--sapBackgroundColor, #fafafa);
88
font-family: var(--sapFontFamily);
99
}
1010

@@ -19,6 +19,7 @@
1919
display: flex;
2020
gap: 1rem;
2121
align-items: center;
22+
color: var(--sapTextColor, #222);
2223
}
2324

2425
.graphToolbar {
@@ -43,4 +44,30 @@
4344

4445
.colorizedTitle {
4546
font-weight: 500;
47+
color: var(--sapTextColor, #222);
48+
}
49+
50+
/* Remove the default fieldset frame when used for grouping only */
51+
.fieldsetReset {
52+
border: 0;
53+
margin: 0;
54+
padding: 0;
55+
min-inline-size: 0;
56+
}
57+
58+
/* React Flow Controls dark mode */
59+
:global([data-theme='dark'] .react-flow__controls) {
60+
background-color: rgba(28, 28, 28, 0.9);
61+
border: 1px solid rgba(255, 255, 255, 0.15);
62+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
63+
}
64+
65+
:global([data-theme='dark'] .react-flow__controls-button) {
66+
background: transparent;
67+
color: #fff;
68+
border-color: rgba(255, 255, 255, 0.25);
69+
}
70+
71+
:global([data-theme='dark'] .react-flow__controls-button:hover) {
72+
background: rgba(255, 255, 255, 0.08);
4673
}

src/components/Graphs/Graph.tsx

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React, { useState, useCallback, useMemo } from 'react';
2-
import { ReactFlow, Background, Controls, MarkerType, Node } from '@xyflow/react';
2+
import { ReactFlow, Background, Controls, MarkerType, Node, Panel } from '@xyflow/react';
33
import type { NodeProps } from '@xyflow/react';
44
import { RadioButton, FlexBox, FlexBoxAlignItems } from '@ui5/webcomponents-react';
55
import styles from './Graph.module.css';
66
import '@xyflow/react/dist/style.css';
7-
import { ManagedResourceItem, NodeData, ColorBy } from './types';
7+
import { NodeData, ColorBy } from './types';
88
import CustomNode from './CustomNode';
99
import { Legend, LegendItem } from './Legend';
1010
import { YamlViewDialog } from '../Yaml/YamlViewDialog';
@@ -13,20 +13,25 @@ import { stringify } from 'yaml';
1313
import { removeManagedFieldsProperty } from '../../utils/removeManagedFieldsProperty';
1414
import { useTranslation } from 'react-i18next';
1515
import { useGraph } from './useGraph';
16+
import { ManagedResourceItem } from '../../lib/shared/types';
17+
import { useTheme } from '../../hooks/useTheme';
1618

1719
const nodeTypes = {
1820
custom: (props: NodeProps<Node<NodeData, 'custom'>>) => (
1921
<CustomNode
2022
label={props.data.label}
2123
type={props.data.type}
2224
status={props.data.status}
25+
transitionTime={props.data.transitionTime}
26+
statusMessage={props.data.statusMessage}
2327
onYamlClick={() => props.data.onYamlClick(props.data.item)}
2428
/>
2529
),
2630
};
2731

2832
const Graph: React.FC = () => {
2933
const { t } = useTranslation();
34+
const { isDarkTheme } = useTheme();
3035
const [colorBy, setColorBy] = useState<ColorBy>('provider');
3136
const [yamlDialogOpen, setYamlDialogOpen] = useState(false);
3237
const [yamlResource, setYamlResource] = useState<ManagedResourceItem | null>(null);
@@ -51,11 +56,11 @@ const Graph: React.FC = () => {
5156

5257
const legendItems: LegendItem[] = useMemo(
5358
() =>
54-
Object.entries(colorMap).map(([name, color]) => ({
55-
name: name === 'default' ? 'default' : name,
56-
color,
57-
})),
58-
[colorMap],
59+
Object.entries(colorMap).map(([name, color]) => {
60+
const displayName = colorBy === 'flux' && (name === 'default' || !name) ? t('common.none') : name;
61+
return { name: displayName, color };
62+
}),
63+
[colorMap, colorBy, t],
5964
);
6065

6166
if (error) {
@@ -71,26 +76,10 @@ const Graph: React.FC = () => {
7176
}
7277

7378
return (
74-
<div className={styles.graphContainer}>
79+
<div className={styles.graphContainer} data-theme={isDarkTheme ? 'dark' : 'light'}>
7580
<div className={styles.graphColumn}>
76-
<div className={styles.graphHeader}>
77-
<FlexBox alignItems={FlexBoxAlignItems.Center} role="radiogroup">
78-
<span className={styles.colorizedTitle}>{t('Graphs.colorizedTitle')}</span>
79-
<RadioButton
80-
name="colorBy"
81-
text={t('Graphs.colorsProviderConfig')}
82-
checked={colorBy === 'provider'}
83-
onChange={() => setColorBy('provider')}
84-
/>
85-
<RadioButton
86-
name="colorBy"
87-
text={t('Graphs.colorsProvider')}
88-
checked={colorBy === 'source'}
89-
onChange={() => setColorBy('source')}
90-
/>
91-
</FlexBox>
92-
</div>
9381
<ReactFlow
82+
data-theme={isDarkTheme ? 'dark' : 'light'}
9483
nodes={nodes}
9584
edges={edges}
9685
nodeTypes={nodeTypes}
@@ -108,16 +97,45 @@ const Graph: React.FC = () => {
10897
zoomOnScroll={true}
10998
panOnDrag={true}
11099
>
111-
<Controls />
100+
<Controls showInteractive={false} />
112101
<Background />
102+
<Panel position="top-left">
103+
<FlexBox alignItems={FlexBoxAlignItems.Center} role="radiogroup">
104+
<fieldset className={styles.fieldsetReset}>
105+
<div className={styles.graphHeader}>
106+
<span className={styles.colorizedTitle}>{t('Graphs.colorizedTitle')}</span>
107+
<RadioButton
108+
name="colorBy"
109+
text={t('Graphs.colorsProviderConfig')}
110+
checked={colorBy === 'provider'}
111+
onChange={() => setColorBy('provider')}
112+
/>
113+
<RadioButton
114+
name="colorBy"
115+
text={t('Graphs.colorsProvider')}
116+
checked={colorBy === 'source'}
117+
onChange={() => setColorBy('source')}
118+
/>
119+
<RadioButton
120+
name="colorBy"
121+
text={t('Graphs.colorsFlux')}
122+
checked={colorBy === 'flux'}
123+
onChange={() => setColorBy('flux')}
124+
/>
125+
</div>
126+
</fieldset>
127+
</FlexBox>
128+
</Panel>
129+
<Panel position="top-right">
130+
<Legend legendItems={legendItems} />
131+
</Panel>
113132
</ReactFlow>
114133
</div>
115134
<YamlViewDialog
116135
isOpen={yamlDialogOpen}
117136
setIsOpen={setYamlDialogOpen}
118137
dialogContent={<YamlViewer yamlString={yamlString} filename={yamlFilename} />}
119138
/>
120-
<Legend legendItems={legendItems} />
121139
</div>
122140
);
123141
};

src/components/Graphs/Legend.module.css

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,32 @@
33
min-width: 240px;
44
max-width: 300px;
55
max-height: 280px;
6-
border: 1px solid #ccc;
6+
border: 1px solid var(--sapList_BorderColor, #ccc);
77
border-radius: 8px;
8-
background-color: #fff;
8+
background-color: var(--sapTile_Background, #fff);
99
margin: 1rem;
10-
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
10+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
1111
overflow: auto;
1212
align-self: flex-start;
13+
color: var(--sapTextColor, #222);
1314
}
1415

1516
.legendTitle {
1617
margin-bottom: 10px;
18+
color: var(--sapTitleColor, var(--sapTextColor, #222));
1719
}
1820

1921
.legendRow {
2022
display: flex;
2123
align-items: center;
2224
margin-bottom: 8px;
25+
color: var(--sapTextColor, #222);
2326
}
2427

2528
.legendColorBox {
2629
width: 16px;
2730
height: 16px;
2831
margin-right: 8px;
2932
border-radius: 3px;
30-
border: 1px solid #999;
33+
border: 1px solid var(--sapList_BorderColor, #999);
3134
}

src/components/Graphs/Legend.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import React from 'react';
22
import styles from './Legend.module.css';
3-
import { useTranslation } from 'react-i18next';
4-
53
export interface LegendItem {
64
name: string;
75
color: string;
@@ -12,11 +10,8 @@ interface LegendProps {
1210
}
1311

1412
export const Legend: React.FC<LegendProps> = ({ legendItems }) => {
15-
const { t } = useTranslation();
16-
1713
return (
1814
<div className={styles.legendContainer}>
19-
<h4 className={styles.legendTitle}>{t('GraphsLegend.title')}</h4>
2015
{legendItems.map(({ name, color }) => (
2116
<div key={name} className={styles.legendRow}>
2217
<div className={styles.legendColorBox} style={{ backgroundColor: color }} />

0 commit comments

Comments
 (0)