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
|
# Experimental Explore to Dashboard workflow
|
||||||
explore2Dashboard = true
|
explore2Dashboard = true
|
||||||
|
|
||||||
|
# Experimental Command Palette
|
||||||
|
commandPalette = true
|
||||||
|
|
||||||
# feature1 = true
|
# feature1 = true
|
||||||
# feature2 = false
|
# feature2 = false
|
||||||
|
|
||||||
|
@ -319,6 +319,7 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json-source-map": "0.6.1",
|
"json-source-map": "0.6.1",
|
||||||
"jsurl": "^0.1.5",
|
"jsurl": "^0.1.5",
|
||||||
|
"kbar": "^0.1.0-beta.30",
|
||||||
"lezer-promql": "0.22.0",
|
"lezer-promql": "0.22.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"logfmt": "^1.3.2",
|
"logfmt": "^1.3.2",
|
||||||
|
@ -55,4 +55,5 @@ export interface FeatureToggles {
|
|||||||
azureMonitorResourcePickerForMetrics?: boolean;
|
azureMonitorResourcePickerForMetrics?: boolean;
|
||||||
explore2Dashboard?: boolean;
|
explore2Dashboard?: boolean;
|
||||||
persistNotifications?: boolean;
|
persistNotifications?: boolean;
|
||||||
|
commandPalette?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -219,5 +219,10 @@ var (
|
|||||||
State: FeatureStateAlpha,
|
State: FeatureStateAlpha,
|
||||||
FrontendOnly: true,
|
FrontendOnly: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "commandPalette",
|
||||||
|
Description: "Enable command palette",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -162,4 +162,8 @@ const (
|
|||||||
// FlagPersistNotifications
|
// FlagPersistNotifications
|
||||||
// PoC Notifications page
|
// PoC Notifications page
|
||||||
FlagPersistNotifications = "persistNotifications"
|
FlagPersistNotifications = "persistNotifications"
|
||||||
|
|
||||||
|
// FlagCommandPalette
|
||||||
|
// Enable command palette
|
||||||
|
FlagCommandPalette = "commandPalette"
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { ComponentType } from 'react';
|
import React, { ComponentType } from 'react';
|
||||||
import { Router, Route, Redirect, Switch } from 'react-router-dom';
|
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 { Provider } from 'react-redux';
|
||||||
import { store } from 'app/store/store';
|
import { store } from 'app/store/store';
|
||||||
import { ErrorBoundaryAlert, GlobalStyles, ModalRoot, ModalsProvider, PortalContainer } from '@grafana/ui';
|
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 { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
|
||||||
import { SearchWrapper } from 'app/features/search';
|
import { SearchWrapper } from 'app/features/search';
|
||||||
import { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
|
import { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
|
||||||
|
import { Action, KBarProvider } from 'kbar';
|
||||||
|
import { CommandPalette } from './features/commandPalette/CommandPalette';
|
||||||
import { I18nProvider } from './core/localisation';
|
import { I18nProvider } from './core/localisation';
|
||||||
import { AngularRoot } from './angular/AngularRoot';
|
import { AngularRoot } from './angular/AngularRoot';
|
||||||
import { loadAndInitAngularIfEnabled } from './angular/loadAndInitAngularIfEnabled';
|
import { loadAndInitAngularIfEnabled } from './angular/loadAndInitAngularIfEnabled';
|
||||||
@ -85,36 +87,48 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
|||||||
|
|
||||||
const newNavigationEnabled = Boolean(config.featureToggles.newNavigation);
|
const newNavigationEnabled = Boolean(config.featureToggles.newNavigation);
|
||||||
|
|
||||||
|
const commandPaletteActionSelected = (action: Action) => {
|
||||||
|
reportInteraction('commandPalette_action_selected', {
|
||||||
|
actionId: action.id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ErrorBoundaryAlert style="page">
|
<ErrorBoundaryAlert style="page">
|
||||||
<ConfigContext.Provider value={config}>
|
<ConfigContext.Provider value={config}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ModalsProvider>
|
<KBarProvider
|
||||||
<GlobalStyles />
|
actions={[]}
|
||||||
<div className="grafana-app">
|
options={{ enableHistory: true, callbacks: { onSelectAction: commandPaletteActionSelected } }}
|
||||||
<Router history={locationService.getHistory()}>
|
>
|
||||||
{ready && <>{newNavigationEnabled ? <NavBarNext /> : <NavBar />}</>}
|
<ModalsProvider>
|
||||||
<main className="main-view">
|
<GlobalStyles />
|
||||||
{pageBanners.map((Banner, index) => (
|
{config.featureToggles.commandPalette && <CommandPalette />}
|
||||||
<Banner key={index.toString()} />
|
<div className="grafana-app">
|
||||||
))}
|
<Router history={locationService.getHistory()}>
|
||||||
|
{ready && <>{newNavigationEnabled ? <NavBarNext /> : <NavBar />}</>}
|
||||||
|
<main className="main-view">
|
||||||
|
{pageBanners.map((Banner, index) => (
|
||||||
|
<Banner key={index.toString()} />
|
||||||
|
))}
|
||||||
|
|
||||||
<AngularRoot />
|
<AngularRoot />
|
||||||
<AppNotificationList />
|
<AppNotificationList />
|
||||||
<SearchWrapper />
|
<SearchWrapper />
|
||||||
{ready && this.renderRoutes()}
|
{ready && this.renderRoutes()}
|
||||||
{bodyRenderHooks.map((Hook, index) => (
|
{bodyRenderHooks.map((Hook, index) => (
|
||||||
<Hook key={index.toString()} />
|
<Hook key={index.toString()} />
|
||||||
))}
|
))}
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
</div>
|
</div>
|
||||||
<LiveConnectionWarning />
|
<LiveConnectionWarning />
|
||||||
<ModalRoot />
|
<ModalRoot />
|
||||||
<PortalContainer />
|
<PortalContainer />
|
||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
|
</KBarProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
</ErrorBoundaryAlert>
|
</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 { ExplorePaneContainer } from './ExplorePaneContainer';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
import { Branding } from '../../core/components/Branding/Branding';
|
import { Branding } from '../../core/components/Branding/Branding';
|
||||||
|
import { ExploreActions } from './ExploreActions';
|
||||||
|
|
||||||
import { getNavModel } from '../../core/selectors/navModel';
|
import { getNavModel } from '../../core/selectors/navModel';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
@ -69,6 +70,7 @@ class WrapperUnconnected extends PureComponent<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-scrollbar-wrapper">
|
<div className="page-scrollbar-wrapper">
|
||||||
|
<ExploreActions exploreIdLeft={ExploreId.left} exploreIdRight={ExploreId.right} />
|
||||||
<div className="explore-wrapper">
|
<div className="explore-wrapper">
|
||||||
<ErrorBoundaryAlert style="page">
|
<ErrorBoundaryAlert style="page">
|
||||||
<ExplorePaneContainer split={hasSplit} exploreId={ExploreId.left} urlQuery={left} />
|
<ExplorePaneContainer split={hasSplit} exploreId={ExploreId.left} urlQuery={left} />
|
||||||
|
83
yarn.lock
83
yarn.lock
@ -6881,6 +6881,40 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@react-aria/button@npm:3.4.3":
|
||||||
version: 3.4.3
|
version: 3.4.3
|
||||||
resolution: "@react-aria/button@npm:3.4.3"
|
resolution: "@react-aria/button@npm:3.4.3"
|
||||||
@ -15087,6 +15121,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"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
|
version: 8.3.0
|
||||||
resolution: "commander@npm:8.3.0"
|
resolution: "commander@npm:8.3.0"
|
||||||
@ -18998,6 +19039,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"fast-glob@npm:^2.2.6":
|
||||||
version: 2.2.7
|
version: 2.2.7
|
||||||
resolution: "fast-glob@npm:2.2.7"
|
resolution: "fast-glob@npm:2.2.7"
|
||||||
@ -20585,6 +20633,7 @@ __metadata:
|
|||||||
js-yaml: ^4.1.0
|
js-yaml: ^4.1.0
|
||||||
json-source-map: 0.6.1
|
json-source-map: 0.6.1
|
||||||
jsurl: ^0.1.5
|
jsurl: ^0.1.5
|
||||||
|
kbar: ^0.1.0-beta.30
|
||||||
lerna: ^4.0.0
|
lerna: ^4.0.0
|
||||||
lezer-promql: 0.22.0
|
lezer-promql: 0.22.0
|
||||||
lint-staged: 12.3.7
|
lint-staged: 12.3.7
|
||||||
@ -24493,6 +24542,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"keycharm@npm:^0.2.0":
|
||||||
version: 0.2.0
|
version: 0.2.0
|
||||||
resolution: "keycharm@npm:0.2.0"
|
resolution: "keycharm@npm:0.2.0"
|
||||||
@ -31317,6 +31382,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-virtualized-auto-sizer@npm:1.0.6":
|
||||||
version: 1.0.6
|
version: 1.0.6
|
||||||
resolution: "react-virtualized-auto-sizer@npm:1.0.6"
|
resolution: "react-virtualized-auto-sizer@npm:1.0.6"
|
||||||
@ -34919,6 +34995,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"tiny-warning@npm:^0.0.3":
|
||||||
version: 0.0.3
|
version: 0.0.3
|
||||||
resolution: "tiny-warning@npm:0.0.3"
|
resolution: "tiny-warning@npm:0.0.3"
|
||||||
|
Loading…
Reference in New Issue
Block a user