Skip to content

Commit e34ab77

Browse files
committed
update
1 parent c8c04f1 commit e34ab77

File tree

5 files changed

+528
-12
lines changed

5 files changed

+528
-12
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ jobs:
1616
- name: Setup Node.js environment
1717
uses: actions/setup-node@v3
1818
with:
19-
node-version: "18.x"
20-
cache: "npm"
19+
node-version: "20.x"
20+
cache: "yarn"
2121

2222
- name: Install dependencies
23-
run: npm ci
23+
run: yarn
2424

2525
- name: Build and test frontend
26-
run: npm run build
26+
run: |
27+
yarn test --ci --runInBand
28+
yarn build

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
> Maintained by [Swiftmade OÜ](https://swiftmade.co)
44
> Forked from [NetSage Sankey Panel](https://github.com/netsage-project/netsage-sankey-panel)
55
6-
This is a panel plugin for generating Sankey diagrams in Grafana 7.0+. Sankey diagrams are good for visualizing flow data and the width of the flows will be proportionate to the selected metric.
6+
This is a panel plugin for generating Sankey diagrams in Grafana 8.0+. Sankey diagrams are good for visualizing flow data and the width of the flows will be proportionate to the selected metric.
7+
8+
## Requirements
9+
10+
- **Grafana 8.0.0 or later** is required for this plugin to work.
711

812
## What's Different in This Fork
913

@@ -24,8 +28,8 @@ The panel will draw links from the first column of data points, to the last in o
2428
```bash
2529
git clone https://github.com/swiftmade/grafana-sankey-panel
2630
cd grafana-sankey-panel
27-
npm install
28-
npm run build
31+
yarn install
32+
yarn build
2933
```
3034

3135
Copy the `dist` folder to your Grafana plugins directory, or use Docker:
@@ -37,10 +41,10 @@ docker-compose up
3741
### Development
3842

3943
```bash
40-
npm run dev # Watch mode for development
41-
npm run build # Production build
42-
npm test # Run tests
43-
npm run lint # Lint code
44+
yarn dev # Watch mode for development
45+
yarn build # Production build
46+
yarn test # Run tests
47+
yarn lint # Lint code
4448
```
4549

4650
## Customizing

src/SankeyPanel.test.tsx

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { SankeyPanel } from './SankeyPanel';
4+
import { FieldType, toDataFrame, LoadingState } from '@grafana/data';
5+
import { parseData } from './dataParser';
6+
7+
// Mock the parseData function
8+
jest.mock('./dataParser');
9+
10+
// Mock the Sankey component
11+
jest.mock('./components/Sankey', () => ({
12+
Sankey: ({ data, width, height }: any) => (
13+
<svg data-testid="sankey-mock" data-width={width} data-height={height}>
14+
{data && <text data-testid="has-data">has data</text>}
15+
</svg>
16+
),
17+
}));
18+
19+
// Mock useTheme2
20+
jest.mock('@grafana/ui', () => ({
21+
useTheme2: () => ({
22+
colors: {
23+
text: {
24+
primary: '#FFFFFF',
25+
},
26+
},
27+
}),
28+
}));
29+
30+
describe('SankeyPanel', () => {
31+
const mockData = {
32+
state: LoadingState.Done,
33+
series: [
34+
toDataFrame({
35+
fields: [
36+
{ name: 'source', type: FieldType.string, values: ['A', 'B'] },
37+
{ name: 'destination', type: FieldType.string, values: ['B', 'C'] },
38+
{ name: 'value', type: FieldType.number, values: [100, 200] },
39+
],
40+
}),
41+
],
42+
timeRange: {} as any,
43+
};
44+
45+
const defaultOptions = {
46+
monochrome: false,
47+
color: 'blue',
48+
textColor: 'white',
49+
nodeColor: 'grey',
50+
nodeWidth: 30,
51+
nodePadding: 30,
52+
iteration: 7,
53+
valueField: 'value',
54+
labelSize: 14,
55+
};
56+
57+
beforeEach(() => {
58+
jest.clearAllMocks();
59+
// Setup default mock implementation
60+
(parseData as jest.Mock).mockReturnValue([
61+
{
62+
nodes: [
63+
{ name: 'A', id: ['row0'] },
64+
{ name: 'B', id: ['row0', 'row1'] },
65+
{ name: 'C', id: ['row1'] },
66+
],
67+
links: [
68+
{
69+
source: 0,
70+
target: 1,
71+
value: 100,
72+
displayValue: '100',
73+
id: 'row0',
74+
color: '#018EDB',
75+
node0: 0,
76+
},
77+
{
78+
source: 1,
79+
target: 2,
80+
value: 200,
81+
displayValue: '200',
82+
id: 'row1',
83+
color: '#DB8500',
84+
node0: 1,
85+
},
86+
],
87+
},
88+
['source', 'destination', 'value'],
89+
[
90+
{ name: 'row0', display: 'A -> B' },
91+
{ name: 'row1', display: 'B -> C' },
92+
],
93+
{
94+
display: (val: number) => ({ text: val.toString(), suffix: '' }),
95+
},
96+
(color: string) => color,
97+
]);
98+
});
99+
100+
it('should render without crashing', () => {
101+
const { container } = render(
102+
<svg>
103+
<SankeyPanel data={mockData} options={defaultOptions} width={800} height={600} id="test-panel" />
104+
</svg>
105+
);
106+
107+
expect(container.querySelector('svg')).toBeInTheDocument();
108+
});
109+
110+
it('should call parseData with correct parameters', () => {
111+
render(<SankeyPanel data={mockData} options={defaultOptions} width={800} height={600} id="test-panel" />);
112+
113+
expect(parseData).toHaveBeenCalledWith(mockData, defaultOptions, defaultOptions.monochrome, defaultOptions.color);
114+
});
115+
116+
it('should pass correct props to Sankey component', () => {
117+
const { getByTestId } = render(
118+
<SankeyPanel data={mockData} options={defaultOptions} width={800} height={600} id="test-panel" />
119+
);
120+
121+
const sankeyMock = getByTestId('sankey-mock');
122+
expect(sankeyMock).toBeInTheDocument();
123+
expect(sankeyMock.getAttribute('data-width')).toBe('800');
124+
expect(sankeyMock.getAttribute('data-height')).toBe('600');
125+
expect(getByTestId('has-data')).toBeInTheDocument();
126+
});
127+
128+
it('should handle monochrome option', () => {
129+
const monochromeOptions = { ...defaultOptions, monochrome: true, color: 'dark-red' };
130+
render(<SankeyPanel data={mockData} options={monochromeOptions} width={800} height={600} id="test-panel" />);
131+
132+
expect(parseData).toHaveBeenCalledWith(mockData, monochromeOptions, true, 'dark-red');
133+
});
134+
135+
it('should handle parsing errors gracefully', () => {
136+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
137+
(parseData as jest.Mock).mockImplementation(() => {
138+
throw new Error('Parse error');
139+
});
140+
141+
const { container } = render(
142+
<SankeyPanel data={mockData} options={defaultOptions} width={800} height={600} id="test-panel" />
143+
);
144+
145+
expect(consoleErrorSpy).toHaveBeenCalledWith('parsing error: ', expect.any(Error));
146+
expect(container.querySelector('g')).toBeInTheDocument();
147+
148+
consoleErrorSpy.mockRestore();
149+
});
150+
151+
it('should handle different panel dimensions', () => {
152+
const { getByTestId, rerender } = render(
153+
<SankeyPanel data={mockData} options={defaultOptions} width={400} height={300} id="test-panel" />
154+
);
155+
156+
let sankeyMock = getByTestId('sankey-mock');
157+
expect(sankeyMock.getAttribute('data-width')).toBe('400');
158+
expect(sankeyMock.getAttribute('data-height')).toBe('300');
159+
160+
rerender(<SankeyPanel data={mockData} options={defaultOptions} width={1200} height={800} id="test-panel" />);
161+
162+
sankeyMock = getByTestId('sankey-mock');
163+
expect(sankeyMock.getAttribute('data-width')).toBe('1200');
164+
expect(sankeyMock.getAttribute('data-height')).toBe('800');
165+
});
166+
167+
it('should update when options change', () => {
168+
const { rerender } = render(
169+
<SankeyPanel data={mockData} options={defaultOptions} width={800} height={600} id="test-panel" />
170+
);
171+
172+
expect(parseData).toHaveBeenCalledTimes(1);
173+
174+
const newOptions = { ...defaultOptions, nodeWidth: 50 };
175+
rerender(<SankeyPanel data={mockData} options={newOptions} width={800} height={600} id="test-panel" />);
176+
177+
expect(parseData).toHaveBeenCalledTimes(2);
178+
});
179+
180+
it('should handle empty data', () => {
181+
const emptyData = {
182+
state: LoadingState.Done,
183+
series: [
184+
toDataFrame({
185+
fields: [
186+
{ name: 'source', type: FieldType.string, values: [] },
187+
{ name: 'destination', type: FieldType.string, values: [] },
188+
{ name: 'value', type: FieldType.number, values: [] },
189+
],
190+
}),
191+
],
192+
timeRange: {} as any,
193+
};
194+
195+
(parseData as jest.Mock).mockReturnValue([
196+
{ nodes: [], links: [] },
197+
['source', 'destination', 'value'],
198+
[],
199+
{ display: (val: number) => ({ text: val.toString(), suffix: '' }) },
200+
(color: string) => color,
201+
]);
202+
203+
const { container } = render(
204+
<SankeyPanel data={emptyData} options={defaultOptions} width={800} height={600} id="test-panel" />
205+
);
206+
207+
expect(container.querySelector('g')).toBeInTheDocument();
208+
});
209+
210+
it('should apply theme text color', () => {
211+
render(<SankeyPanel data={mockData} options={defaultOptions} width={800} height={600} id="test-panel" />);
212+
213+
// The component should use theme.colors.text.primary for textColor
214+
// This is verified by the mock returning '#FFFFFF'
215+
expect(parseData).toHaveBeenCalled();
216+
});
217+
218+
it('should pass custom node width and padding', () => {
219+
const customOptions = {
220+
...defaultOptions,
221+
nodeWidth: 50,
222+
nodePadding: 40,
223+
};
224+
225+
render(<SankeyPanel data={customOptions} options={customOptions} width={800} height={600} id="test-panel" />);
226+
227+
expect(parseData).toHaveBeenCalled();
228+
});
229+
});

src/SankeyPanel.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ export const SankeyPanel: React.FC<Props> = ({ options, data, width, height, id
2828
} catch (error) {
2929
console.error('parsing error: ', error);
3030
}
31+
3132
const displayNames = parsedData[1];
3233
const pluginData = parsedData[0];
3334
const rowDisplayNames = parsedData[2];
3435
const field = parsedData[3];
35-
const fixColor = parsedData[4];
36+
const fixColor = parsedData[4] || ((color: string) => color);
3637
// const textColor = fixColor(graphOptions.textColor);
3738
const textColor = theme.colors.text.primary;
3839
const nodeColor = fixColor(graphOptions.nodeColor);

0 commit comments

Comments
 (0)