Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,14 +290,12 @@
"Graphs": {
"colorsProvider": "Provider",
"colorsProviderConfig": "Provider Config",
"colorizedTitle": "Visualize: ",
"colorizedTitle": "Group by: ",
"colorsFlux": "Flux",
"loadingError": "Error loading graph data",
"loadingGraph": "Loading graph data...",
"noResources": "No resources to display"
},
"GraphsLegend": {
"title": "Legend"
},
"validationErrors": {
"required": "This field is required!",
"properFormatting": "Use A-Z, a-z, 0-9, hyphen (-), and period (.), but note that whitespace (spaces, tabs, etc.) is not allowed for proper compatibility.",
Expand Down Expand Up @@ -328,7 +326,8 @@
"ready": "Ready",
"synced": "Synced",
"healthy": "Healthy",
"installed": "Installed"
"installed": "Installed",
"none": "None"
},
"errors": {
"installError": "Install error",
Expand Down
11 changes: 10 additions & 1 deletion src/components/Graphs/CustomNode.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
position: relative;
font-family: var(--sapFontFamily);
pointer-events: auto;
color: var(--sapTextColor, #222);
}

.nodeContent {
Expand All @@ -32,7 +33,7 @@

.nodeType {
font-size: 12px;
color: #888;
color: var(--sapContent_LabelColor, #888);
margin-top: 2px;
}

Expand All @@ -48,4 +49,12 @@

.handleHidden {
visibility: hidden;
}

:global([data-theme='dark']) .nodeContainer {
color: #fff;
}

:global([data-theme='dark']) .nodeType {
color: rgba(255, 255, 255, 0.75);
}
52 changes: 32 additions & 20 deletions src/components/Graphs/CustomNode.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,49 @@
import React from 'react';
import { Button, Icon } from '@ui5/webcomponents-react';
import StatusIcon from './StatusIcon';
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
import styles from './CustomNode.module.css';
import { Handle, Position } from '@xyflow/react';
import { useTranslation } from 'react-i18next';

export interface CustomNodeProps {
label: string;
type?: string;
status: string;
transitionTime?: string;
statusMessage?: string;
onYamlClick: () => void;
}

const CustomNode: React.FC<CustomNodeProps> = ({ label, type, status, onYamlClick }) => (
<div className={styles.nodeContainer}>
<Handle type="target" position={Position.Top} className={styles.handleHidden} />
<Handle type="source" position={Position.Bottom} className={styles.handleHidden} />
<div className={styles.nodeContent}>
<div className={styles.statusIcon}>
<StatusIcon isOk={status === 'OK'} />
</div>
<div className={styles.nodeTextContainer}>
<div className={styles.nodeLabel} title={label}>
{label}
const CustomNode: React.FC<CustomNodeProps> = ({ label, type, status, transitionTime, statusMessage, onYamlClick }) => {
const { t } = useTranslation();
return (
<div className={styles.nodeContainer}>
<Handle type="target" position={Position.Top} className={styles.handleHidden} />
<Handle type="source" position={Position.Bottom} className={styles.handleHidden} />
<div className={styles.nodeContent}>
<div className={styles.statusIcon}>
<ResourceStatusCell
isOk={status === 'OK'}
transitionTime={transitionTime ?? ''}
positiveText={t('common.healthy')}
negativeText={t('errors.notHealthy')}
message={statusMessage}
/>
</div>
<div className={styles.nodeTextContainer}>
<div className={styles.nodeLabel} title={label}>
{label}
</div>
{type && <div className={styles.nodeType}>{type}</div>}
</div>
{type && <div className={styles.nodeType}>{type}</div>}
</div>
<div className={styles.yamlButtonWrapper}>
<Button design="Transparent" aria-label="YAML" title="YAML" onClick={onYamlClick}>
<Icon name="document" design="Information" />
</Button>
</div>
</div>
<div className={styles.yamlButtonWrapper}>
<Button design="Transparent" aria-label="YAML" title="YAML" onClick={onYamlClick}>
<Icon name="document" design="Information" />
</Button>
</div>
</div>
);
);
};

export default CustomNode;
31 changes: 29 additions & 2 deletions src/components/Graphs/Graph.module.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
.graphContainer {
display: flex;
height: 600px;
border: 1px solid #ddd;
border: 1px solid var(--sapList_BorderColor, #ddd);
border-radius: 8px;
overflow: hidden;
background-color: #fafafa;
background-color: var(--sapBackgroundColor, #fafafa);
font-family: var(--sapFontFamily);
}

Expand All @@ -19,6 +19,7 @@
display: flex;
gap: 1rem;
align-items: center;
color: var(--sapTextColor, #222);
}

.graphToolbar {
Expand All @@ -43,4 +44,30 @@

.colorizedTitle {
font-weight: 500;
color: var(--sapTextColor, #222);
}

/* Remove the default fieldset frame when used for grouping only */
.fieldsetReset {
border: 0;
margin: 0;
padding: 0;
min-inline-size: 0;
}

/* React Flow Controls dark mode */
:global([data-theme='dark'] .react-flow__controls) {
background-color: rgba(28, 28, 28, 0.9);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}

:global([data-theme='dark'] .react-flow__controls-button) {
background: transparent;
color: #fff;
border-color: rgba(255, 255, 255, 0.25);
}

:global([data-theme='dark'] .react-flow__controls-button:hover) {
background: rgba(255, 255, 255, 0.08);
}
72 changes: 45 additions & 27 deletions src/components/Graphs/Graph.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useState, useCallback, useMemo } from 'react';
import { ReactFlow, Background, Controls, MarkerType, Node } from '@xyflow/react';
import { ReactFlow, Background, Controls, MarkerType, Node, Panel } from '@xyflow/react';
import type { NodeProps } from '@xyflow/react';
import { RadioButton, FlexBox, FlexBoxAlignItems } from '@ui5/webcomponents-react';
import styles from './Graph.module.css';
import '@xyflow/react/dist/style.css';
import { ManagedResourceItem, NodeData, ColorBy } from './types';
import { NodeData, ColorBy } from './types';
import CustomNode from './CustomNode';
import { Legend, LegendItem } from './Legend';
import { YamlViewDialog } from '../Yaml/YamlViewDialog';
Expand All @@ -13,20 +13,25 @@ import { stringify } from 'yaml';
import { removeManagedFieldsProperty } from '../../utils/removeManagedFieldsProperty';
import { useTranslation } from 'react-i18next';
import { useGraph } from './useGraph';
import { ManagedResourceItem } from '../../lib/shared/types';
import { useTheme } from '../../hooks/useTheme';

const nodeTypes = {
custom: (props: NodeProps<Node<NodeData, 'custom'>>) => (
<CustomNode
label={props.data.label}
type={props.data.type}
status={props.data.status}
transitionTime={props.data.transitionTime}
statusMessage={props.data.statusMessage}
onYamlClick={() => props.data.onYamlClick(props.data.item)}
/>
),
};

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

const legendItems: LegendItem[] = useMemo(
() =>
Object.entries(colorMap).map(([name, color]) => ({
name: name === 'default' ? 'default' : name,
color,
})),
[colorMap],
Object.entries(colorMap).map(([name, color]) => {
const displayName = colorBy === 'flux' && (name === 'default' || !name) ? t('common.none') : name;
return { name: displayName, color };
}),
[colorMap, colorBy, t],
);

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

return (
<div className={styles.graphContainer}>
<div className={styles.graphContainer} data-theme={isDarkTheme ? 'dark' : 'light'}>
<div className={styles.graphColumn}>
<div className={styles.graphHeader}>
<FlexBox alignItems={FlexBoxAlignItems.Center} role="radiogroup">
<span className={styles.colorizedTitle}>{t('Graphs.colorizedTitle')}</span>
<RadioButton
name="colorBy"
text={t('Graphs.colorsProviderConfig')}
checked={colorBy === 'provider'}
onChange={() => setColorBy('provider')}
/>
<RadioButton
name="colorBy"
text={t('Graphs.colorsProvider')}
checked={colorBy === 'source'}
onChange={() => setColorBy('source')}
/>
</FlexBox>
</div>
<ReactFlow
data-theme={isDarkTheme ? 'dark' : 'light'}
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
Expand All @@ -108,16 +97,45 @@ const Graph: React.FC = () => {
zoomOnScroll={true}
panOnDrag={true}
>
<Controls />
<Controls showInteractive={false} />
<Background />
<Panel position="top-left">
<FlexBox alignItems={FlexBoxAlignItems.Center} role="radiogroup">
<fieldset className={styles.fieldsetReset}>
<div className={styles.graphHeader}>
<span className={styles.colorizedTitle}>{t('Graphs.colorizedTitle')}</span>
<RadioButton
name="colorBy"
text={t('Graphs.colorsProviderConfig')}
checked={colorBy === 'provider'}
onChange={() => setColorBy('provider')}
/>
<RadioButton
name="colorBy"
text={t('Graphs.colorsProvider')}
checked={colorBy === 'source'}
onChange={() => setColorBy('source')}
/>
<RadioButton
name="colorBy"
text={t('Graphs.colorsFlux')}
checked={colorBy === 'flux'}
onChange={() => setColorBy('flux')}
/>
</div>
</fieldset>
</FlexBox>
</Panel>
<Panel position="top-right">
<Legend legendItems={legendItems} />
</Panel>
</ReactFlow>
</div>
<YamlViewDialog
isOpen={yamlDialogOpen}
setIsOpen={setYamlDialogOpen}
dialogContent={<YamlViewer yamlString={yamlString} filename={yamlFilename} />}
/>
<Legend legendItems={legendItems} />
</div>
);
};
Expand Down
11 changes: 7 additions & 4 deletions src/components/Graphs/Legend.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,32 @@
min-width: 240px;
max-width: 300px;
max-height: 280px;
border: 1px solid #ccc;
border: 1px solid var(--sapList_BorderColor, #ccc);
border-radius: 8px;
background-color: #fff;
background-color: var(--sapTile_Background, #fff);
margin: 1rem;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
overflow: auto;
align-self: flex-start;
color: var(--sapTextColor, #222);
}

.legendTitle {
margin-bottom: 10px;
color: var(--sapTitleColor, var(--sapTextColor, #222));
}

.legendRow {
display: flex;
align-items: center;
margin-bottom: 8px;
color: var(--sapTextColor, #222);
}

.legendColorBox {
width: 16px;
height: 16px;
margin-right: 8px;
border-radius: 3px;
border: 1px solid #999;
border: 1px solid var(--sapList_BorderColor, #999);
}
5 changes: 0 additions & 5 deletions src/components/Graphs/Legend.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React from 'react';
import styles from './Legend.module.css';
import { useTranslation } from 'react-i18next';

export interface LegendItem {
name: string;
color: string;
Expand All @@ -12,11 +10,8 @@ interface LegendProps {
}

export const Legend: React.FC<LegendProps> = ({ legendItems }) => {
const { t } = useTranslation();

return (
<div className={styles.legendContainer}>
<h4 className={styles.legendTitle}>{t('GraphsLegend.title')}</h4>
{legendItems.map(({ name, color }) => (
<div key={name} className={styles.legendRow}>
<div className={styles.legendColorBox} style={{ backgroundColor: color }} />
Expand Down
Loading
Loading