mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Command Palette Scaffolding + Explore (#47445)
* Add feature flag and scaffodling * start adding actions * WIP * move action files * Start adding styles * Fix implementation based on feedback * Add more hackathon code back to command palette * Cleanup * Cleanup unused service files for simple MVP pass * Move type def to library * WIP * Move provider to proper place to pick up other routes’ actions * Build actions off navbar, add explore actions * Work around undefined typescript stuff * Fix based on feedback * close palette on ESC * Fix based on PR feedback pt 1 * Move styles to classes * Move another inline style to a class * Enable command palette by default * change around async hook structure * Add simple feature tracking * Code cleanup, and be sure the command is accurate * Change to only render if there are actions, and only add actions once past login
This commit is contained in:
parent
75d528d7bd
commit
3e7db088ac
@ -1140,6 +1140,9 @@ promQueryBuilder = true
|
||||
# Experimental Explore to Dashboard workflow
|
||||
explore2Dashboard = true
|
||||
|
||||
# Experimental Command Palette
|
||||
commandPalette = true
|
||||
|
||||
# feature1 = true
|
||||
# feature2 = false
|
||||
|
||||
|
@ -319,6 +319,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-source-map": "0.6.1",
|
||||
"jsurl": "^0.1.5",
|
||||
"kbar": "^0.1.0-beta.30",
|
||||
"lezer-promql": "0.22.0",
|
||||
"lodash": "4.17.21",
|
||||
"logfmt": "^1.3.2",
|
||||
|
@ -55,4 +55,5 @@ export interface FeatureToggles {
|
||||
azureMonitorResourcePickerForMetrics?: boolean;
|
||||
explore2Dashboard?: boolean;
|
||||
persistNotifications?: boolean;
|
||||
commandPalette?: boolean;
|
||||
}
|
||||
|
@ -219,5 +219,10 @@ var (
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "commandPalette",
|
||||
Description: "Enable command palette",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -162,4 +162,8 @@ const (
|
||||
// FlagPersistNotifications
|
||||
// PoC Notifications page
|
||||
FlagPersistNotifications = "persistNotifications"
|
||||
|
||||
// FlagCommandPalette
|
||||
// Enable command palette
|
||||
FlagCommandPalette = "commandPalette"
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { ComponentType } from 'react';
|
||||
import { Router, Route, Redirect, Switch } from 'react-router-dom';
|
||||
import { config, locationService, navigationLogger } from '@grafana/runtime';
|
||||
import { config, locationService, navigationLogger, reportInteraction } from '@grafana/runtime';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from 'app/store/store';
|
||||
import { ErrorBoundaryAlert, GlobalStyles, ModalRoot, ModalsProvider, PortalContainer } from '@grafana/ui';
|
||||
@ -15,6 +15,8 @@ import { GrafanaRoute } from './core/navigation/GrafanaRoute';
|
||||
import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
|
||||
import { SearchWrapper } from 'app/features/search';
|
||||
import { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
|
||||
import { Action, KBarProvider } from 'kbar';
|
||||
import { CommandPalette } from './features/commandPalette/CommandPalette';
|
||||
import { I18nProvider } from './core/localisation';
|
||||
import { AngularRoot } from './angular/AngularRoot';
|
||||
import { loadAndInitAngularIfEnabled } from './angular/loadAndInitAngularIfEnabled';
|
||||
@ -85,14 +87,25 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
||||
|
||||
const newNavigationEnabled = Boolean(config.featureToggles.newNavigation);
|
||||
|
||||
const commandPaletteActionSelected = (action: Action) => {
|
||||
reportInteraction('commandPalette_action_selected', {
|
||||
actionId: action.id,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<ErrorBoundaryAlert style="page">
|
||||
<ConfigContext.Provider value={config}>
|
||||
<ThemeProvider>
|
||||
<KBarProvider
|
||||
actions={[]}
|
||||
options={{ enableHistory: true, callbacks: { onSelectAction: commandPaletteActionSelected } }}
|
||||
>
|
||||
<ModalsProvider>
|
||||
<GlobalStyles />
|
||||
{config.featureToggles.commandPalette && <CommandPalette />}
|
||||
<div className="grafana-app">
|
||||
<Router history={locationService.getHistory()}>
|
||||
{ready && <>{newNavigationEnabled ? <NavBarNext /> : <NavBar />}</>}
|
||||
@ -115,6 +128,7 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
||||
<ModalRoot />
|
||||
<PortalContainer />
|
||||
</ModalsProvider>
|
||||
</KBarProvider>
|
||||
</ThemeProvider>
|
||||
</ConfigContext.Provider>
|
||||
</ErrorBoundaryAlert>
|
||||
|
146
public/app/features/commandPalette/CommandPalette.tsx
Normal file
146
public/app/features/commandPalette/CommandPalette.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
KBarAnimator,
|
||||
KBarPortal,
|
||||
KBarPositioner,
|
||||
KBarResults,
|
||||
KBarSearch,
|
||||
useMatches,
|
||||
Action,
|
||||
VisualState,
|
||||
useRegisterActions,
|
||||
useKBar,
|
||||
} from 'kbar';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { ResultItem } from './ResultItem';
|
||||
import getGlobalActions from './actions/global.static.actions';
|
||||
import getDashboardNavActions from './actions/dashboard.nav.actions';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { StoreState } from 'app/types';
|
||||
import { css } from '@emotion/css';
|
||||
import { keybindingSrv } from '../../core/services/keybindingSrv';
|
||||
import { reportInteraction, locationService } from '@grafana/runtime';
|
||||
|
||||
/**
|
||||
* Wrap all the components from KBar here.
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
export const CommandPalette = () => {
|
||||
const styles = useStyles2(getSearchStyles);
|
||||
const [actions, setActions] = useState<Action[]>([]);
|
||||
const { notHidden, query, showing } = useKBar((state) => ({
|
||||
notHidden: state.visualState !== VisualState.hidden,
|
||||
showing: state.visualState === VisualState.showing,
|
||||
}));
|
||||
const isNotLogin = locationService.getLocation().pathname !== '/login';
|
||||
|
||||
const { navBarTree } = useSelector((state: StoreState) => {
|
||||
return {
|
||||
navBarTree: state.navBarTree,
|
||||
};
|
||||
});
|
||||
|
||||
keybindingSrv.bind('esc', () => {
|
||||
if (notHidden) {
|
||||
query.setVisualState(VisualState.animatingOut);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (isNotLogin) {
|
||||
const staticActions = getGlobalActions(navBarTree);
|
||||
const dashAct = await getDashboardNavActions('go/dashboard');
|
||||
setActions([...staticActions, ...dashAct]);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isNotLogin]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showing) {
|
||||
reportInteraction('commandPalette_opened');
|
||||
}
|
||||
}, [showing]);
|
||||
|
||||
useRegisterActions(actions, [actions]);
|
||||
|
||||
return actions.length > 0 ? (
|
||||
<KBarPortal>
|
||||
<KBarPositioner className={styles.positioner}>
|
||||
<KBarAnimator className={styles.animator}>
|
||||
<KBarSearch className={styles.search} />
|
||||
<RenderResults />
|
||||
</KBarAnimator>
|
||||
</KBarPositioner>
|
||||
</KBarPortal>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const RenderResults = () => {
|
||||
const { results, rootActionId } = useMatches();
|
||||
const styles = useStyles2(getSearchStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.resultsContainer}>
|
||||
<KBarResults
|
||||
items={results}
|
||||
onRender={({ item, active }) =>
|
||||
typeof item === 'string' ? (
|
||||
<div className={styles.sectionHeader}>{item}</div>
|
||||
) : (
|
||||
<ResultItem action={item} active={active} currentRootActionId={rootActionId!} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getSearchStyles = (theme: GrafanaTheme2) => ({
|
||||
positioner: css({
|
||||
zIndex: theme.zIndex.portal,
|
||||
marginTop: '0px',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
background: theme.components.overlay.background,
|
||||
backdropFilter: 'blur(1px)',
|
||||
},
|
||||
}),
|
||||
animator: css({
|
||||
maxWidth: theme.breakpoints.values.sm, // supposed to be 600...
|
||||
width: '100%',
|
||||
background: theme.colors.background.canvas,
|
||||
color: theme.colors.text.primary,
|
||||
borderRadius: theme.shape.borderRadius(4),
|
||||
overflow: 'hidden',
|
||||
boxShadow: theme.shadows.z3,
|
||||
}),
|
||||
search: css({
|
||||
padding: theme.spacing(2, 3),
|
||||
fontSize: theme.typography.fontSize,
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
background: theme.colors.background.canvas,
|
||||
color: theme.colors.text.primary,
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
sectionHeader: css({
|
||||
padding: theme.spacing(1, 2),
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
fontWeight: theme.typography.body.fontWeight,
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
resultsContainer: css({
|
||||
padding: theme.spacing(2, 0),
|
||||
}),
|
||||
});
|
123
public/app/features/commandPalette/ResultItem.tsx
Normal file
123
public/app/features/commandPalette/ResultItem.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import { ActionId, ActionImpl } from 'kbar';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export const ResultItem = React.forwardRef(
|
||||
(
|
||||
{
|
||||
action,
|
||||
active,
|
||||
currentRootActionId,
|
||||
}: {
|
||||
action: ActionImpl;
|
||||
active: boolean;
|
||||
currentRootActionId: ActionId;
|
||||
},
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) => {
|
||||
const ancestors = React.useMemo(() => {
|
||||
if (!currentRootActionId) {
|
||||
return action.ancestors;
|
||||
}
|
||||
|
||||
const index = action.ancestors.findIndex((ancestor) => ancestor.id === currentRootActionId);
|
||||
// +1 removes the currentRootAction; e.g.
|
||||
// if we are on the "Set theme" parent action,
|
||||
// the UI should not display "Set theme… > Dark"
|
||||
// but rather just "Dark"
|
||||
return action.ancestors.slice(index + 1);
|
||||
}, [action.ancestors, currentRootActionId]);
|
||||
|
||||
const theme = useTheme2();
|
||||
const styles = getResultItemStyles(theme, active);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={styles.row}>
|
||||
<div className={styles.actionContainer}>
|
||||
{action.icon}
|
||||
<div className={styles.textContainer}>
|
||||
<div>
|
||||
{ancestors.length > 0 &&
|
||||
ancestors.map((ancestor) => (
|
||||
<React.Fragment key={ancestor.id}>
|
||||
<span className={styles.breadcrumbAncestor}>{ancestor.name}</span>
|
||||
<span className={styles.breadcrumbAncestor}>›</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<span>{action.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
{action.subtitle && <span className={styles.subtitleText}>{action.subtitle}</span>}
|
||||
</div>
|
||||
{action.shortcut?.length ? (
|
||||
<div aria-hidden className={styles.shortcutContainer}>
|
||||
{action.shortcut.map((sc) => (
|
||||
<kbd key={sc} className={styles.shortcut}>
|
||||
{sc}
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
ResultItem.displayName = 'ResultItem';
|
||||
|
||||
const getResultItemStyles = (theme: GrafanaTheme2, isActive: boolean) => {
|
||||
const textColor = isActive ? theme.colors.text.maxContrast : theme.colors.text.primary;
|
||||
const rowBackgroundColor = isActive ? theme.colors.background.primary : 'transparent';
|
||||
const shortcutBackgroundColor = isActive ? theme.colors.background.secondary : theme.colors.background.primary;
|
||||
return {
|
||||
row: css({
|
||||
color: textColor,
|
||||
padding: theme.spacing(1, 2),
|
||||
background: rowBackgroundColor,
|
||||
display: 'flex',
|
||||
alightItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
cursor: 'pointer',
|
||||
'&:before': {
|
||||
display: isActive ? 'block' : 'none',
|
||||
content: '" "',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: theme.spacing(0.5),
|
||||
borderRadius: theme.shape.borderRadius(1),
|
||||
backgroundImage: theme.colors.gradients.brandVertical,
|
||||
},
|
||||
}),
|
||||
actionContainer: css({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(2),
|
||||
alignitems: 'center',
|
||||
fontsize: theme.typography.fontSize,
|
||||
}),
|
||||
textContainer: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
shortcut: css({
|
||||
padding: theme.spacing(0, 1),
|
||||
background: shortcutBackgroundColor,
|
||||
borderRadius: theme.shape.borderRadius(),
|
||||
fontsize: theme.typography.fontSize,
|
||||
}),
|
||||
breadcrumbAncestor: css({
|
||||
opacity: 0.5,
|
||||
marginRight: theme.spacing(1),
|
||||
}),
|
||||
subtitleText: css({
|
||||
fontSize: theme.typography.fontSize - 2,
|
||||
}),
|
||||
shortcutContainer: css({
|
||||
display: 'grid',
|
||||
gridAutoFlow: 'column',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
};
|
||||
};
|
@ -0,0 +1,24 @@
|
||||
import { locationService, getBackendSrv } from '@grafana/runtime';
|
||||
import { Action } from 'kbar';
|
||||
|
||||
async function getDashboardNav(parentId: string): Promise<Action[]> {
|
||||
const data: Array<{ type: string; title: string; url: string }> = await getBackendSrv().get('/api/search');
|
||||
|
||||
const goToDashboardActions: Action[] = data
|
||||
.filter((item) => item.type === 'dash-db')
|
||||
.map((item) => ({
|
||||
parent: parentId,
|
||||
id: `go/dashboard/${item.url}`,
|
||||
name: `Go to dashboard ${item.title}`,
|
||||
perform: () => {
|
||||
locationService.push(item.url);
|
||||
},
|
||||
}));
|
||||
|
||||
return goToDashboardActions;
|
||||
}
|
||||
|
||||
export default async (parentId: string) => {
|
||||
const dashboardNav = await getDashboardNav(parentId);
|
||||
return dashboardNav;
|
||||
};
|
@ -0,0 +1,152 @@
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { Action, Priority } from 'kbar';
|
||||
|
||||
export default (navBarTree: NavModelItem[]) => {
|
||||
const globalActions: Action[] = [
|
||||
{
|
||||
id: 'go/search',
|
||||
name: 'Go to dashboard search',
|
||||
keywords: 'navigate',
|
||||
perform: () => locationService.push('?search=open'),
|
||||
section: 'Navigation',
|
||||
shortcut: ['s', 'o'],
|
||||
},
|
||||
{
|
||||
id: 'go/dashboard',
|
||||
name: 'Go to dashboard...',
|
||||
keywords: 'navigate',
|
||||
section: 'Navigation',
|
||||
priority: Priority.NORMAL,
|
||||
},
|
||||
{
|
||||
id: 'preferences/theme',
|
||||
name: 'Change theme...',
|
||||
keywords: 'interface color dark light',
|
||||
section: 'Preferences',
|
||||
},
|
||||
{
|
||||
id: 'preferences/dark-theme',
|
||||
name: 'Dark',
|
||||
keywords: 'dark theme',
|
||||
section: '',
|
||||
perform: () => {
|
||||
locationService.push({ search: '?theme=dark' });
|
||||
location.reload();
|
||||
},
|
||||
parent: 'preferences/theme',
|
||||
},
|
||||
{
|
||||
id: 'preferences/light-theme',
|
||||
name: 'Light',
|
||||
keywords: 'light theme',
|
||||
section: '',
|
||||
perform: () => {
|
||||
locationService.push({ search: '?theme=light' });
|
||||
location.reload();
|
||||
},
|
||||
parent: 'preferences/theme',
|
||||
},
|
||||
];
|
||||
|
||||
// this maps actions to navbar by URL items for showing/hiding
|
||||
// actions is an array for multiple child actions that would be under one navbar item
|
||||
const navBarActionMap = [
|
||||
{
|
||||
url: '/dashboard/new',
|
||||
actions: [
|
||||
{
|
||||
id: 'management/create-folder',
|
||||
name: 'Create folder',
|
||||
keywords: 'new add',
|
||||
perform: () => locationService.push('/dashboards/folder/new'),
|
||||
section: 'Management',
|
||||
},
|
||||
{
|
||||
id: 'management/create-dashboard',
|
||||
name: 'Create dashboard',
|
||||
keywords: 'new add',
|
||||
perform: () => locationService.push('/dashboard/new'),
|
||||
section: 'Management',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
url: '/',
|
||||
actions: [
|
||||
{
|
||||
id: 'go/home',
|
||||
name: 'Go to home',
|
||||
keywords: 'navigate',
|
||||
perform: () => locationService.push('/'),
|
||||
section: 'Navigation',
|
||||
shortcut: ['g', 'h'],
|
||||
priority: Priority.HIGH,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
url: '/explore',
|
||||
actions: [
|
||||
{
|
||||
id: 'go/explore',
|
||||
name: 'Go to explore',
|
||||
keywords: 'navigate',
|
||||
perform: () => locationService.push('/explore'),
|
||||
section: 'Navigation',
|
||||
priority: Priority.NORMAL,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
url: '/alerting',
|
||||
actions: [
|
||||
{
|
||||
id: 'go/alerting',
|
||||
name: 'Go to alerting',
|
||||
keywords: 'navigate notification',
|
||||
perform: () => locationService.push('/alerting'),
|
||||
section: 'Navigation',
|
||||
priority: Priority.NORMAL,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
url: '/profile',
|
||||
actions: [
|
||||
{
|
||||
id: 'go/profile',
|
||||
name: 'Go to profile',
|
||||
keywords: 'navigate preferences',
|
||||
perform: () => locationService.push('/profile'),
|
||||
section: 'Navigation',
|
||||
shortcut: ['g', 'p'],
|
||||
priority: Priority.LOW,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
url: '/datasources',
|
||||
actions: [
|
||||
{
|
||||
id: 'go/configuration',
|
||||
name: 'Go to data sources configuration',
|
||||
keywords: 'navigate settings ds',
|
||||
perform: () => locationService.push('/datasources'),
|
||||
section: 'Navigation',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const navBarActions: Action[] = [];
|
||||
|
||||
navBarActionMap.forEach((navBarAction) => {
|
||||
const navBarItem = navBarTree.find((navBarItem) => navBarItem.url === navBarAction.url);
|
||||
if (navBarItem && !navBarItem.hideFromNavbar) {
|
||||
navBarActions.push(...navBarAction.actions);
|
||||
}
|
||||
});
|
||||
|
||||
return [...globalActions, ...navBarActions];
|
||||
};
|
94
public/app/features/explore/ExploreActions.tsx
Normal file
94
public/app/features/explore/ExploreActions.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useRegisterActions, useKBar, Action, Priority } from 'kbar';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { ExploreId } from 'app/types';
|
||||
import { splitOpen, splitClose } from './state/main';
|
||||
import { isSplit } from './state/selectors';
|
||||
import { runQueries } from './state/query';
|
||||
|
||||
interface Props {
|
||||
exploreIdLeft: ExploreId;
|
||||
exploreIdRight?: ExploreId;
|
||||
}
|
||||
|
||||
export const ExploreActions: FC<Props> = ({ exploreIdLeft, exploreIdRight }: Props) => {
|
||||
const [actions, setActions] = useState<Action[]>([]);
|
||||
const { query } = useKBar();
|
||||
const dispatch = useDispatch();
|
||||
const splitted = useSelector(isSplit);
|
||||
|
||||
useEffect(() => {
|
||||
const exploreSection = {
|
||||
name: 'Explore',
|
||||
priority: Priority.HIGH + 1,
|
||||
};
|
||||
|
||||
const actionsArr: Action[] = [];
|
||||
|
||||
if (splitted) {
|
||||
actionsArr.push({
|
||||
id: 'explore/run-query-left',
|
||||
name: 'Run Query (Left)',
|
||||
keywords: 'query left',
|
||||
perform: () => {
|
||||
dispatch(runQueries(exploreIdLeft));
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
if (exploreIdRight) {
|
||||
// we should always have the right exploreId if split
|
||||
actionsArr.push({
|
||||
id: 'explore/run-query-right',
|
||||
name: 'Run Query (Right)',
|
||||
keywords: 'query right',
|
||||
perform: () => {
|
||||
dispatch(runQueries(exploreIdRight));
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
actionsArr.push({
|
||||
id: 'explore/split-view-close-left',
|
||||
name: 'Close split view left',
|
||||
keywords: 'split',
|
||||
perform: () => {
|
||||
dispatch(splitClose(exploreIdLeft));
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
actionsArr.push({
|
||||
id: 'explore/split-view-close-right',
|
||||
name: 'Close split view right',
|
||||
keywords: 'split',
|
||||
perform: () => {
|
||||
dispatch(splitClose(exploreIdRight));
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
actionsArr.push({
|
||||
id: 'explore/run-query',
|
||||
name: 'Run Query',
|
||||
keywords: 'query',
|
||||
perform: () => {
|
||||
dispatch(runQueries(exploreIdLeft));
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
actionsArr.push({
|
||||
id: 'explore/split-view-open',
|
||||
name: 'Open split view',
|
||||
keywords: 'split',
|
||||
perform: () => {
|
||||
dispatch(splitOpen());
|
||||
},
|
||||
section: exploreSection,
|
||||
});
|
||||
}
|
||||
setActions(actionsArr);
|
||||
}, [exploreIdLeft, exploreIdRight, splitted, query, dispatch]);
|
||||
|
||||
useRegisterActions(!query ? [] : actions, [actions, query]);
|
||||
|
||||
return null;
|
||||
};
|
@ -6,6 +6,7 @@ import { lastSavedUrl, resetExploreAction, richHistoryUpdatedAction } from './st
|
||||
import { ExplorePaneContainer } from './ExplorePaneContainer';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { Branding } from '../../core/components/Branding/Branding';
|
||||
import { ExploreActions } from './ExploreActions';
|
||||
|
||||
import { getNavModel } from '../../core/selectors/navModel';
|
||||
import { StoreState } from 'app/types';
|
||||
@ -69,6 +70,7 @@ class WrapperUnconnected extends PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<div className="page-scrollbar-wrapper">
|
||||
<ExploreActions exploreIdLeft={ExploreId.left} exploreIdRight={ExploreId.right} />
|
||||
<div className="explore-wrapper">
|
||||
<ErrorBoundaryAlert style="page">
|
||||
<ExplorePaneContainer split={hasSplit} exploreId={ExploreId.left} urlQuery={left} />
|
||||
|
83
yarn.lock
83
yarn.lock
@ -6881,6 +6881,40 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@reach/observe-rect@npm:^1.1.0":
|
||||
version: 1.2.0
|
||||
resolution: "@reach/observe-rect@npm:1.2.0"
|
||||
checksum: 7dd903eeaad0e22c6d973bd26265d91eadba56ab5134701ceb3e85214db75339fae94aa7e8b88a65e8daa64bc7cf1b915d4ffcdfd324466b561dc6adc3c6e070
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@reach/portal@npm:^0.16.0":
|
||||
version: 0.16.2
|
||||
resolution: "@reach/portal@npm:0.16.2"
|
||||
dependencies:
|
||||
"@reach/utils": 0.16.0
|
||||
tiny-warning: ^1.0.3
|
||||
tslib: ^2.3.0
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || 17.x
|
||||
react-dom: ^16.8.0 || 17.x
|
||||
checksum: 7413dcd169cfb9854dd0d3a01f811ec19ef170558fcbd00118d676fc02c197d1c0bce1d1357508879d4775169561d103e13a8e4d74cc677eb0037cc7b04f7a1e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@reach/utils@npm:0.16.0":
|
||||
version: 0.16.0
|
||||
resolution: "@reach/utils@npm:0.16.0"
|
||||
dependencies:
|
||||
tiny-warning: ^1.0.3
|
||||
tslib: ^2.3.0
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || 17.x
|
||||
react-dom: ^16.8.0 || 17.x
|
||||
checksum: 36bc0eb41a71798eb6186b23de265ba709e51dae5bf214fb8505c66bb3f2e6a41bb2401457350436ba89ca9e3a50f93a04fe7c33d15648ce11e568a85622d770
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/button@npm:3.4.3":
|
||||
version: 3.4.3
|
||||
resolution: "@react-aria/button@npm:3.4.3"
|
||||
@ -15087,6 +15121,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"command-score@npm:^0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "command-score@npm:0.1.2"
|
||||
checksum: b733fd552d7e569070da3d474b1ed5f54785fdf3dd61670002e0a00b2eff1a547c2b6d3af3683c012f4f39c6455f9e7ee5e9997a79c08048ec37ec2195d3df08
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:*, commander@npm:8.3.0, commander@npm:^8.0.0, commander@npm:^8.1.0, commander@npm:^8.3.0":
|
||||
version: 8.3.0
|
||||
resolution: "commander@npm:8.3.0"
|
||||
@ -18998,6 +19039,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-equals@npm:^2.0.3":
|
||||
version: 2.0.4
|
||||
resolution: "fast-equals@npm:2.0.4"
|
||||
checksum: 1aac8a2e16b33e5e402bb5cd46be65f1ca331903c2c44e3bd75324e8472ee04f0acdbc6889e45fc28f9707ca3964f2a86789b32305d4d8b85dc97f61e446ef4b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-glob@npm:^2.2.6":
|
||||
version: 2.2.7
|
||||
resolution: "fast-glob@npm:2.2.7"
|
||||
@ -20585,6 +20633,7 @@ __metadata:
|
||||
js-yaml: ^4.1.0
|
||||
json-source-map: 0.6.1
|
||||
jsurl: ^0.1.5
|
||||
kbar: ^0.1.0-beta.30
|
||||
lerna: ^4.0.0
|
||||
lezer-promql: 0.22.0
|
||||
lint-staged: 12.3.7
|
||||
@ -24493,6 +24542,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"kbar@npm:^0.1.0-beta.30":
|
||||
version: 0.1.0-beta.33
|
||||
resolution: "kbar@npm:0.1.0-beta.33"
|
||||
dependencies:
|
||||
"@reach/portal": ^0.16.0
|
||||
command-score: ^0.1.2
|
||||
fast-equals: ^2.0.3
|
||||
react-virtual: ^2.8.2
|
||||
tiny-invariant: ^1.2.0
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0
|
||||
checksum: 545720900a046501157f8aab9530f62dec356438b85f728968fdc08192cc65afd903cd27e3a0c0f92a74ad6d15d808f5eb12c8b6a801cf881208e3dc2594eb8b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"keycharm@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "keycharm@npm:0.2.0"
|
||||
@ -31317,6 +31382,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-virtual@npm:^2.8.2":
|
||||
version: 2.10.4
|
||||
resolution: "react-virtual@npm:2.10.4"
|
||||
dependencies:
|
||||
"@reach/observe-rect": ^1.1.0
|
||||
peerDependencies:
|
||||
react: ^16.6.3 || ^17.0.0
|
||||
checksum: 1bebc741b01057829a7d7f29256114caecf0597d41b187cb41e75af77f24a87c780bc1a81ec11205b78ee2e9c801fc5e36b20a9e1ab7ddc70a18dd95417795f8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-virtualized-auto-sizer@npm:1.0.6":
|
||||
version: 1.0.6
|
||||
resolution: "react-virtualized-auto-sizer@npm:1.0.6"
|
||||
@ -34919,6 +34995,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tiny-invariant@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "tiny-invariant@npm:1.2.0"
|
||||
checksum: e09a718a7c4a499ba592cdac61f015d87427a0867ca07f50c11fd9b623f90cdba18937b515d4a5e4f43dac92370498d7bdaee0d0e7a377a61095e02c4a92eade
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tiny-warning@npm:^0.0.3":
|
||||
version: 0.0.3
|
||||
resolution: "tiny-warning@npm:0.0.3"
|
||||
|
Loading…
Reference in New Issue
Block a user