Span Details: Two-column view (#112856)
* Span Details: Two-column view Fixes #108465 * Use different flow * Remove redundant comment * Fix resizing and background color * Clean up styles * Fix tests * Clean up * Update types * Revert i18n key changes * Clean up i18n keys
This commit is contained in:
-3
@@ -39,9 +39,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
padding: '0.25em 0.1em',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': {
|
||||
background: autoColor(theme, '#e8e8e8'),
|
||||
},
|
||||
}),
|
||||
headerLabel: css({
|
||||
width: '120px',
|
||||
|
||||
-3
@@ -38,9 +38,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
color: 'inherit',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'&:hover': {
|
||||
background: autoColor(theme, '#e8e8e8'),
|
||||
},
|
||||
}),
|
||||
AccordianLogsContent: css({
|
||||
label: 'AccordianLogsContent',
|
||||
|
||||
+1
-6
@@ -37,20 +37,15 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
AccordianReferences: css({
|
||||
label: 'AccordianReferences',
|
||||
position: 'relative',
|
||||
marginBottom: '0.25rem',
|
||||
}),
|
||||
AccordianReferencesHeader: css({
|
||||
label: 'AccordianReferencesHeader',
|
||||
color: 'inherit',
|
||||
display: 'block',
|
||||
padding: '0.25rem 0',
|
||||
'&:hover': {
|
||||
background: autoColor(theme, '#dadada'),
|
||||
},
|
||||
}),
|
||||
AccordianReferencesContent: css({
|
||||
label: 'AccordianReferencesContent',
|
||||
background: autoColor(theme, '#f0f0f0'),
|
||||
borderTop: `1px solid ${autoColor(theme, '#d8d8d8')}`,
|
||||
padding: '0.5rem 0.5rem 0.25rem 0.5rem',
|
||||
}),
|
||||
@@ -96,7 +91,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
debugLabel: css({
|
||||
margin: '0 5px 0 5px',
|
||||
'&::before': {
|
||||
color: '#bbb',
|
||||
color: autoColor(theme, '#666'),
|
||||
content: 'attr(data-label)',
|
||||
},
|
||||
}),
|
||||
|
||||
-2
@@ -32,8 +32,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
KeyValueTable: css({
|
||||
label: 'KeyValueTable',
|
||||
background: autoColor(theme, '#fff'),
|
||||
border: `1px solid ${autoColor(theme, '#ddd')}`,
|
||||
marginBottom: '0.5rem',
|
||||
maxHeight: '450px',
|
||||
overflow: 'auto',
|
||||
}),
|
||||
|
||||
+18
-3
@@ -1,14 +1,29 @@
|
||||
import { LinkModel } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
|
||||
type Props = {
|
||||
focusSpanLink: LinkModel;
|
||||
};
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
shareButton: css({
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
span: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function ShareSpanButton(props: Props) {
|
||||
const { focusSpanLink } = props;
|
||||
const { interpolatedParams, ...linkProps } = focusSpanLink ?? {};
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<span>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
@@ -29,7 +44,7 @@ export function ShareSpanButton(props: Props) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button variant="secondary" size="sm" icon="share-alt" fill="outline">
|
||||
<Button variant="secondary" size="sm" icon="share-alt" fill="outline" className={styles.shareButton}>
|
||||
<Trans i18nKey="explore.span-detail.share-span">Share</Trans>
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
+22
-11
@@ -1,3 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { CoreApp, TimeRange } from '@grafana/data';
|
||||
import { usePluginLinks } from '@grafana/runtime';
|
||||
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer';
|
||||
@@ -25,6 +27,10 @@ const timeRange = {
|
||||
to: new Date(1000),
|
||||
} as unknown as TimeRange;
|
||||
|
||||
function getContent(result: React.ReactElement) {
|
||||
return result.props.children.props.children[0];
|
||||
}
|
||||
|
||||
describe('getSpanDetailLinkButtons', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -55,8 +61,9 @@ describe('getSpanDetailLinkButtons', () => {
|
||||
app: CoreApp.Explore,
|
||||
});
|
||||
|
||||
expect(result.props.children).toHaveLength(1);
|
||||
expect(result.props.children[0].props.link.title).toBe('Logs for this span');
|
||||
const content = getContent(result);
|
||||
expect(content).toHaveLength(1);
|
||||
expect(content[0].props.spanLinkModel.linkModel.title).toBe('Logs for this span');
|
||||
});
|
||||
|
||||
it('should create profile link button when profiles link exists', () => {
|
||||
@@ -75,8 +82,9 @@ describe('getSpanDetailLinkButtons', () => {
|
||||
app: CoreApp.Dashboard,
|
||||
});
|
||||
|
||||
expect(result.props.children).toHaveLength(1);
|
||||
expect(result.props.children[0].props.link.title).toBe('Profiles for this span');
|
||||
const content = getContent(result);
|
||||
expect(content).toHaveLength(1);
|
||||
expect(content[0].props.spanLinkModel.linkModel.title).toBe('Profiles for this span');
|
||||
});
|
||||
|
||||
it('should create session link button when session link exists', () => {
|
||||
@@ -91,8 +99,9 @@ describe('getSpanDetailLinkButtons', () => {
|
||||
app: CoreApp.Explore,
|
||||
});
|
||||
|
||||
expect(result.props.children).toHaveLength(1);
|
||||
expect(result.props.children[0].props.link.title).toBe('Session for this span');
|
||||
const content = getContent(result);
|
||||
expect(content).toHaveLength(1);
|
||||
expect(content[0].props.spanLinkModel.linkModel.title).toBe('Session for this span');
|
||||
});
|
||||
|
||||
it('should create profile drilldown button when plugin link exists', () => {
|
||||
@@ -121,9 +130,10 @@ describe('getSpanDetailLinkButtons', () => {
|
||||
app: CoreApp.Explore,
|
||||
});
|
||||
|
||||
expect(result.props.children).toHaveLength(2);
|
||||
expect(result.props.children[0].props.link.title).toBe('Profiles for this span');
|
||||
expect(result.props.children[1].props.link.title).toBe('Open in Profiles Drilldown');
|
||||
const content = getContent(result);
|
||||
expect(content).toHaveLength(2);
|
||||
expect(content[0].props.spanLinkModel.linkModel.title).toBe('Profiles for this span');
|
||||
expect(content[1].props.spanLinkModel.linkModel.title).toBe('Open in Profiles Drilldown');
|
||||
});
|
||||
|
||||
it('should not create profile drilldown button when not in Explore', () => {
|
||||
@@ -152,8 +162,9 @@ describe('getSpanDetailLinkButtons', () => {
|
||||
app: CoreApp.Dashboard,
|
||||
});
|
||||
|
||||
expect(result.props.children).toHaveLength(1);
|
||||
expect(result.props.children[0].props.link.title).toBe('Profiles for this span');
|
||||
const content = getContent(result);
|
||||
expect(content).toHaveLength(1);
|
||||
expect(content[0].props.spanLinkModel.linkModel.title).toBe('Profiles for this span');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+67
-13
@@ -1,11 +1,20 @@
|
||||
import { css } from '@emotion/css';
|
||||
import * as React from 'react';
|
||||
|
||||
import { CoreApp, IconName, LinkModel, PluginExtensionPoints, RawTimeRange, TimeRange } from '@grafana/data';
|
||||
import {
|
||||
CoreApp,
|
||||
GrafanaTheme2,
|
||||
IconName,
|
||||
LinkModel,
|
||||
PluginExtensionPoints,
|
||||
RawTimeRange,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { config, locationService, reportInteraction, usePluginLinks } from '@grafana/runtime';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { DataLinkButton, Dropdown, Menu, ToolbarButton } from '@grafana/ui';
|
||||
import { Button, DataLinkButton, Dropdown, Menu, useStyles2 } from '@grafana/ui';
|
||||
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer';
|
||||
|
||||
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
|
||||
@@ -28,6 +37,7 @@ export type Props = {
|
||||
timeRange: TimeRange;
|
||||
createSpanLink?: SpanLinkFunc;
|
||||
app: CoreApp;
|
||||
shareButton?: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -51,9 +61,10 @@ const MAX_LINKS = 3;
|
||||
const ABSOLUTE_LINK_PATTERN = /^https?:\/\//i;
|
||||
|
||||
export const getSpanDetailLinkButtons = (props: Props) => {
|
||||
const { span, createSpanLink, traceToProfilesOptions, timeRange, datasourceType, app } = props;
|
||||
const { span, createSpanLink, traceToProfilesOptions, timeRange, datasourceType, app, shareButton } = props;
|
||||
|
||||
let linkToProfiles: SpanLinkDef | undefined;
|
||||
let content = shareButton ? <>{shareButton}</> : undefined;
|
||||
|
||||
if (createSpanLink) {
|
||||
const links = (createSpanLink(span) || [])
|
||||
@@ -112,23 +123,65 @@ export const getSpanDetailLinkButtons = (props: Props) => {
|
||||
});
|
||||
|
||||
if (links.length > MAX_LINKS) {
|
||||
return <DropDownMenu links={links}></DropDownMenu>;
|
||||
} else {
|
||||
return (
|
||||
content = (
|
||||
<>
|
||||
{links.map(({ linkModel, icon, className }, index) => (
|
||||
<DataLinkButton key={index} link={linkModel} buttonProps={{ icon, className }}></DataLinkButton>
|
||||
<DropDownMenu links={links}></DropDownMenu>
|
||||
{shareButton}
|
||||
</>
|
||||
);
|
||||
} else if (links.length > 0) {
|
||||
content = (
|
||||
<>
|
||||
{links.map((spanLinkModel, index) => (
|
||||
<SingleLinkButton spanLinkModel={spanLinkModel} key={index} />
|
||||
))}
|
||||
{shareButton}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <></>;
|
||||
if (!content) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={css({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDisplay: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '5px',
|
||||
})}
|
||||
>
|
||||
{content}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
function getResponsibleButtonStyles(theme: GrafanaTheme2) {
|
||||
return css({
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
span: { display: 'none' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const SingleLinkButton: React.FC<{ spanLinkModel: SpanLinkModel }> = ({ spanLinkModel }) => {
|
||||
const styles = useStyles2(getResponsibleButtonStyles);
|
||||
const { linkModel, icon, className } = spanLinkModel;
|
||||
return (
|
||||
<span className={styles}>
|
||||
<DataLinkButton link={linkModel} buttonProps={{ icon, className }}></DataLinkButton>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const DropDownMenu = ({ links }: { links: SpanLinkModel[] }) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [_, setIsOpen] = React.useState(false);
|
||||
const styles = useStyles2(getResponsibleButtonStyles);
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
@@ -144,14 +197,15 @@ const DropDownMenu = ({ links }: { links: SpanLinkModel[] }) => {
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu} placement="bottom-start" onVisibleChange={setIsOpen}>
|
||||
<ToolbarButton
|
||||
<Button
|
||||
variant="primary"
|
||||
icon="link"
|
||||
isOpen={isOpen}
|
||||
size="sm"
|
||||
className={styles}
|
||||
aria-label={t('explore.drop-down-menu.aria-label-links', 'Links')}
|
||||
>
|
||||
<Trans i18nKey="explore.drop-down-menu.links">Links</Trans>
|
||||
</ToolbarButton>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
+200
-103
@@ -14,7 +14,7 @@
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { SpanStatusCode } from '@opentelemetry/api';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
import {
|
||||
CoreApp,
|
||||
@@ -114,17 +114,32 @@ const useResourceAttributesExtensionLinks = ({
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
card: css({
|
||||
':not(:empty)': {
|
||||
border: '1px solid ' + theme.colors.border.weak,
|
||||
'&:hover': {
|
||||
border: '1px solid ' + theme.colors.border.strong,
|
||||
},
|
||||
},
|
||||
borderRadius: theme.shape.radius.md,
|
||||
margin: '6px',
|
||||
padding: '5px',
|
||||
}),
|
||||
header: css({
|
||||
label: 'SpanDetailHeader',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
gap: '0 1rem',
|
||||
marginBottom: '0.25rem',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
content: css({
|
||||
label: 'SpanDetailContent',
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
}),
|
||||
listWrapper: css({
|
||||
label: 'SpanDetailListWrapper',
|
||||
overflow: 'hidden',
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
@@ -133,13 +148,25 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
list: css({
|
||||
textAlign: 'left',
|
||||
}),
|
||||
spanDetailComponent: css({
|
||||
label: 'SpanDetailComponent',
|
||||
display: 'flex',
|
||||
flexDirection: 'column', // On bigger screens display attributes below service name
|
||||
}),
|
||||
serviceNameAndLinks: css({
|
||||
label: 'ServiceNameAndLinks',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
marginBottom: '16px',
|
||||
}),
|
||||
operationName: css({
|
||||
label: 'SpanDetailOperationName',
|
||||
margin: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: '50%',
|
||||
flexGrow: 0,
|
||||
flexGrow: 1,
|
||||
flexShrink: 0,
|
||||
}),
|
||||
AccordianWarnings: css({
|
||||
@@ -152,9 +179,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
label: 'AccordianWarningsHeader',
|
||||
background: autoColor(theme, '#fff7e6'),
|
||||
padding: '0.25rem 0.5rem',
|
||||
'&:hover': {
|
||||
background: autoColor(theme, '#ffe7ba'),
|
||||
},
|
||||
}),
|
||||
AccordianWarningsHeaderOpen: css({
|
||||
label: 'AccordianWarningsHeaderOpen',
|
||||
@@ -180,6 +204,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
letterSpacing: '0.25px',
|
||||
margin: '0.5em 0 -0.75em',
|
||||
textAlign: 'right',
|
||||
clear: 'both',
|
||||
}),
|
||||
debugLabel: css({
|
||||
label: 'debugLabel',
|
||||
@@ -314,6 +339,8 @@ export default function SpanDetail(props: SpanDetailProps) {
|
||||
: []),
|
||||
];
|
||||
|
||||
const mainContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
if (span.kind) {
|
||||
overviewItems.push({
|
||||
@@ -358,15 +385,6 @@ export default function SpanDetail(props: SpanDetailProps) {
|
||||
});
|
||||
}
|
||||
|
||||
const linksComponent = getSpanDetailLinkButtons({
|
||||
span,
|
||||
createSpanLink,
|
||||
datasourceType,
|
||||
traceToProfilesOptions,
|
||||
timeRange,
|
||||
app,
|
||||
});
|
||||
|
||||
const { interpolatedParams, ...focusSpanLink } = createFocusSpanLink(traceID, spanID);
|
||||
const resourceLinksGetter = useResourceAttributesExtensionLinks({
|
||||
process,
|
||||
@@ -376,102 +394,132 @@ export default function SpanDetail(props: SpanDetailProps) {
|
||||
timeRange,
|
||||
});
|
||||
|
||||
const linksComponent = getSpanDetailLinkButtons({
|
||||
span,
|
||||
createSpanLink,
|
||||
datasourceType,
|
||||
traceToProfilesOptions,
|
||||
timeRange,
|
||||
app,
|
||||
shareButton: <ShareSpanButton focusSpanLink={focusSpanLink} />,
|
||||
});
|
||||
|
||||
const listOfContentCards = [];
|
||||
|
||||
listOfContentCards.push(
|
||||
<AccordianKeyValues
|
||||
data={tags}
|
||||
label={t('explore.span-detail.label-span-attributes', 'Span attributes')}
|
||||
isOpen={isTagsOpen}
|
||||
linksGetter={resourceLinksGetter}
|
||||
onToggle={() => tagsToggle(spanID)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (process.tags) {
|
||||
listOfContentCards.push(
|
||||
<AccordianKeyValues
|
||||
data={process.tags}
|
||||
label={t('explore.span-detail.label-resource-attributes', 'Resource attributes')}
|
||||
linksGetter={resourceLinksGetter}
|
||||
isOpen={isProcessOpen}
|
||||
onToggle={() => processToggle(spanID)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (logs && logs.length > 0) {
|
||||
listOfContentCards.push(
|
||||
<AccordianLogs
|
||||
logs={logs}
|
||||
isOpen={logsState.isOpen}
|
||||
openedItems={logsState.openedItems}
|
||||
onToggle={() => logsToggle(spanID)}
|
||||
onItemToggle={(logItem) => logItemToggle(spanID, logItem)}
|
||||
timestamp={traceStartTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (warnings && warnings.length > 0) {
|
||||
listOfContentCards.push(
|
||||
<AccordianKeyValues
|
||||
data={warnings.map((warning) => ({
|
||||
key: '',
|
||||
value: warning,
|
||||
type: 'warning',
|
||||
}))}
|
||||
onlyValues={true}
|
||||
showSummary={false}
|
||||
showCountBadge={true}
|
||||
isOpen={isWarningsOpen}
|
||||
onToggle={() => warningsToggle(spanID)}
|
||||
label={t('explore.span-detail.label-warnings', 'Warnings')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (stackTraces?.length) {
|
||||
listOfContentCards.push(
|
||||
<AccordianKeyValues
|
||||
data={stackTraces.map((stackTrace) => ({
|
||||
key: '',
|
||||
value: stackTrace,
|
||||
type: 'code',
|
||||
}))}
|
||||
onlyValues={true}
|
||||
showSummary={false}
|
||||
showCountBadge={true}
|
||||
isOpen={isStackTracesOpen}
|
||||
onToggle={() => stackTracesToggle(spanID)}
|
||||
label={t('explore.span-detail.label-stack-trace', 'Stack trace')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (references && references.length > 0 && (references.length > 1 || references[0].refType !== 'CHILD_OF')) {
|
||||
listOfContentCards.push(
|
||||
<AccordianReferences
|
||||
data={references}
|
||||
isOpen={referencesState.isOpen}
|
||||
openedItems={referencesState.openedItems}
|
||||
onToggle={() => referencesToggle(spanID)}
|
||||
onItemToggle={(reference) => referenceItemToggle(spanID, reference)}
|
||||
createFocusSpanLink={createFocusSpanLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (span.tags.some((tag) => tag.key === pyroscopeProfileIdTagKey)) {
|
||||
listOfContentCards.push(
|
||||
<SpanFlameGraph
|
||||
span={span}
|
||||
timeZone={timeZone}
|
||||
traceFlameGraphs={traceFlameGraphs}
|
||||
setTraceFlameGraphs={setTraceFlameGraphs}
|
||||
traceToProfilesOptions={traceToProfilesOptions}
|
||||
setRedrawListView={setRedrawListView}
|
||||
traceDuration={traceDuration}
|
||||
traceName={traceName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="span-detail-component">
|
||||
<div data-testid="span-detail-component" ref={mainContainerRef} className={styles.spanDetailComponent}>
|
||||
<div className={styles.header}>
|
||||
<h6 className={styles.operationName} title={operationName}>
|
||||
{operationName}
|
||||
</h6>
|
||||
<div className={styles.serviceNameAndLinks}>
|
||||
<h6 className={styles.operationName} title={operationName}>
|
||||
{operationName}
|
||||
</h6>
|
||||
{linksComponent}
|
||||
</div>
|
||||
<div className={styles.listWrapper}>
|
||||
<LabeledList className={styles.list} divider={false} items={overviewItems} color={color} />
|
||||
</div>
|
||||
<ShareSpanButton focusSpanLink={focusSpanLink} />
|
||||
</div>
|
||||
<div className={styles.linkList}>{linksComponent}</div>
|
||||
<div className={styles.content}>
|
||||
<div>
|
||||
<AccordianKeyValues
|
||||
data={tags}
|
||||
label={t('explore.span-detail.label-span-attributes', 'Span attributes')}
|
||||
isOpen={isTagsOpen}
|
||||
linksGetter={resourceLinksGetter}
|
||||
onToggle={() => tagsToggle(spanID)}
|
||||
/>
|
||||
{process.tags && (
|
||||
<AccordianKeyValues
|
||||
data={process.tags}
|
||||
label={t('explore.span-detail.label-resource-attributes', 'Resource attributes')}
|
||||
linksGetter={resourceLinksGetter}
|
||||
isOpen={isProcessOpen}
|
||||
onToggle={() => processToggle(spanID)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{logs && logs.length > 0 && (
|
||||
<AccordianLogs
|
||||
logs={logs}
|
||||
isOpen={logsState.isOpen}
|
||||
openedItems={logsState.openedItems}
|
||||
onToggle={() => logsToggle(spanID)}
|
||||
onItemToggle={(logItem) => logItemToggle(spanID, logItem)}
|
||||
timestamp={traceStartTime}
|
||||
/>
|
||||
)}
|
||||
|
||||
{warnings && warnings.length > 0 && (
|
||||
<AccordianKeyValues
|
||||
data={warnings.map((warning) => ({
|
||||
key: '',
|
||||
value: warning,
|
||||
type: 'text',
|
||||
}))}
|
||||
showSummary={false}
|
||||
showCountBadge={true}
|
||||
isOpen={isWarningsOpen}
|
||||
onlyValues={true}
|
||||
onToggle={() => warningsToggle(spanID)}
|
||||
label={t('explore.span-detail.warnings', 'Warnings')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{stackTraces?.length ? (
|
||||
<AccordianKeyValues
|
||||
data={stackTraces.map((stackTrace) => ({
|
||||
key: '',
|
||||
value: stackTrace,
|
||||
type: 'code',
|
||||
}))}
|
||||
onlyValues={true}
|
||||
showSummary={false}
|
||||
showCountBadge={true}
|
||||
isOpen={isStackTracesOpen}
|
||||
onToggle={() => stackTracesToggle(spanID)}
|
||||
label={t('explore.span-detail.label-stack-trace', 'Stack trace')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{references && references.length > 0 && (references.length > 1 || references[0].refType !== 'CHILD_OF') && (
|
||||
<AccordianReferences
|
||||
data={references}
|
||||
isOpen={referencesState.isOpen}
|
||||
openedItems={referencesState.openedItems}
|
||||
onToggle={() => referencesToggle(spanID)}
|
||||
onItemToggle={(reference) => referenceItemToggle(spanID, reference)}
|
||||
createFocusSpanLink={createFocusSpanLink}
|
||||
/>
|
||||
)}
|
||||
{span.tags.some((tag) => tag.key === pyroscopeProfileIdTagKey) && (
|
||||
<SpanFlameGraph
|
||||
span={span}
|
||||
timeZone={timeZone}
|
||||
traceFlameGraphs={traceFlameGraphs}
|
||||
setTraceFlameGraphs={setTraceFlameGraphs}
|
||||
traceToProfilesOptions={traceToProfilesOptions}
|
||||
setRedrawListView={setRedrawListView}
|
||||
traceDuration={traceDuration}
|
||||
traceName={traceName}
|
||||
/>
|
||||
)}
|
||||
<CardsContainer listOfContentCards={listOfContentCards} mainContainerRef={mainContainerRef} />
|
||||
|
||||
<small className={styles.debugInfo}>
|
||||
{/* TODO: fix keyboard a11y */}
|
||||
@@ -507,3 +555,52 @@ export const getAbsoluteTime = (startTime: number, timeZone: TimeZone) => {
|
||||
const absoluteTime = match[1] ? match[1] : dateStr;
|
||||
return ` (${absoluteTime})`;
|
||||
};
|
||||
|
||||
const CardsContainer = ({
|
||||
listOfContentCards,
|
||||
mainContainerRef,
|
||||
}: {
|
||||
listOfContentCards: React.ReactNode[];
|
||||
mainContainerRef?: React.RefObject<HTMLDivElement>;
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const useTwoColumns =
|
||||
mainContainerRef && mainContainerRef.current && mainContainerRef.current.getBoundingClientRect().width > 1000;
|
||||
|
||||
if (useTwoColumns) {
|
||||
return (
|
||||
<>
|
||||
<div className={css({ float: 'left', width: '50%' })}>
|
||||
{listOfContentCards.map((card, index) =>
|
||||
index % 2 === 0 ? (
|
||||
<div className={styles.card} key={index}>
|
||||
{card}
|
||||
</div>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={css({ float: 'right', width: '50%' })}>
|
||||
{listOfContentCards.map((card, index) =>
|
||||
index % 2 === 1 ? (
|
||||
<div className={styles.card} key={index}>
|
||||
{card}
|
||||
</div>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css({ clear: 'both', width: '100%' })}>
|
||||
{listOfContentCards.map((card, index) => (
|
||||
<div className={styles.card} key={index}>
|
||||
{card}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7465,6 +7465,7 @@
|
||||
"label-resource-attributes": "Resource attributes",
|
||||
"label-span-attributes": "Span attributes",
|
||||
"label-stack-trace": "Stack trace",
|
||||
"label-warnings": "Warnings",
|
||||
"overview-items": {
|
||||
"label": {
|
||||
"child-count": "Child Count:",
|
||||
@@ -7473,8 +7474,7 @@
|
||||
"start-time": "Start Time:"
|
||||
}
|
||||
},
|
||||
"share-span": "Share",
|
||||
"warnings": "Warnings"
|
||||
"share-span": "Share"
|
||||
},
|
||||
"span-filters": {
|
||||
"aria-label-select-max-span-operator": "Select max span operator",
|
||||
|
||||
Reference in New Issue
Block a user