Skip to content
2 changes: 1 addition & 1 deletion public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@
"Graphs": {
"colorsProvider": "Provider",
"colorsProviderConfig": "Provider Config",
"colorizedTitle": "Group by: ",
"colorBy": "Color by",
"colorsFlux": "Flux",
"loadingError": "Error loading graph data",
"loadingGraph": "Loading graph data...",
Expand Down
31 changes: 4 additions & 27 deletions src/components/Graphs/Graph.module.css
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
.graphContainer {
display: flex;
height: 600px;
overflow: hidden;
padding: 2px;
font-family: var(--sapFontFamily);
}

.graphColumn {
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--sapTile_Background, #fff);
}

.graphHeader {
padding: 0.5rem;
.panelContent {
display: flex;
align-items: flex-start;
gap: 1rem;
align-items: center;
color: var(--sapTextColor, #222);
font-size: var(--sapFontSize);
}

.graphToolbar {
padding: 0.5rem;
display: flex;
gap: 1rem;
align-items: center;
}

.message {
Expand All @@ -40,20 +31,6 @@
color: #c00;
}

.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);
Expand Down
35 changes: 4 additions & 31 deletions src/components/Graphs/Graph.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useCallback, useMemo } from '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 { NodeData, ColorBy } from './types';
Expand Down Expand Up @@ -32,7 +32,7 @@ const Graph: React.FC = () => {
const { t } = useTranslation();
const { openInAside } = useSplitter();
const { isDarkTheme } = useTheme();
const [colorBy, setColorBy] = useState<ColorBy>('provider');
const [colorBy, setColorBy] = useState<ColorBy>('source');

const handleYamlClick = useCallback(
(item: ManagedResourceItem) => {
Expand Down Expand Up @@ -92,35 +92,8 @@ const Graph: React.FC = () => {
>
<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 position="top-right" className={styles.panelContent}>
<Legend legendItems={legendItems} colorBy={colorBy} onColorByChange={setColorBy} />
</Panel>
</ReactFlow>
</div>
Expand Down
56 changes: 45 additions & 11 deletions src/components/Graphs/Legend.module.css
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
.legendWrapper {
display: flex;
align-items: flex-start;
gap: 1rem;
}

.legendContainer {
padding: 1rem;
min-width: 220px;
max-width: 300px;
max-height: 280px;
min-width: 150px;
max-width: 250px;
max-height: 120px;
border: 1px solid var(--sapList_BorderColor, #ccc);
border-radius: 8px;
background-color: var(--sapTile_Background, #fff);
margin: 1rem;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
overflow: auto;
align-self: flex-start;
overflow-y: auto;
color: var(--sapTextColor, #222);
font-size: var(--sapFontSize);
}

.legendTitle {
margin-bottom: 10px;
color: var(--sapTitleColor, var(--sapTextColor, #222));
.colorFilterContainer {
padding: 1rem;
border: 1px solid var(--sapList_BorderColor, #ccc);
border-radius: 8px;
background-color: var(--sapTile_Background, #fff);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
color: var(--sapTextColor, #222);
font-size: var(--sapFontSize);
display: flex;
justify-content: center;
align-items: center;
height: 16px;
width: 16px;
}

.legendRow {
Expand All @@ -30,9 +44,29 @@
}

.legendColorBox {
width: 16px;
height: 16px;
width: 14px;
height: 14px;
margin-right: 8px;
border-radius: 3px;
border: 1px solid var(--sapList_BorderColor, #999);
}

.popoverContent {
padding: 0.5rem;
min-width: 150px;
}

.popoverHeader {
margin: 0 0 0.5rem 0;
padding: 0 0 0.5rem 0;
border-bottom: 1px solid var(--sapList_BorderColor, #e0e0e0);
font-size: var(--sapFontSize, 14px);
font-weight: 600;
color: var(--sapTextColor, #222);
}

.popoverButtonContainer {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
78 changes: 69 additions & 9 deletions src/components/Graphs/Legend.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,83 @@
import React from 'react';
import React, { useId, useState } from 'react';
import { Button, Popover } from '@ui5/webcomponents-react';
import { useTranslation } from 'react-i18next';
import { ColorBy } from './types';
import styles from './Legend.module.css';

export interface LegendItem {
name: string;
color: string;
}

interface LegendProps {
legendItems: LegendItem[];
colorBy: ColorBy;
onColorByChange: (colorBy: ColorBy) => void;
}

export const Legend: React.FC<LegendProps> = ({ legendItems }) => {
export const Legend: React.FC<LegendProps> = ({ legendItems, colorBy, onColorByChange }) => {
const { t } = useTranslation();
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
const colorButtonId = useId();

return (
<div className={styles.legendContainer}>
{legendItems.map(({ name, color }) => (
<div key={name} className={styles.legendRow}>
<div className={styles.legendColorBox} style={{ backgroundColor: color }} />
<span>{name}</span>
</div>
))}
<div className={styles.legendWrapper}>
<div className={styles.legendContainer}>
{legendItems.map(({ name, color }) => (
<div key={name} className={styles.legendRow}>
<div className={styles.legendColorBox} style={{ backgroundColor: color }} />
<span>{name}</span>
</div>
))}
</div>
<div className={styles.colorFilterContainer}>
<Popover
opener={colorButtonId}
open={colorPopoverOpen}
placement="Bottom"
onClose={() => setColorPopoverOpen(false)}
>
<div className={styles.popoverContent}>
<h4 className={styles.popoverHeader}>{t('Graphs.colorBy')}</h4>
<div className={styles.popoverButtonContainer}>
<Button
design={colorBy === 'source' ? 'Emphasized' : 'Default'}
onClick={() => {
onColorByChange('source');
setColorPopoverOpen(false);
}}
>
{t('Graphs.colorsProvider')}
</Button>
<Button
design={colorBy === 'provider' ? 'Emphasized' : 'Default'}
onClick={() => {
onColorByChange('provider');
setColorPopoverOpen(false);
}}
>
{t('Graphs.colorsProviderConfig')}
</Button>
<Button
design={colorBy === 'flux' ? 'Emphasized' : 'Default'}
onClick={() => {
onColorByChange('flux');
setColorPopoverOpen(false);
}}
>
{t('Graphs.colorsFlux')}
</Button>
</div>
</div>
</Popover>
<Button
id={colorButtonId}
design="Transparent"
icon="palette"
tooltip={t('Graphs.colorBy')}
onClick={() => setColorPopoverOpen(!colorPopoverOpen)}
/>
</div>
</div>
);
};
14 changes: 9 additions & 5 deletions src/components/Graphs/useGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useMemo, useEffect, useState } from 'react';
import { useApiResource, useProvidersConfigResource } from '../../lib/api/useApiResource';
import { ManagedResourcesRequest } from '../../lib/api/types/crossplane/listManagedResources';
import { resourcesInterval } from '../../lib/shared/constants';
import { Node, Edge, Position, MarkerType } from '@xyflow/react';
import { Node, Edge, Position } from '@xyflow/react';
import dagre from 'dagre';
import { NodeData, ColorBy } from './types';
import { buildTreeData, generateColorMap } from './graphUtils';
Expand All @@ -24,14 +24,18 @@ function buildGraph(
treeData.forEach((n) => {
const colorKey: string =
colorBy === 'source' ? n.providerType : colorBy === 'flux' ? (n.fluxName ?? 'default') : n.providerConfigName;
const borderColor = colorMap[colorKey] || '#ccc';
//some opacity for background
const backgroundColor = `${borderColor}08`;

const node: Node<NodeData> = {
id: n.id,
type: 'custom',
data: { ...n },
style: {
border: `2px solid ${colorMap[colorKey] || '#ccc'}`,
border: `2px solid ${borderColor}`,
borderRadius: 8,
backgroundColor: 'var(--sapTile_Background, #fff)',
backgroundColor,
width: nodeWidth,
height: nodeHeight,
},
Expand All @@ -53,7 +57,7 @@ function buildGraph(
id: `e-${n.parentId}-${n.id}`,
source: n.parentId,
target: n.id,
markerEnd: { type: MarkerType.ArrowClosed },
style: { strokeWidth: 2, stroke: '#888' },
});
}
n.extraRefs?.forEach((refId) => {
Expand All @@ -63,7 +67,7 @@ function buildGraph(
id: `e-${refId}-${n.id}`,
source: refId,
target: n.id,
markerEnd: { type: MarkerType.ArrowClosed },
style: { strokeWidth: 2, stroke: '#888' },
});
}
});
Expand Down
Loading