Skip to content

Commit

Permalink
Tests and improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
joey-grafana committed Jan 8, 2025
1 parent fe0c848 commit 2be0320
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 83 deletions.
61 changes: 61 additions & 0 deletions src/components/Home/AttributePanelRow.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { AttributePanelRow } from './AttributePanelRow';
import { locationService } from '@grafana/runtime';
import { MetricFunction } from 'utils/shared';

jest.mock('@grafana/runtime', () => ({
locationService: {
push: jest.fn(),
},
}));

jest.mock('utils/analytics', () => ({
reportAppInteraction: jest.fn(),
USER_EVENTS_ACTIONS: {
home: {
attribute_panel_item_clicked: 'attribute_panel_item_clicked',
},
},
USER_EVENTS_PAGES: {
home: 'home',
},
}));

describe('AttributePanelRow', () => {
const mockProps = {
index: 0,
type: 'errors' as MetricFunction,
label: 'Test Label',
labelTitle: 'Label Title',
value: 'Test Text',
valueTitle: 'Text Title',
url: '/test-url',
};

it('renders correctly with required props', () => {
render(<AttributePanelRow {...mockProps} />);

expect(screen.getByText(mockProps.labelTitle)).toBeInTheDocument();
expect(screen.getByText(mockProps.valueTitle)).toBeInTheDocument();
expect(screen.getByText(mockProps.label)).toBeInTheDocument();
expect(screen.getByText(mockProps.value)).toBeInTheDocument();
});

it('navigates to the correct URL on click', () => {
render(<AttributePanelRow {...mockProps} />);
const rowElement = screen.getByText(mockProps.label).closest('div');
fireEvent.click(rowElement!);
expect(locationService.push).toHaveBeenCalledWith(mockProps.url);
});

it('renders the row header only if index is 0', () => {
render(<AttributePanelRow {...mockProps} />);
expect(screen.getByText(mockProps.labelTitle)).toBeInTheDocument();
});

it('does not render the row header only if index is > 0', () => {
render(<AttributePanelRow {...{ ...mockProps, index: 1 }} />);
expect(screen.queryByText(mockProps.labelTitle)).not.toBeInTheDocument();
});
});
16 changes: 8 additions & 8 deletions src/components/Home/AttributePanelRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,32 @@ type Props = {
type: MetricFunction;
label: string;
labelTitle: string;
text: string;
textTitle: string;
value: string;
valueTitle: string;
url: string;
}

export const AttributePanelRow = (props: Props) => {
const { index, type, label, labelTitle, text, textTitle, url } = props;
const { index, type, label, labelTitle, value, valueTitle, url } = props;
const styles = useStyles2(getStyles);

return (
<div key={index}>
{index === 0 && (
<div className={styles.rowHeader}>
<span>{labelTitle}</span>
<span className={styles.rowHeaderText}>{textTitle}</span>
<span className={styles.valueTitle}>{valueTitle}</span>
</div>
)}

<div
className={styles.row}
key={index}
onClick={() => {
reportAppInteraction(USER_EVENTS_PAGES.home, USER_EVENTS_ACTIONS.home.attribute_panel_item_clicked, {
reportAppInteraction(USER_EVENTS_PAGES.home, USER_EVENTS_ACTIONS.home.panel_row_clicked, {
type,
index,
value: text
value
});
locationService.push(url);
}}
Expand All @@ -45,7 +45,7 @@ export const AttributePanelRow = (props: Props) => {

<div className={styles.action}>
<span className={styles.actionText}>
{text}
{value}
</span>
<Icon
className={styles.actionIcon}
Expand All @@ -67,7 +67,7 @@ function getStyles(theme: GrafanaTheme2) {
alignItems: 'center',
padding: `0 ${theme.spacing(2)} ${theme.spacing(1)} ${theme.spacing(2)}`,
}),
rowHeaderText: css({
valueTitle: css({
margin: '0 45px 0 0',
}),
row: css({
Expand Down
69 changes: 69 additions & 0 deletions src/components/Home/AttributePanelRows.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { AttributePanelRows } from './AttributePanelRows';
import { DataFrame, Field } from '@grafana/data';
import { MetricFunction } from 'utils/shared';

describe('AttributePanelRows', () => {
const createField = (name: string, values: any[], labels: Record<string, string> = {}) => ({
name,
values,
labels,
}) as Field;

const createDataFrame = (fields: Field[]) => ({
fields,
}) as DataFrame;

const dummySeries = [
createDataFrame([
createField('time', []),
createField('Test service 1', [10, 20], { 'resource.service.name': '"Test service 1"' }),
]),
createDataFrame([
createField('time', []),
createField('Test service 2', [15, 5], { 'resource.service.name': '"Test service 2"' }),
]),
];

const dummyDurationSeries = [
createDataFrame([
createField('traceIdHidden', ['trace-1', 'trace-2']),
createField('spanID', ['span-1', 'span-2']),
createField('traceName', ['Test name 1', 'Test name 2']),
createField('traceService', ['Test service 1', 'Test service 2']),
createField('duration', [3000, 500]),
]),
];

it('renders message if provided', () => {
const msg = 'No data available.';
render(<AttributePanelRows series={[]} type={'errors' as MetricFunction} message={msg} />);
expect(screen.getByText(msg)).toBeInTheDocument();
});

it('renders an empty container if no series or message is provided', () => {
render(<AttributePanelRows series={[]} type={'errors' as MetricFunction} />);
expect(screen.getByText('No series data')).toBeInTheDocument();
});

it('renders error rows sorted by total errors when type is "errors"', () => {
render(<AttributePanelRows series={dummySeries} type={'errors' as MetricFunction} />);

expect(screen.getAllByText('Total errors').length).toBe(1);

const labels = screen.getAllByText('Test service', { exact: false });
expect(labels[0].textContent).toContain('Test service 1');
expect(labels[1].textContent).toContain('Test service 2');
});

it('renders duration rows sorted by duration when type is not "errors"', () => {
render(<AttributePanelRows series={dummyDurationSeries} type={'duration' as MetricFunction} />);

expect(screen.getAllByText('Duration').length).toBe(1);

const labels = screen.getAllByText('Test', { exact: false });
expect(labels[0].textContent).toContain('Test service 1: Test name 1');
expect(labels[1].textContent).toContain('Test service 2: Test name 2');
});
});
140 changes: 66 additions & 74 deletions src/components/Home/AttributePanelRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,70 +16,6 @@ export const AttributePanelRows = (props: Props) => {
const { series, type, message } = props;
const styles = useStyles2(getStyles);

const getLabel = (df: DataFrame) => {
const valuesField = df.fields.find((f) => f.name !== 'time');
const labels = valuesField?.labels;
return labels?.['resource.service.name'].slice(1, -1) ?? 'Service name not found'; // remove quotes
}

const getLabelForDuration = (traceServiceField: Field | undefined, traceNameField: Field | undefined, index: number) => {
let label = '';
if (traceServiceField?.values[index]) {
label = traceServiceField.values[index];
}
if (traceNameField?.values[index]) {
label = label.length === 0 ? traceNameField.values[index] : `${label}: ${traceNameField.values[index]}`;
}
return label.length === 0 ? 'Trace service & name not found' : label;
}

const getUrl = (df: DataFrame) => {
const valuesField = df.fields.find((f) => f.name !== 'time');
const labels = valuesField?.labels;
const serviceName = labels?.['resource.service.name'].slice(1, -1) ?? 'Service name not found'; // remove quotes

if (serviceName) {
const params = {
'var-filters': `resource.service.name|=|${serviceName}`,
'var-metric': type,
}
const url = urlUtil.renderUrl(EXPLORATIONS_ROUTE, params);

return `${url}&var-filters=nestedSetParent|<|0`;
}
return '';
}

const getUrlForDuration = (traceId: string, spanIdField: Field | undefined, traceServiceField: Field | undefined, index: number) => {
if (!spanIdField || !spanIdField.values[index] || !traceServiceField || !traceServiceField.values[index]) {
console.error('SpanId or traceService not found');
return ROUTES.Explore;
}

const params = {
traceId,
spanId: spanIdField.values[index],
'var-filters': `resource.service.name|=|${traceServiceField.values[index]}`,
'var-metric': type,
}
const url = urlUtil.renderUrl(EXPLORATIONS_ROUTE, params);

return `${url}&var-filters=nestedSetParent|<|0`;
}

const getTotalErrs = (df: DataFrame) => {
const valuesField = df.fields.find((f) => f.name !== 'time');
return valuesField?.values?.reduce((x, acc) => x + acc) ?? 1;
}

const getDuration = (durationField: Field | undefined, index: number) => {
if (!durationField || !durationField.values) {
return 'Durations not found';
}

return formatDuration(durationField.values[index] / 1000);
}

if (message) {
return (
<div className={styles.container}>
Expand All @@ -97,6 +33,26 @@ export const AttributePanelRows = (props: Props) => {

if (series && series.length > 0) {
if (type === 'errors') {
const getLabel = (df: DataFrame) => {
const valuesField = df.fields.find((f) => f.name !== 'time');
return valuesField?.labels?.['resource.service.name'].slice(1, -1) /* remove quotes */ ?? 'Service name not found';
}

const getUrl = (df: DataFrame) => {
const serviceName = getLabel(df);
const params = {
'var-filters': `resource.service.name|=|${serviceName}`,
'var-metric': type,
}
const url = urlUtil.renderUrl(EXPLORATIONS_ROUTE, params);
return `${url}&var-filters=nestedSetParent|<|0`;
}

const getTotalErrs = (df: DataFrame) => {
const valuesField = df.fields.find((f) => f.name !== 'time');
return valuesField?.values?.reduce((x, acc) => x + acc) ?? 1;
}

return (
<div className={styles.container}>
{series
Expand All @@ -108,8 +64,8 @@ export const AttributePanelRows = (props: Props) => {
index={index}
label={getLabel(df)}
labelTitle='Service'
text={getTotalErrs(df)}
textTitle='Total errors'
value={getTotalErrs(df)}
valueTitle='Total errors'
url={getUrl(df)}
/>
</span>
Expand All @@ -118,16 +74,52 @@ export const AttributePanelRows = (props: Props) => {
);
}

const sortByField = series[0].fields.find((f) => f.name === 'duration');
if (sortByField && sortByField.values) {
const sortedByDuration = sortByField?.values.map((_, i) => i)?.sort((a, b) => sortByField?.values[b] - sortByField?.values[a]);
const durField = series[0].fields.find((f) => f.name === 'duration');
if (durField && durField.values) {
const sortedByDuration = durField?.values.map((_, i) => i)?.sort((a, b) => durField?.values[b] - durField?.values[a]);
const sortedFields = series[0].fields.map((f) => {
return {
...f,
values: sortedByDuration?.map((i) => f.values[i]),
};
});

const getLabel = (traceServiceField: Field | undefined, traceNameField: Field | undefined, index: number) => {
let label = '';
if (traceServiceField?.values[index]) {
label = traceServiceField.values[index];
}
if (traceNameField?.values[index]) {
label = label.length === 0 ? traceNameField.values[index] : `${label}: ${traceNameField.values[index]}`;
}
return label.length === 0 ? 'Trace service & name not found' : label;
}

const getUrl = (traceId: string, spanIdField: Field | undefined, traceServiceField: Field | undefined, index: number) => {
if (!spanIdField || !spanIdField.values[index] || !traceServiceField || !traceServiceField.values[index]) {
console.error('SpanId or traceService not found');
return ROUTES.Explore;
}

const params = {
traceId,
spanId: spanIdField.values[index],
'var-filters': `resource.service.name|=|${traceServiceField.values[index]}`,
'var-metric': type,
}
const url = urlUtil.renderUrl(EXPLORATIONS_ROUTE, params);

return `${url}&var-filters=nestedSetParent|<|0`;
}

const getDuration = (durationField: Field | undefined, index: number) => {
if (!durationField || !durationField.values) {
return 'Duration not found';
}

return formatDuration(durationField.values[index] / 1000);
}

const traceIdField = sortedFields.find((f) => f.name === 'traceIdHidden');
const spanIdField = sortedFields.find((f) => f.name === 'spanID');
const traceNameField = sortedFields.find((f) => f.name === 'traceName');
Expand All @@ -141,19 +133,19 @@ export const AttributePanelRows = (props: Props) => {
<AttributePanelRow
type={type}
index={index}
label={getLabelForDuration(traceServiceField, traceNameField, index)}
label={getLabel(traceServiceField, traceNameField, index)}
labelTitle='Trace'
text={getDuration(durationField, index)}
textTitle='Duration'
url={getUrlForDuration(traceId, spanIdField, traceServiceField, index)}
value={getDuration(durationField, index)}
valueTitle='Duration'
url={getUrl(traceId, spanIdField, traceServiceField, index)}
/>
</span>
))}
</div>
);
}
}
return <></>;
return <div className={styles.container}>No series data</div>;
}

function getStyles(theme: GrafanaTheme2) {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const USER_EVENTS_ACTIONS = {
},
[USER_EVENTS_PAGES.home]: {
homepage_initialized: 'homepage_initialized',
attribute_panel_item_clicked: 'attribute_panel_item_clicked',
panel_row_clicked: 'panel_row_clicked',
explore_traces_clicked: 'explore_traces_clicked',
read_documentation_clicked: 'read_documentation_clicked',
},
Expand Down

0 comments on commit 2be0320

Please sign in to comment.