-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Homepage * Navigation and error checking * Several improvements * Card design * Add skeleton component for loading state * Light theme styling * Overall styling and responsiveness * Lazy load home page * Feature tracking * Remove preview badge * Move rockets * AttributePanelRows * Loading, error, empty states * Update url link * Improve styling * Improve skeleton styling * Fix cspell * Utils and styling * Update url * Update error messages and improvements * Reuse AttributePanel for duration * Update icon * Remove DurationAttributePanel file * Add AttributePanelRow * Update icon hover * Tests and improvements * Update slice to replace * Reuse comparison logic * Remove superfluous part of query * Only count error total if value is a number * Fix spellcheck * Init acc in reduce to 0
- Loading branch information
1 parent
85b88b3
commit 992e0c1
Showing
16 changed files
with
1,064 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
import React from 'react'; | ||
|
||
import { | ||
SceneComponentProps, | ||
SceneFlexLayout, | ||
sceneGraph, | ||
SceneObjectBase, | ||
SceneObjectState, | ||
SceneQueryRunner, | ||
} from '@grafana/scenes'; | ||
import { GrafanaTheme2, LoadingState } from '@grafana/data'; | ||
import { explorationDS, MetricFunction } from 'utils/shared'; | ||
import { LoadingStateScene } from 'components/states/LoadingState/LoadingStateScene'; | ||
import { useStyles2 } from '@grafana/ui'; | ||
import { css } from '@emotion/css'; | ||
import { MINI_PANEL_HEIGHT } from 'components/Explore/TracesByService/TracesByServiceScene'; | ||
import { AttributePanelScene } from './AttributePanelScene'; | ||
import Skeleton from 'react-loading-skeleton'; | ||
import { getErrorMessage, getNoDataMessage } from 'utils/utils'; | ||
import { getMinimumsForDuration, getYBuckets } from 'components/Explore/TracesByService/REDPanel'; | ||
|
||
export interface AttributePanelState extends SceneObjectState { | ||
panel?: SceneFlexLayout; | ||
query: { | ||
query: string; | ||
step?: string; | ||
}; | ||
title: string; | ||
type: MetricFunction; | ||
renderDurationPanel?: boolean; | ||
} | ||
|
||
export class AttributePanel extends SceneObjectBase<AttributePanelState> { | ||
constructor(state: AttributePanelState) { | ||
super({ | ||
$data: new SceneQueryRunner({ | ||
datasource: explorationDS, | ||
queries: [{ refId: 'A', queryType: 'traceql', tableType: 'spans', limit: 10, ...state.query }], | ||
}), | ||
...state, | ||
}); | ||
|
||
|
||
this.addActivationHandler(() => { | ||
const data = sceneGraph.getData(this); | ||
|
||
this._subs.add( | ||
data.subscribeToState((data) => { | ||
if (data.data?.state === LoadingState.Done) { | ||
if (data.data.series.length === 0 || data.data.series[0].length === 0) { | ||
this.setState({ | ||
panel: new SceneFlexLayout({ | ||
children: [ | ||
new AttributePanelScene({ | ||
message: getNoDataMessage(state.title.toLowerCase()), | ||
title: state.title, | ||
type: state.type, | ||
}), | ||
], | ||
}), | ||
}); | ||
} else if (data.data.series.length > 0) { | ||
if (state.type === 'errors' || state.renderDurationPanel) { | ||
this.setState({ | ||
panel: new SceneFlexLayout({ | ||
children: [ | ||
new AttributePanelScene({ | ||
series: data.data.series, | ||
title: state.title, | ||
type: state.type | ||
}), | ||
], | ||
}) | ||
}); | ||
} else { | ||
let yBuckets = getYBuckets(data.data?.series ?? []); | ||
if (yBuckets?.length) { | ||
const { minDuration } = getMinimumsForDuration(yBuckets); | ||
|
||
this.setState({ | ||
panel: new SceneFlexLayout({ | ||
children: [ | ||
new AttributePanel({ | ||
query: { | ||
query: `{nestedSetParent<0 && kind=server && duration > ${minDuration}}`, | ||
}, | ||
title: state.title, | ||
type: state.type, | ||
renderDurationPanel: true, | ||
}), | ||
], | ||
}) | ||
}); | ||
} | ||
} | ||
} | ||
} else if (data.data?.state === LoadingState.Error) { | ||
this.setState({ | ||
panel: new SceneFlexLayout({ | ||
children: [ | ||
new AttributePanelScene({ | ||
message: getErrorMessage(data), | ||
title: state.title, | ||
type: state.type, | ||
}), | ||
], | ||
}) | ||
}); | ||
} else if (data.data?.state === LoadingState.Loading || data.data?.state === LoadingState.Streaming) { | ||
this.setState({ | ||
panel: new SceneFlexLayout({ | ||
direction: 'column', | ||
maxHeight: MINI_PANEL_HEIGHT, | ||
height: MINI_PANEL_HEIGHT, | ||
children: [ | ||
new LoadingStateScene({ | ||
component: () => SkeletonComponent(), | ||
}), | ||
], | ||
}), | ||
}); | ||
} | ||
}) | ||
); | ||
}); | ||
} | ||
|
||
public static Component = ({ model }: SceneComponentProps<AttributePanel>) => { | ||
const { panel } = model.useState(); | ||
const styles = useStyles2(getStyles); | ||
|
||
if (!panel) { | ||
return; | ||
} | ||
|
||
return ( | ||
<div className={styles.container}> | ||
<panel.Component model={panel} /> | ||
</div> | ||
); | ||
}; | ||
} | ||
|
||
function getStyles() { | ||
return { | ||
container: css({ | ||
minWidth: '350px', | ||
width: '-webkit-fill-available', | ||
}), | ||
}; | ||
} | ||
|
||
export const SkeletonComponent = () => { | ||
const styles = useStyles2(getSkeletonStyles); | ||
|
||
return ( | ||
<div className={styles.container}> | ||
<div className={styles.title}> | ||
<Skeleton count={1} width={200} /> | ||
</div> | ||
<div className={styles.tracesContainer}> | ||
{[...Array(11)].map((_, i) => ( | ||
<div className={styles.row} key={i}> | ||
<div className={styles.rowLeft}> | ||
<Skeleton count={1} /> | ||
</div> | ||
<div className={styles.rowRight}> | ||
<Skeleton count={1} /> | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
function getSkeletonStyles(theme: GrafanaTheme2) { | ||
return { | ||
container: css({ | ||
border: `1px solid ${theme.isDark ? theme.colors.border.medium : theme.colors.border.weak}`, | ||
borderRadius: theme.spacing(0.5), | ||
marginBottom: theme.spacing(4), | ||
width: '100%', | ||
}), | ||
title: css({ | ||
color: theme.colors.text.secondary, | ||
backgroundColor: theme.colors.background.secondary, | ||
fontSize: '1.3rem', | ||
padding: `${theme.spacing(1.5)} ${theme.spacing(2)}`, | ||
textAlign: 'center', | ||
}), | ||
tracesContainer: css({ | ||
padding: `13px ${theme.spacing(2)}`, | ||
}), | ||
row: css({ | ||
display: 'flex', | ||
justifyContent: 'space-between', | ||
}), | ||
rowLeft: css({ | ||
margin: '7px 0', | ||
width: '150px', | ||
}), | ||
rowRight: css({ | ||
width: '50px', | ||
}), | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.