CommandPalette.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { css } from '@emotion/css';
  2. import { FocusScope } from '@react-aria/focus';
  3. import {
  4. KBarAnimator,
  5. KBarPortal,
  6. KBarPositioner,
  7. KBarResults,
  8. KBarSearch,
  9. useMatches,
  10. Action,
  11. VisualState,
  12. useRegisterActions,
  13. useKBar,
  14. } from 'kbar';
  15. import React, { useEffect, useState } from 'react';
  16. import { useSelector } from 'react-redux';
  17. import { GrafanaTheme2 } from '@grafana/data';
  18. import { reportInteraction, locationService } from '@grafana/runtime';
  19. import { useStyles2 } from '@grafana/ui';
  20. import { StoreState } from 'app/types';
  21. import { keybindingSrv } from '../../core/services/keybindingSrv';
  22. import { ResultItem } from './ResultItem';
  23. import getDashboardNavActions from './actions/dashboard.nav.actions';
  24. import getGlobalActions from './actions/global.static.actions';
  25. /**
  26. * Wrap all the components from KBar here.
  27. * @constructor
  28. */
  29. export const CommandPalette = () => {
  30. const styles = useStyles2(getSearchStyles);
  31. const [actions, setActions] = useState<Action[]>([]);
  32. const { query, showing } = useKBar((state) => ({
  33. showing: state.visualState === VisualState.showing,
  34. }));
  35. const isNotLogin = locationService.getLocation().pathname !== '/login';
  36. const { navBarTree } = useSelector((state: StoreState) => {
  37. return {
  38. navBarTree: state.navBarTree,
  39. };
  40. });
  41. useEffect(() => {
  42. (async () => {
  43. if (isNotLogin) {
  44. const staticActions = getGlobalActions(navBarTree);
  45. const dashAct = await getDashboardNavActions('go/dashboard');
  46. setActions([...staticActions, ...dashAct]);
  47. }
  48. })();
  49. // eslint-disable-next-line react-hooks/exhaustive-deps
  50. }, [isNotLogin]);
  51. useEffect(() => {
  52. if (showing) {
  53. reportInteraction('commandPalette_opened');
  54. keybindingSrv.bindGlobal('esc', () => {
  55. query.setVisualState(VisualState.animatingOut);
  56. });
  57. }
  58. return () => {
  59. keybindingSrv.bindGlobal('esc', () => {
  60. keybindingSrv.globalEsc();
  61. });
  62. };
  63. // eslint-disable-next-line react-hooks/exhaustive-deps
  64. }, [showing]);
  65. useRegisterActions(actions, [actions]);
  66. return actions.length > 0 ? (
  67. <KBarPortal>
  68. <KBarPositioner className={styles.positioner}>
  69. <KBarAnimator className={styles.animator}>
  70. <FocusScope contain>
  71. <KBarSearch className={styles.search} />
  72. <RenderResults />
  73. </FocusScope>
  74. </KBarAnimator>
  75. </KBarPositioner>
  76. </KBarPortal>
  77. ) : null;
  78. };
  79. const RenderResults = () => {
  80. const { results, rootActionId } = useMatches();
  81. const styles = useStyles2(getSearchStyles);
  82. return (
  83. <div className={styles.resultsContainer}>
  84. <KBarResults
  85. items={results}
  86. onRender={({ item, active }) =>
  87. typeof item === 'string' ? (
  88. <div className={styles.sectionHeader}>{item}</div>
  89. ) : (
  90. <ResultItem action={item} active={active} currentRootActionId={rootActionId!} />
  91. )
  92. }
  93. />
  94. </div>
  95. );
  96. };
  97. const getSearchStyles = (theme: GrafanaTheme2) => ({
  98. positioner: css({
  99. zIndex: theme.zIndex.portal,
  100. marginTop: '0px',
  101. '&::before': {
  102. content: '""',
  103. position: 'fixed',
  104. top: 0,
  105. right: 0,
  106. bottom: 0,
  107. left: 0,
  108. background: theme.components.overlay.background,
  109. backdropFilter: 'blur(1px)',
  110. },
  111. }),
  112. animator: css({
  113. maxWidth: theme.breakpoints.values.sm, // supposed to be 600...
  114. width: '100%',
  115. background: theme.colors.background.canvas,
  116. color: theme.colors.text.primary,
  117. borderRadius: theme.shape.borderRadius(4),
  118. overflow: 'hidden',
  119. boxShadow: theme.shadows.z3,
  120. }),
  121. search: css({
  122. padding: theme.spacing(2, 3),
  123. fontSize: theme.typography.fontSize,
  124. width: '100%',
  125. boxSizing: 'border-box',
  126. outline: 'none',
  127. border: 'none',
  128. background: theme.colors.background.canvas,
  129. color: theme.colors.text.primary,
  130. borderBottom: `1px solid ${theme.colors.border.weak}`,
  131. }),
  132. sectionHeader: css({
  133. padding: theme.spacing(1, 2),
  134. fontSize: theme.typography.h6.fontSize,
  135. fontWeight: theme.typography.body.fontWeight,
  136. color: theme.colors.text.secondary,
  137. }),
  138. resultsContainer: css({
  139. padding: theme.spacing(2, 0),
  140. }),
  141. });