ResultItem.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import { css } from '@emotion/css';
  2. import { ActionId, ActionImpl } from 'kbar';
  3. import React from 'react';
  4. import { GrafanaTheme2 } from '@grafana/data';
  5. import { useTheme2 } from '@grafana/ui';
  6. export const ResultItem = React.forwardRef(
  7. (
  8. {
  9. action,
  10. active,
  11. currentRootActionId,
  12. }: {
  13. action: ActionImpl;
  14. active: boolean;
  15. currentRootActionId: ActionId;
  16. },
  17. ref: React.Ref<HTMLDivElement>
  18. ) => {
  19. const ancestors = React.useMemo(() => {
  20. if (!currentRootActionId) {
  21. return action.ancestors;
  22. }
  23. const index = action.ancestors.findIndex((ancestor) => ancestor.id === currentRootActionId);
  24. // +1 removes the currentRootAction; e.g.
  25. // if we are on the "Set theme" parent action,
  26. // the UI should not display "Set theme… > Dark"
  27. // but rather just "Dark"
  28. return action.ancestors.slice(index + 1);
  29. }, [action.ancestors, currentRootActionId]);
  30. const theme = useTheme2();
  31. const styles = getResultItemStyles(theme, active);
  32. return (
  33. <div ref={ref} className={styles.row}>
  34. <div className={styles.actionContainer}>
  35. {action.icon}
  36. <div className={styles.textContainer}>
  37. <div>
  38. {ancestors.length > 0 &&
  39. ancestors.map((ancestor) => (
  40. <React.Fragment key={ancestor.id}>
  41. <span className={styles.breadcrumbAncestor}>{ancestor.name}</span>
  42. <span className={styles.breadcrumbAncestor}>&rsaquo;</span>
  43. </React.Fragment>
  44. ))}
  45. <span>{action.name}</span>
  46. </div>
  47. </div>
  48. {action.subtitle && <span className={styles.subtitleText}>{action.subtitle}</span>}
  49. </div>
  50. {action.shortcut?.length ? (
  51. <div aria-hidden className={styles.shortcutContainer}>
  52. {action.shortcut.map((sc) => (
  53. <kbd key={sc} className={styles.shortcut}>
  54. {sc}
  55. </kbd>
  56. ))}
  57. </div>
  58. ) : null}
  59. </div>
  60. );
  61. }
  62. );
  63. ResultItem.displayName = 'ResultItem';
  64. const getResultItemStyles = (theme: GrafanaTheme2, isActive: boolean) => {
  65. const textColor = isActive ? theme.colors.text.maxContrast : theme.colors.text.primary;
  66. const rowBackgroundColor = isActive ? theme.colors.background.primary : 'transparent';
  67. const shortcutBackgroundColor = isActive ? theme.colors.background.secondary : theme.colors.background.primary;
  68. return {
  69. row: css({
  70. color: textColor,
  71. padding: theme.spacing(1, 2),
  72. background: rowBackgroundColor,
  73. display: 'flex',
  74. alightItems: 'center',
  75. justifyContent: 'space-between',
  76. cursor: 'pointer',
  77. '&:before': {
  78. display: isActive ? 'block' : 'none',
  79. content: '" "',
  80. position: 'absolute',
  81. left: 0,
  82. top: 0,
  83. bottom: 0,
  84. width: theme.spacing(0.5),
  85. borderRadius: theme.shape.borderRadius(1),
  86. backgroundImage: theme.colors.gradients.brandVertical,
  87. },
  88. }),
  89. actionContainer: css({
  90. display: 'flex',
  91. gap: theme.spacing(2),
  92. alignitems: 'center',
  93. fontsize: theme.typography.fontSize,
  94. }),
  95. textContainer: css({
  96. display: 'flex',
  97. flexDirection: 'column',
  98. }),
  99. shortcut: css({
  100. padding: theme.spacing(0, 1),
  101. background: shortcutBackgroundColor,
  102. borderRadius: theme.shape.borderRadius(),
  103. fontsize: theme.typography.fontSize,
  104. }),
  105. breadcrumbAncestor: css({
  106. opacity: 0.5,
  107. marginRight: theme.spacing(1),
  108. }),
  109. subtitleText: css({
  110. fontSize: theme.typography.fontSize - 2,
  111. }),
  112. shortcutContainer: css({
  113. display: 'grid',
  114. gridAutoFlow: 'column',
  115. gap: theme.spacing(1),
  116. }),
  117. };
  118. };