mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Content Outline (#74536)
* Add images * Basic button functionality; TODO placeholders for dispatching contentOutlineToggle and rendering content outline component * Basic content outline container * Content outline toggles * Remove icon files from explore * Scroll into view v1 * outline that reflect's explore's order of vizs * Update icon name * Add scrollId to PanelChrome; scrolling enabled for Table * Add queries icon * Improve scroll behavior in split view * Add wrapper so the sticky navigation doesn't scroll when on the bottom of the window * Fix the issue with logs gap; center icons * Memoize register and unregister functions; adjust content height * Make displayOrderId optional * Use Node API for finding position of panels in content outline; add tooltip * Dock content outline in expanded mode; at tooltip to toggle button * Handle content outline visibility from Explore and not redux; pass outlineItems as a prop * Fix ContentOutline test * Add interaction tracking * Add padding to fix test * Replace string literals with objects for styles * Update event reporting payloads * Custom content outline button; content outline container improvements * Add aria-expanded to content outline button in ExploreToolbar * Fix vertical and horizontal scrolling * Add aria-controls * Remove unneccessary css since ExploreToolbar is sticky * Update feature toggles; Fix typos * Make content outline button more prominent in split mode; add padding to content outline items; * Diego's UX updates * WIP: some scroll fixes * Fix test and type error * Add id to ContentOutline to differentiate in split mode * No default exports --------- Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
parent
91cf4f0c1c
commit
4ec54bc2c3
@ -3770,10 +3770,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/features/explore/Explore.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"]
|
||||
"public/app/features/explore/ContentOutline/ContentOutline.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/explore/ExploreDrawer.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
|
@ -24,6 +24,7 @@ Some features are enabled by default. You can disable these feature by setting t
|
||||
| `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | |
|
||||
| `publicDashboards` | Enables public access to dashboards | Yes |
|
||||
| `featureHighlights` | Highlight Grafana Enterprise features | |
|
||||
| `exploreContentOutline` | Content outline sidebar | Yes |
|
||||
| `dataConnectionsConsole` | Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins. | Yes |
|
||||
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
|
||||
| `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | Yes |
|
||||
|
@ -30,6 +30,7 @@ export interface FeatureToggles {
|
||||
migrationLocking?: boolean;
|
||||
storage?: boolean;
|
||||
correlations?: boolean;
|
||||
exploreContentOutline?: boolean;
|
||||
datasourceQueryMultiStatus?: boolean;
|
||||
traceToMetrics?: boolean;
|
||||
newDBLibrary?: boolean;
|
||||
|
@ -86,6 +86,14 @@ var (
|
||||
Stage: FeatureStagePublicPreview,
|
||||
Owner: grafanaExploreSquad,
|
||||
},
|
||||
{
|
||||
Name: "exploreContentOutline",
|
||||
Description: "Content outline sidebar",
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Owner: grafanaExploreSquad,
|
||||
Expression: "true", // enabled by default
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "datasourceQueryMultiStatus",
|
||||
Description: "Introduce HTTP 207 Multi Status for api/ds/query",
|
||||
|
@ -11,6 +11,7 @@ featureHighlights,GA,@grafana/grafana-as-code,false,false,false,false
|
||||
migrationLocking,preview,@grafana/backend-platform,false,false,false,false
|
||||
storage,experimental,@grafana/grafana-app-platform-squad,false,false,false,false
|
||||
correlations,preview,@grafana/explore-squad,false,false,false,false
|
||||
exploreContentOutline,GA,@grafana/explore-squad,false,false,false,true
|
||||
datasourceQueryMultiStatus,experimental,@grafana/plugins-platform-backend,false,false,false,false
|
||||
traceToMetrics,experimental,@grafana/observability-traces-and-profiling,false,false,false,true
|
||||
newDBLibrary,preview,@grafana/backend-platform,false,false,false,false
|
||||
|
|
@ -55,6 +55,10 @@ const (
|
||||
// Correlations page
|
||||
FlagCorrelations = "correlations"
|
||||
|
||||
// FlagExploreContentOutline
|
||||
// Content outline sidebar
|
||||
FlagExploreContentOutline = "exploreContentOutline"
|
||||
|
||||
// FlagDatasourceQueryMultiStatus
|
||||
// Introduce HTTP 207 Multi Status for api/ds/query
|
||||
FlagDatasourceQueryMultiStatus = "datasourceQueryMultiStatus"
|
||||
|
@ -0,0 +1,70 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { ContentOutline } from './ContentOutline';
|
||||
|
||||
jest.mock('./ContentOutlineContext', () => ({
|
||||
useContentOutlineContext: jest.fn(),
|
||||
}));
|
||||
|
||||
const scrollIntoViewMock = jest.fn();
|
||||
const scrollerMock = document.createElement('div');
|
||||
|
||||
const setup = () => {
|
||||
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
|
||||
|
||||
scrollerMock.scroll = jest.fn();
|
||||
|
||||
// Mock useContentOutlineContext with custom outlineItems
|
||||
const mockUseContentOutlineContext = require('./ContentOutlineContext').useContentOutlineContext;
|
||||
mockUseContentOutlineContext.mockReturnValue({
|
||||
outlineItems: [
|
||||
{
|
||||
id: 'item-1',
|
||||
icon: 'test-icon',
|
||||
title: 'Item 1',
|
||||
ref: document.createElement('div'),
|
||||
},
|
||||
{
|
||||
id: 'item-2',
|
||||
icon: 'test-icon',
|
||||
title: 'Item 2',
|
||||
ref: document.createElement('div'),
|
||||
},
|
||||
],
|
||||
register: jest.fn(),
|
||||
unregister: jest.fn(),
|
||||
});
|
||||
|
||||
return render(<ContentOutline scroller={scrollerMock} panelId="content-outline-container-1" />);
|
||||
};
|
||||
|
||||
describe('<ContentOutline />', () => {
|
||||
beforeEach(() => {
|
||||
setup();
|
||||
});
|
||||
|
||||
it('toggles content on button click', () => {
|
||||
let showContentOutlineButton = screen.getByLabelText('Expand content outline');
|
||||
expect(showContentOutlineButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(showContentOutlineButton);
|
||||
const hideContentOutlineButton = screen.getByText('Collapse outline');
|
||||
expect(hideContentOutlineButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(hideContentOutlineButton);
|
||||
showContentOutlineButton = screen.getByLabelText('Expand content outline');
|
||||
expect(showContentOutlineButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('scrolls into view on content button click', () => {
|
||||
const itemButtons = screen.getAllByLabelText(/Item/i);
|
||||
|
||||
itemButtons.forEach((button) => {
|
||||
fireEvent.click(button);
|
||||
|
||||
//assert scrollIntoView is called
|
||||
expect(scrollerMock.scroll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,94 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { useStyles2, PanelContainer, CustomScrollbar } from '@grafana/ui';
|
||||
|
||||
import { useContentOutlineContext } from './ContentOutlineContext';
|
||||
import { ContentOutlineItemButton } from './ContentOutlineItemButton';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
wrapper: css({
|
||||
label: 'wrapper',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
marginRight: theme.spacing(1),
|
||||
height: '100%',
|
||||
backgroundColor: theme.colors.background.primary,
|
||||
}),
|
||||
content: css({
|
||||
label: 'content',
|
||||
top: 0,
|
||||
}),
|
||||
buttonStyles: css({
|
||||
textAlign: 'left',
|
||||
width: '100%',
|
||||
padding: theme.spacing(0, 1.5),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement | undefined; panelId: string }) {
|
||||
const [expanded, toggleExpanded] = useToggle(false);
|
||||
const styles = useStyles2((theme) => getStyles(theme));
|
||||
const { outlineItems } = useContentOutlineContext();
|
||||
|
||||
const scrollIntoView = (ref: HTMLElement | null, buttonTitle: string) => {
|
||||
let scrollValue = 0;
|
||||
let el: HTMLElement | null | undefined = ref;
|
||||
|
||||
do {
|
||||
scrollValue += el?.offsetTop || 0;
|
||||
el = el?.offsetParent as HTMLElement;
|
||||
} while (el && el !== scroller);
|
||||
|
||||
scroller?.scroll({
|
||||
top: scrollValue,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
||||
item: 'select_section',
|
||||
type: buttonTitle,
|
||||
});
|
||||
};
|
||||
|
||||
const toggle = () => {
|
||||
toggleExpanded();
|
||||
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
||||
item: 'outline',
|
||||
type: expanded ? 'minimize' : 'expand',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PanelContainer className={styles.wrapper} id={panelId}>
|
||||
<CustomScrollbar>
|
||||
<div className={styles.content}>
|
||||
<ContentOutlineItemButton
|
||||
title={expanded ? 'Collapse outline' : undefined}
|
||||
icon={expanded ? 'angle-left' : 'angle-right'}
|
||||
onClick={toggle}
|
||||
tooltip={!expanded ? 'Expand content outline' : undefined}
|
||||
className={styles.buttonStyles}
|
||||
aria-expanded={expanded}
|
||||
/>
|
||||
|
||||
{outlineItems.map((item) => (
|
||||
<ContentOutlineItemButton
|
||||
key={item.id}
|
||||
title={expanded ? item.title : undefined}
|
||||
className={styles.buttonStyles}
|
||||
icon={item.icon}
|
||||
onClick={() => scrollIntoView(item.ref, item.title)}
|
||||
tooltip={!expanded ? item.title : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</PanelContainer>
|
||||
);
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { useState, useContext, createContext, ReactNode, useCallback } from 'react';
|
||||
|
||||
import { ContentOutlineItemBaseProps } from './ContentOutlineItem';
|
||||
|
||||
export interface ContentOutlineItemContextProps extends ContentOutlineItemBaseProps {
|
||||
id: string;
|
||||
ref: HTMLElement | null;
|
||||
}
|
||||
|
||||
type RegisterFunction = ({ title, icon, ref }: Omit<ContentOutlineItemContextProps, 'id'>) => string;
|
||||
|
||||
interface ContentOutlineContextProps {
|
||||
outlineItems: ContentOutlineItemContextProps[];
|
||||
register: RegisterFunction;
|
||||
unregister: (id: string) => void;
|
||||
}
|
||||
|
||||
const ContentOutlineContext = createContext<ContentOutlineContextProps | undefined>(undefined);
|
||||
|
||||
export const ContentOutlineContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [outlineItems, setOutlineItems] = useState<ContentOutlineItemContextProps[]>([]);
|
||||
|
||||
const register: RegisterFunction = useCallback(({ title, icon, ref }) => {
|
||||
const id = uniqueId(`${title}-${icon}_`);
|
||||
|
||||
setOutlineItems((prevItems) => {
|
||||
const updatedItems = [...prevItems, { id, title, icon, ref }];
|
||||
|
||||
return updatedItems.sort((a, b) => {
|
||||
if (a.ref && b.ref) {
|
||||
const diff = a.ref.compareDocumentPosition(b.ref);
|
||||
if (diff === Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
return 1;
|
||||
} else if (diff === Node.DOCUMENT_POSITION_FOLLOWING) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
return id;
|
||||
}, []);
|
||||
|
||||
const unregister = useCallback((id: string) => {
|
||||
setOutlineItems((prevItems) => prevItems.filter((item) => item.id !== id));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ContentOutlineContext.Provider value={{ outlineItems, register, unregister }}>
|
||||
{children}
|
||||
</ContentOutlineContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useContentOutlineContext() {
|
||||
const ctx = useContext(ContentOutlineContext);
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('useContentOutlineContext must be used within a ContentOutlineContextProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import React, { useEffect, useRef, ReactNode } from 'react';
|
||||
|
||||
import { useContentOutlineContext } from './ContentOutlineContext';
|
||||
|
||||
export interface ContentOutlineItemBaseProps {
|
||||
title: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface ContentOutlineItemProps extends ContentOutlineItemBaseProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ContentOutlineItem({ title, icon, children, className }: ContentOutlineItemProps) {
|
||||
const { register, unregister } = useContentOutlineContext();
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// When the component mounts, register it and get its unique ID.
|
||||
const id = register({ title: title, icon: icon, ref: ref.current });
|
||||
|
||||
// When the component unmounts, unregister it using its unique ID.
|
||||
return () => unregister(id);
|
||||
}, [title, icon, register, unregister]);
|
||||
|
||||
return (
|
||||
<div className={className} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { cx, css } from '@emotion/css';
|
||||
import React, { ButtonHTMLAttributes } from 'react';
|
||||
|
||||
import { IconName, isIconName, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, useStyles2, Tooltip } from '@grafana/ui';
|
||||
|
||||
type CommonProps = {
|
||||
title?: string;
|
||||
icon: string;
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type ContentOutlineItemButtonProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export function ContentOutlineItemButton({ title, icon, tooltip, className, ...rest }: ContentOutlineItemButtonProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const buttonStyles = cx(styles.button, className);
|
||||
|
||||
const body = (
|
||||
<button className={buttonStyles} aria-label={tooltip} {...rest}>
|
||||
{renderIcon(icon)}
|
||||
{title}
|
||||
</button>
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<Tooltip content={tooltip} placement="bottom">
|
||||
{body}
|
||||
</Tooltip>
|
||||
) : (
|
||||
body
|
||||
);
|
||||
}
|
||||
|
||||
function renderIcon(icon: IconName | React.ReactNode) {
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isIconName(icon)) {
|
||||
return <Icon name={icon} size={'lg'} />;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
button: css({
|
||||
label: 'content-outline-item-button',
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
height: theme.spacing(theme.components.height.md),
|
||||
padding: theme.spacing(0, 1),
|
||||
gap: theme.spacing(1),
|
||||
color: theme.colors.text.secondary,
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': {
|
||||
color: theme.colors.text.primary,
|
||||
background: theme.colors.background.secondary,
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
@ -8,6 +8,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { getPluginLinkExtensions } from '@grafana/runtime';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { ContentOutlineContextProvider } from './ContentOutline/ContentOutlineContext';
|
||||
import { Explore, Props } from './Explore';
|
||||
import { initialExploreState } from './state/main';
|
||||
import { scanStopAction } from './state/query';
|
||||
@ -141,7 +142,9 @@ const setup = (overrideProps?: Partial<Props>) => {
|
||||
|
||||
return render(
|
||||
<TestProvider store={store}>
|
||||
<Explore {...exploreProps} />
|
||||
<ContentOutlineContextProvider>
|
||||
<Explore {...exploreProps} />
|
||||
</ContentOutlineContextProvider>
|
||||
</TestProvider>
|
||||
);
|
||||
};
|
||||
|
@ -35,6 +35,9 @@ import { StoreState } from 'app/types';
|
||||
|
||||
import { getTimeZone } from '../profile/state/selectors';
|
||||
|
||||
import { ContentOutline } from './ContentOutline/ContentOutline';
|
||||
import { ContentOutlineContextProvider } from './ContentOutline/ContentOutlineContext';
|
||||
import { ContentOutlineItem } from './ContentOutline/ContentOutlineItem';
|
||||
import { CorrelationHelper } from './CorrelationHelper';
|
||||
import { CustomContainer } from './CustomContainer';
|
||||
import ExploreQueryInspector from './ExploreQueryInspector';
|
||||
@ -69,30 +72,37 @@ import { updateTimeRange } from './state/time';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
exploreMain: css`
|
||||
label: exploreMain;
|
||||
exploreMain: css({
|
||||
label: 'exploreMain',
|
||||
// Is needed for some transition animations to work.
|
||||
position: relative;
|
||||
margin-top: 21px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
queryContainer: css`
|
||||
label: queryContainer;
|
||||
// Need to override normal css class and don't want to count on ordering of the classes in html.
|
||||
height: auto !important;
|
||||
flex: unset !important;
|
||||
display: unset !important;
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
exploreContainer: css`
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
padding: ${theme.spacing(2)};
|
||||
padding-top: 0;
|
||||
`,
|
||||
position: 'relative',
|
||||
marginTop: '21px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
queryContainer: css({
|
||||
label: 'queryContainer',
|
||||
padding: theme.spacing(1),
|
||||
}),
|
||||
exploreContainer: css({
|
||||
label: 'exploreContainer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingRight: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
left: css({
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
wrapper: css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: theme.spacing(2),
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -109,6 +119,7 @@ enum ExploreDrawer {
|
||||
|
||||
interface ExploreState {
|
||||
openDrawer?: ExploreDrawer;
|
||||
contentOutlineVisible: boolean;
|
||||
}
|
||||
|
||||
export type Props = ExploreProps & ConnectedProps<typeof connector>;
|
||||
@ -137,6 +148,7 @@ export type Props = ExploreProps & ConnectedProps<typeof connector>;
|
||||
* The result viewers determine some of the query options sent to the datasource, e.g.,
|
||||
* `format`, to indicate eventual transformations by the datasources' result transformers.
|
||||
*/
|
||||
|
||||
export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
scrollElement: HTMLDivElement | undefined;
|
||||
topOfViewRef = createRef<HTMLDivElement>();
|
||||
@ -148,6 +160,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
super(props);
|
||||
this.state = {
|
||||
openDrawer: undefined,
|
||||
contentOutlineVisible: false,
|
||||
};
|
||||
this.graphEventBus = props.eventBus.newScopedBus('graph', { onlyLocal: false });
|
||||
this.logsEventBus = props.eventBus.newScopedBus('logs', { onlyLocal: false });
|
||||
@ -174,6 +187,18 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
}
|
||||
};
|
||||
|
||||
onContentOutlineToogle = () => {
|
||||
this.setState((state) => {
|
||||
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
||||
item: 'outline',
|
||||
type: state.contentOutlineVisible ? 'close' : 'open',
|
||||
});
|
||||
return {
|
||||
contentOutlineVisible: !state.contentOutlineVisible,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Used by Logs details.
|
||||
* Returns true if all queries have the filter, otherwise false.
|
||||
@ -318,18 +343,20 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
|
||||
return Object.entries(groupedByPlugin).map(([pluginId, frames], index) => {
|
||||
return (
|
||||
<CustomContainer
|
||||
key={index}
|
||||
timeZone={timeZone}
|
||||
pluginId={pluginId}
|
||||
frames={frames}
|
||||
state={queryResponse.state}
|
||||
absoluteRange={absoluteRange}
|
||||
height={400}
|
||||
width={width}
|
||||
splitOpenFn={this.onSplitOpen(pluginId)}
|
||||
eventBus={eventBus}
|
||||
/>
|
||||
<ContentOutlineItem title={pluginId} icon="plug" key={index}>
|
||||
<CustomContainer
|
||||
key={index}
|
||||
timeZone={timeZone}
|
||||
pluginId={pluginId}
|
||||
frames={frames}
|
||||
state={queryResponse.state}
|
||||
absoluteRange={absoluteRange}
|
||||
height={400}
|
||||
width={width}
|
||||
splitOpenFn={this.onSplitOpen(pluginId)}
|
||||
eventBus={eventBus}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -338,68 +365,82 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
const { graphResult, absoluteRange, timeZone, queryResponse, showFlameGraph } = this.props;
|
||||
|
||||
return (
|
||||
<GraphContainer
|
||||
data={graphResult!}
|
||||
height={showFlameGraph ? 180 : 400}
|
||||
width={width}
|
||||
absoluteRange={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
onChangeTime={this.onUpdateTimeRange}
|
||||
annotations={queryResponse.annotations}
|
||||
splitOpenFn={this.onSplitOpen('graph')}
|
||||
loadingState={queryResponse.state}
|
||||
eventBus={this.graphEventBus}
|
||||
/>
|
||||
<ContentOutlineItem title="Graph" icon="graph-bar">
|
||||
<GraphContainer
|
||||
data={graphResult!}
|
||||
height={showFlameGraph ? 180 : 400}
|
||||
width={width}
|
||||
absoluteRange={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
onChangeTime={this.onUpdateTimeRange}
|
||||
annotations={queryResponse.annotations}
|
||||
splitOpenFn={this.onSplitOpen('graph')}
|
||||
loadingState={queryResponse.state}
|
||||
eventBus={this.graphEventBus}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderTablePanel(width: number) {
|
||||
const { exploreId, timeZone } = this.props;
|
||||
return (
|
||||
<TableContainer
|
||||
ariaLabel={selectors.pages.Explore.General.table}
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
onCellFilterAdded={this.onCellFilterAdded}
|
||||
timeZone={timeZone}
|
||||
splitOpenFn={this.onSplitOpen('table')}
|
||||
/>
|
||||
<ContentOutlineItem title="Table" icon="table">
|
||||
<TableContainer
|
||||
ariaLabel={selectors.pages.Explore.General.table}
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
onCellFilterAdded={this.onCellFilterAdded}
|
||||
timeZone={timeZone}
|
||||
splitOpenFn={this.onSplitOpen('table')}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderRawPrometheus(width: number) {
|
||||
const { exploreId, datasourceInstance, timeZone } = this.props;
|
||||
return (
|
||||
<RawPrometheusContainer
|
||||
showRawPrometheus={true}
|
||||
ariaLabel={selectors.pages.Explore.General.table}
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
onCellFilterAdded={datasourceInstance?.modifyQuery ? this.onCellFilterAdded : undefined}
|
||||
timeZone={timeZone}
|
||||
splitOpenFn={this.onSplitOpen('table')}
|
||||
/>
|
||||
<ContentOutlineItem title="Raw Prometheus" icon="gf-prometheus">
|
||||
<RawPrometheusContainer
|
||||
showRawPrometheus={true}
|
||||
ariaLabel={selectors.pages.Explore.General.table}
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
onCellFilterAdded={datasourceInstance?.modifyQuery ? this.onCellFilterAdded : undefined}
|
||||
timeZone={timeZone}
|
||||
splitOpenFn={this.onSplitOpen('table')}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderLogsPanel(width: number) {
|
||||
const { exploreId, syncedTimes, theme, queryResponse } = this.props;
|
||||
const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
|
||||
// Need to make ContentOutlineItem a flex container so the gap works
|
||||
const logsContentOutlineWrapper = css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
});
|
||||
return (
|
||||
<LogsContainer
|
||||
exploreId={exploreId}
|
||||
loadingState={queryResponse.state}
|
||||
syncedTimes={syncedTimes}
|
||||
width={width - spacing}
|
||||
onClickFilterLabel={this.onClickFilterLabel}
|
||||
onClickFilterOutLabel={this.onClickFilterOutLabel}
|
||||
onStartScanning={this.onStartScanning}
|
||||
onStopScanning={this.onStopScanning}
|
||||
eventBus={this.logsEventBus}
|
||||
splitOpenFn={this.onSplitOpen('logs')}
|
||||
scrollElement={this.scrollElement}
|
||||
isFilterLabelActive={this.isFilterLabelActive}
|
||||
/>
|
||||
<ContentOutlineItem title="Logs" icon="gf-logs" className={logsContentOutlineWrapper}>
|
||||
<LogsContainer
|
||||
exploreId={exploreId}
|
||||
loadingState={queryResponse.state}
|
||||
syncedTimes={syncedTimes}
|
||||
width={width - spacing}
|
||||
onClickFilterLabel={this.onClickFilterLabel}
|
||||
onClickFilterOutLabel={this.onClickFilterOutLabel}
|
||||
onStartScanning={this.onStartScanning}
|
||||
onStopScanning={this.onStopScanning}
|
||||
eventBus={this.logsEventBus}
|
||||
splitOpenFn={this.onSplitOpen('logs')}
|
||||
scrollElement={this.scrollElement}
|
||||
isFilterLabelActive={this.isFilterLabelActive}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
}
|
||||
|
||||
@ -407,17 +448,19 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
const { logsSample, timeZone, setSupplementaryQueryEnabled, exploreId, datasourceInstance, queries } = this.props;
|
||||
|
||||
return (
|
||||
<LogsSamplePanel
|
||||
queryResponse={logsSample.data}
|
||||
timeZone={timeZone}
|
||||
enabled={logsSample.enabled}
|
||||
queries={queries}
|
||||
datasourceInstance={datasourceInstance}
|
||||
splitOpen={this.onSplitOpen('logsSample')}
|
||||
setLogsSampleEnabled={(enabled: boolean) =>
|
||||
setSupplementaryQueryEnabled(exploreId, enabled, SupplementaryQueryType.LogsSample)
|
||||
}
|
||||
/>
|
||||
<ContentOutlineItem title="Logs Sample" icon="gf-logs">
|
||||
<LogsSamplePanel
|
||||
queryResponse={logsSample.data}
|
||||
timeZone={timeZone}
|
||||
enabled={logsSample.enabled}
|
||||
queries={queries}
|
||||
datasourceInstance={datasourceInstance}
|
||||
splitOpen={this.onSplitOpen('logsSample')}
|
||||
setLogsSampleEnabled={(enabled: boolean) =>
|
||||
setSupplementaryQueryEnabled(exploreId, enabled, SupplementaryQueryType.LogsSample)
|
||||
}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
}
|
||||
|
||||
@ -426,19 +469,25 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
const datasourceType = datasourceInstance ? datasourceInstance?.type : 'unknown';
|
||||
|
||||
return (
|
||||
<NodeGraphContainer
|
||||
dataFrames={this.memoizedGetNodeGraphDataFrames(queryResponse.series)}
|
||||
exploreId={exploreId}
|
||||
withTraceView={showTrace}
|
||||
datasourceType={datasourceType}
|
||||
splitOpenFn={this.onSplitOpen('nodeGraph')}
|
||||
/>
|
||||
<ContentOutlineItem title="Node Graph" icon="code-branch">
|
||||
<NodeGraphContainer
|
||||
dataFrames={this.memoizedGetNodeGraphDataFrames(queryResponse.series)}
|
||||
exploreId={exploreId}
|
||||
withTraceView={showTrace}
|
||||
datasourceType={datasourceType}
|
||||
splitOpenFn={this.onSplitOpen('nodeGraph')}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderFlameGraphPanel() {
|
||||
const { queryResponse } = this.props;
|
||||
return <FlameGraphExploreContainer dataFrames={queryResponse.flameGraphFrames} />;
|
||||
return (
|
||||
<ContentOutlineItem title="Flame Graph" icon="fire">
|
||||
<FlameGraphExploreContainer dataFrames={queryResponse.flameGraphFrames} />
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderTraceViewPanel() {
|
||||
@ -448,14 +497,16 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
return (
|
||||
// If there is no data (like 404) we show a separate error so no need to show anything here
|
||||
dataFrames.length && (
|
||||
<TraceViewContainer
|
||||
exploreId={exploreId}
|
||||
dataFrames={dataFrames}
|
||||
splitOpenFn={this.onSplitOpen('traceView')}
|
||||
scrollElement={this.scrollElement}
|
||||
queryResponse={queryResponse}
|
||||
topOfViewRef={this.topOfViewRef}
|
||||
/>
|
||||
<ContentOutlineItem title="Traces" icon="file-alt">
|
||||
<TraceViewContainer
|
||||
exploreId={exploreId}
|
||||
dataFrames={dataFrames}
|
||||
splitOpenFn={this.onSplitOpen('traceView')}
|
||||
scrollElement={this.scrollElement}
|
||||
queryResponse={queryResponse}
|
||||
topOfViewRef={this.topOfViewRef}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -481,7 +532,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
correlationEditorDetails,
|
||||
correlationEditorHelperData,
|
||||
} = this.props;
|
||||
const { openDrawer } = this.state;
|
||||
const { openDrawer, contentOutlineVisible } = this.state;
|
||||
const styles = getStyles(theme);
|
||||
const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted;
|
||||
const showRichHistory = openDrawer === ExploreDrawer.RichHistory;
|
||||
@ -508,85 +559,118 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
|
||||
<CustomScrollbar
|
||||
testId={selectors.pages.Explore.General.scrollView}
|
||||
scrollRefCallback={(scrollElement) => (this.scrollElement = scrollElement || undefined)}
|
||||
<ContentOutlineContextProvider>
|
||||
<ExploreToolbar
|
||||
exploreId={exploreId}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onContentOutlineToogle={this.onContentOutlineToogle}
|
||||
isContentOutlineOpen={contentOutlineVisible}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
paddingLeft: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
{datasourceInstance ? (
|
||||
<div className={styles.exploreContainer} ref={this.topOfViewRef}>
|
||||
<PanelContainer className={styles.queryContainer}>
|
||||
{correlationsBox}
|
||||
<QueryRows exploreId={exploreId} />
|
||||
<SecondaryActions
|
||||
// do not allow people to add queries with potentially different datasources in correlations editor mode
|
||||
addQueryRowButtonDisabled={isLive || (isCorrelationsEditorMode && datasourceInstance.meta.mixed)}
|
||||
// We cannot show multiple traces at the same time right now so we do not show add query button.
|
||||
//TODO:unification
|
||||
addQueryRowButtonHidden={false}
|
||||
richHistoryRowButtonHidden={richHistoryRowButtonHidden}
|
||||
richHistoryButtonActive={showRichHistory}
|
||||
queryInspectorButtonActive={showQueryInspector}
|
||||
onClickAddQueryRowButton={this.onClickAddQueryRowButton}
|
||||
onClickRichHistoryButton={this.toggleShowRichHistory}
|
||||
onClickQueryInspectorButton={this.toggleShowQueryInspector}
|
||||
/>
|
||||
<ResponseErrorContainer exploreId={exploreId} />
|
||||
</PanelContainer>
|
||||
<AutoSizer onResize={this.onResize} disableHeight>
|
||||
{({ width }) => {
|
||||
if (width === 0) {
|
||||
return null;
|
||||
}
|
||||
<div className={styles.wrapper}>
|
||||
{contentOutlineVisible && (
|
||||
<div className={styles.left}>
|
||||
<ContentOutline scroller={this.scrollElement} panelId={`content-outline-container-${exploreId}`} />
|
||||
</div>
|
||||
)}
|
||||
<CustomScrollbar
|
||||
testId={selectors.pages.Explore.General.scrollView}
|
||||
scrollRefCallback={(scrollElement) => (this.scrollElement = scrollElement || undefined)}
|
||||
hideHorizontalTrack
|
||||
>
|
||||
<div className={styles.exploreContainer} ref={this.topOfViewRef}>
|
||||
{datasourceInstance ? (
|
||||
<>
|
||||
<ContentOutlineItem title="Queries" icon="arrow">
|
||||
<PanelContainer className={styles.queryContainer}>
|
||||
{correlationsBox}
|
||||
<QueryRows exploreId={exploreId} />
|
||||
<SecondaryActions
|
||||
// do not allow people to add queries with potentially different datasources in correlations editor mode
|
||||
addQueryRowButtonDisabled={
|
||||
isLive || (isCorrelationsEditorMode && datasourceInstance.meta.mixed)
|
||||
}
|
||||
// We cannot show multiple traces at the same time right now so we do not show add query button.
|
||||
//TODO:unification
|
||||
addQueryRowButtonHidden={false}
|
||||
richHistoryRowButtonHidden={richHistoryRowButtonHidden}
|
||||
richHistoryButtonActive={showRichHistory}
|
||||
queryInspectorButtonActive={showQueryInspector}
|
||||
onClickAddQueryRowButton={this.onClickAddQueryRowButton}
|
||||
onClickRichHistoryButton={this.toggleShowRichHistory}
|
||||
onClickQueryInspectorButton={this.toggleShowQueryInspector}
|
||||
/>
|
||||
<ResponseErrorContainer exploreId={exploreId} />
|
||||
</PanelContainer>
|
||||
</ContentOutlineItem>
|
||||
<AutoSizer onResize={this.onResize} disableHeight>
|
||||
{({ width }) => {
|
||||
if (width === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={cx(styles.exploreMain)} style={{ width }}>
|
||||
<ErrorBoundaryAlert>
|
||||
{showPanels && (
|
||||
<>
|
||||
{showMetrics && graphResult && (
|
||||
<ErrorBoundaryAlert>{this.renderGraphPanel(width)}</ErrorBoundaryAlert>
|
||||
)}
|
||||
{showRawPrometheus && (
|
||||
<ErrorBoundaryAlert>{this.renderRawPrometheus(width)}</ErrorBoundaryAlert>
|
||||
)}
|
||||
{showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>}
|
||||
{showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel(width)}</ErrorBoundaryAlert>}
|
||||
{showNodeGraph && <ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>}
|
||||
{showFlameGraph && <ErrorBoundaryAlert>{this.renderFlameGraphPanel()}</ErrorBoundaryAlert>}
|
||||
{showTrace && <ErrorBoundaryAlert>{this.renderTraceViewPanel()}</ErrorBoundaryAlert>}
|
||||
{showLogsSample && <ErrorBoundaryAlert>{this.renderLogsSamplePanel()}</ErrorBoundaryAlert>}
|
||||
{showCustom && <ErrorBoundaryAlert>{this.renderCustom(width)}</ErrorBoundaryAlert>}
|
||||
{showNoData && <ErrorBoundaryAlert>{this.renderNoData()}</ErrorBoundaryAlert>}
|
||||
</>
|
||||
)}
|
||||
{showRichHistory && (
|
||||
<RichHistoryContainer
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
onClose={this.toggleShowRichHistory}
|
||||
/>
|
||||
)}
|
||||
{showQueryInspector && (
|
||||
<ExploreQueryInspector
|
||||
exploreId={exploreId}
|
||||
width={width}
|
||||
onClose={this.toggleShowQueryInspector}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryAlert>
|
||||
</main>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
) : (
|
||||
this.renderEmptyState(styles.exploreContainer)
|
||||
)}
|
||||
</CustomScrollbar>
|
||||
</>
|
||||
return (
|
||||
<main className={cx(styles.exploreMain)} style={{ width }}>
|
||||
<ErrorBoundaryAlert>
|
||||
{showPanels && (
|
||||
<>
|
||||
{showMetrics && graphResult && (
|
||||
<ErrorBoundaryAlert>{this.renderGraphPanel(width)}</ErrorBoundaryAlert>
|
||||
)}
|
||||
{showRawPrometheus && (
|
||||
<ErrorBoundaryAlert>{this.renderRawPrometheus(width)}</ErrorBoundaryAlert>
|
||||
)}
|
||||
{showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>}
|
||||
{showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel(width)}</ErrorBoundaryAlert>}
|
||||
{showNodeGraph && (
|
||||
<ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>
|
||||
)}
|
||||
{showFlameGraph && (
|
||||
<ErrorBoundaryAlert>{this.renderFlameGraphPanel()}</ErrorBoundaryAlert>
|
||||
)}
|
||||
{showTrace && <ErrorBoundaryAlert>{this.renderTraceViewPanel()}</ErrorBoundaryAlert>}
|
||||
{showLogsSample && (
|
||||
<ErrorBoundaryAlert>{this.renderLogsSamplePanel()}</ErrorBoundaryAlert>
|
||||
)}
|
||||
{showCustom && <ErrorBoundaryAlert>{this.renderCustom(width)}</ErrorBoundaryAlert>}
|
||||
{showNoData && <ErrorBoundaryAlert>{this.renderNoData()}</ErrorBoundaryAlert>}
|
||||
</>
|
||||
)}
|
||||
{showRichHistory && (
|
||||
<RichHistoryContainer
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
onClose={this.toggleShowRichHistory}
|
||||
/>
|
||||
)}
|
||||
{showQueryInspector && (
|
||||
<ExploreQueryInspector
|
||||
exploreId={exploreId}
|
||||
width={width}
|
||||
onClose={this.toggleShowQueryInspector}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundaryAlert>
|
||||
</main>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</>
|
||||
) : (
|
||||
this.renderEmptyState(styles.exploreContainer)
|
||||
)}
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</ContentOutlineContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { EventBusSrv, GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { useStyles2, CustomScrollbar } from '@grafana/ui';
|
||||
import { stopQueryState } from 'app/core/utils/explore';
|
||||
import { StoreState, useSelector } from 'app/types';
|
||||
|
||||
@ -14,14 +14,11 @@ import { getExploreItemSelector } from './state/selectors';
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
explore: css`
|
||||
label: explorePaneContainer;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 600px;
|
||||
& + & {
|
||||
border-left: 1px dotted ${theme.colors.border.medium};
|
||||
}
|
||||
height: 100%;
|
||||
`,
|
||||
};
|
||||
};
|
||||
@ -51,9 +48,11 @@ function ExplorePaneContainerUnconnected({ exploreId }: Props) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.explore} ref={ref} data-testid={selectors.pages.Explore.General.container}>
|
||||
<Explore exploreId={exploreId} eventBus={eventBus.current} />
|
||||
</div>
|
||||
<CustomScrollbar>
|
||||
<div className={styles.explore} ref={ref} data-testid={selectors.pages.Explore.General.container}>
|
||||
<Explore exploreId={exploreId} eventBus={eventBus.current} />
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import React, { RefObject, useMemo } from 'react';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
|
||||
import { DataSourceInstanceSettings, RawTimeRange, GrafanaTheme2 } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { reportInteraction, config } from '@grafana/runtime';
|
||||
import {
|
||||
defaultIntervals,
|
||||
PageToolbar,
|
||||
@ -43,25 +43,39 @@ import { isLeftPaneSelector, isSplit, selectCorrelationDetails, selectPanesEntri
|
||||
import { syncTimes, changeRefreshInterval } from './state/time';
|
||||
import { LiveTailControls } from './useLiveTailControls';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
const getStyles = (theme: GrafanaTheme2, splitted: Boolean) => ({
|
||||
rotateIcon: css({
|
||||
'> div > svg': {
|
||||
transform: 'rotate(180deg)',
|
||||
},
|
||||
}),
|
||||
toolbarButton: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
marginRight: theme.spacing(0.5),
|
||||
width: splitted && theme.spacing(6),
|
||||
}),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
exploreId: string;
|
||||
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
|
||||
onContentOutlineToogle: () => void;
|
||||
isContentOutlineOpen: boolean;
|
||||
topOfViewRef?: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props) {
|
||||
export function ExploreToolbar({
|
||||
exploreId,
|
||||
topOfViewRef,
|
||||
onChangeTime,
|
||||
onContentOutlineToogle,
|
||||
isContentOutlineOpen,
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const splitted = useSelector(isSplit);
|
||||
const styles = useStyles2(getStyles, splitted);
|
||||
|
||||
const timeZone = useSelector((state: StoreState) => getTimeZone(state.user));
|
||||
const fiscalYearStartMonth = useSelector((state: StoreState) => getFiscalYearStartMonth(state.user));
|
||||
const { refreshInterval, datasourceInstance, range, isLive, isPaused, syncedTimes } = useSelector(
|
||||
@ -217,6 +231,21 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props)
|
||||
<PageToolbar
|
||||
aria-label={t('explore.toolbar.aria-label', 'Explore toolbar')}
|
||||
leftItems={[
|
||||
config.featureToggles.exploreContentOutline && (
|
||||
<ToolbarButton
|
||||
key="content-outline"
|
||||
variant="canvas"
|
||||
tooltip="Content outline"
|
||||
icon="list-ui-alt"
|
||||
iconOnly={splitted}
|
||||
onClick={onContentOutlineToogle}
|
||||
aria-expanded={isContentOutlineOpen}
|
||||
aria-controls={isContentOutlineOpen ? 'content-outline-container' : undefined}
|
||||
className={styles.toolbarButton}
|
||||
>
|
||||
Outline
|
||||
</ToolbarButton>
|
||||
),
|
||||
<DataSourcePicker
|
||||
key={`${exploreId}-ds-picker`}
|
||||
mixed={!isCorrelationsEditorMode}
|
||||
@ -225,7 +254,7 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props)
|
||||
hideTextValue={showSmallDataSourcePicker}
|
||||
width={showSmallDataSourcePicker ? 8 : undefined}
|
||||
/>,
|
||||
]}
|
||||
].filter(Boolean)}
|
||||
forceShowLeftItems
|
||||
>
|
||||
{[
|
||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Components } from '@grafana/e2e-selectors';
|
||||
import { HorizontalGroup, ToolbarButton, useTheme2 } from '@grafana/ui';
|
||||
import { ToolbarButton, useTheme2 } from '@grafana/ui';
|
||||
|
||||
type Props = {
|
||||
addQueryRowButtonDisabled?: boolean;
|
||||
@ -20,6 +20,9 @@ type Props = {
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
containerMargin: css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${theme.spacing(1)};
|
||||
margin-top: ${theme.spacing(2)};
|
||||
`,
|
||||
};
|
||||
@ -30,38 +33,36 @@ export function SecondaryActions(props: Props) {
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<div className={styles.containerMargin}>
|
||||
<HorizontalGroup>
|
||||
{!props.addQueryRowButtonHidden && (
|
||||
<ToolbarButton
|
||||
variant="canvas"
|
||||
aria-label="Add query"
|
||||
onClick={props.onClickAddQueryRowButton}
|
||||
disabled={props.addQueryRowButtonDisabled}
|
||||
icon="plus"
|
||||
>
|
||||
Add query
|
||||
</ToolbarButton>
|
||||
)}
|
||||
{!props.richHistoryRowButtonHidden && (
|
||||
<ToolbarButton
|
||||
variant={props.richHistoryButtonActive ? 'active' : 'canvas'}
|
||||
aria-label="Query history"
|
||||
onClick={props.onClickRichHistoryButton}
|
||||
data-testid={Components.QueryTab.queryHistoryButton}
|
||||
icon="history"
|
||||
>
|
||||
Query history
|
||||
</ToolbarButton>
|
||||
)}
|
||||
{!props.addQueryRowButtonHidden && (
|
||||
<ToolbarButton
|
||||
variant={props.queryInspectorButtonActive ? 'active' : 'canvas'}
|
||||
aria-label="Query inspector"
|
||||
onClick={props.onClickQueryInspectorButton}
|
||||
icon="info-circle"
|
||||
variant="canvas"
|
||||
aria-label="Add query"
|
||||
onClick={props.onClickAddQueryRowButton}
|
||||
disabled={props.addQueryRowButtonDisabled}
|
||||
icon="plus"
|
||||
>
|
||||
Query inspector
|
||||
Add query
|
||||
</ToolbarButton>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
{!props.richHistoryRowButtonHidden && (
|
||||
<ToolbarButton
|
||||
variant={props.richHistoryButtonActive ? 'active' : 'canvas'}
|
||||
aria-label="Query history"
|
||||
onClick={props.onClickRichHistoryButton}
|
||||
data-testid={Components.QueryTab.queryHistoryButton}
|
||||
icon="history"
|
||||
>
|
||||
Query history
|
||||
</ToolbarButton>
|
||||
)}
|
||||
<ToolbarButton
|
||||
variant={props.queryInspectorButtonActive ? 'active' : 'canvas'}
|
||||
aria-label="Query inspector"
|
||||
onClick={props.onClickQueryInspectorButton}
|
||||
icon="info-circle"
|
||||
>
|
||||
Query inspector
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user