SearchCardExpanded.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { css } from '@emotion/css';
  2. import classNames from 'classnames';
  3. import React, { useState } from 'react';
  4. import SVG from 'react-inlinesvg';
  5. import { GrafanaTheme2 } from '@grafana/data';
  6. import { Icon, Spinner, TagList, useTheme2 } from '@grafana/ui';
  7. import { DashboardSectionItem } from '../types';
  8. import { getThumbnailURL } from './SearchCard';
  9. export interface Props {
  10. className?: string;
  11. imageHeight: number;
  12. imageWidth: number;
  13. item: DashboardSectionItem;
  14. lastUpdated?: string | null;
  15. }
  16. export function SearchCardExpanded({ className, imageHeight, imageWidth, item, lastUpdated }: Props) {
  17. const theme = useTheme2();
  18. const [hasImage, setHasImage] = useState(true);
  19. const imageSrc = getThumbnailURL(item.uid!, theme.isLight);
  20. const styles = getStyles(theme, imageHeight, imageWidth);
  21. const folderTitle = item.folderTitle || 'General';
  22. return (
  23. <a className={classNames(className, styles.card)} key={item.uid} href={item.url}>
  24. <div className={styles.imageContainer}>
  25. {hasImage ? (
  26. <img
  27. loading="lazy"
  28. className={styles.image}
  29. src={imageSrc}
  30. onLoad={() => setHasImage(true)}
  31. onError={() => setHasImage(false)}
  32. />
  33. ) : (
  34. <div className={styles.imagePlaceholder}>
  35. {item.icon ? (
  36. <SVG src={item.icon} width={36} height={36} title={item.title} />
  37. ) : (
  38. <Icon name="apps" size="xl" />
  39. )}
  40. </div>
  41. )}
  42. </div>
  43. <div className={styles.info}>
  44. <div className={styles.infoHeader}>
  45. <div className={styles.titleContainer}>
  46. <div>{item.title}</div>
  47. <div className={styles.folder}>
  48. <Icon name={'folder'} />
  49. {folderTitle}
  50. </div>
  51. </div>
  52. {lastUpdated !== null && (
  53. <div className={styles.updateContainer}>
  54. <div>Last updated</div>
  55. {lastUpdated ? <div className={styles.update}>{lastUpdated}</div> : <Spinner />}
  56. </div>
  57. )}
  58. </div>
  59. <div>
  60. <TagList className={styles.tagList} tags={item.tags} />
  61. </div>
  62. </div>
  63. </a>
  64. );
  65. }
  66. const getStyles = (theme: GrafanaTheme2, imageHeight: Props['imageHeight'], imageWidth: Props['imageWidth']) => {
  67. const IMAGE_HORIZONTAL_MARGIN = theme.spacing(4);
  68. return {
  69. card: css`
  70. background-color: ${theme.colors.background.secondary};
  71. border: 1px solid ${theme.colors.border.medium};
  72. border-radius: 4px;
  73. box-shadow: ${theme.shadows.z3};
  74. display: flex;
  75. flex-direction: column;
  76. height: 100%;
  77. max-width: calc(${imageWidth}px + (${IMAGE_HORIZONTAL_MARGIN} * 2))};
  78. width: 100%;
  79. `,
  80. folder: css`
  81. align-items: center;
  82. color: ${theme.colors.text.secondary};
  83. display: flex;
  84. font-size: ${theme.typography.size.sm};
  85. gap: ${theme.spacing(0.5)};
  86. `,
  87. image: css`
  88. box-shadow: ${theme.shadows.z2};
  89. height: ${imageHeight}px;
  90. margin: ${theme.spacing(1)} calc(${IMAGE_HORIZONTAL_MARGIN} - 1px) 0;
  91. width: ${imageWidth}px;
  92. `,
  93. imageContainer: css`
  94. flex: 1;
  95. position: relative;
  96. &:after {
  97. background: linear-gradient(180deg, rgba(196, 196, 196, 0) 0%, rgba(127, 127, 127, 0.25) 100%);
  98. bottom: 0;
  99. content: '';
  100. left: 0;
  101. margin: ${theme.spacing(1)} calc(${IMAGE_HORIZONTAL_MARGIN} - 1px) 0;
  102. position: absolute;
  103. right: 0;
  104. top: 0;
  105. }
  106. `,
  107. imagePlaceholder: css`
  108. align-items: center;
  109. color: ${theme.colors.text.secondary};
  110. display: flex;
  111. height: ${imageHeight}px;
  112. justify-content: center;
  113. margin: ${theme.spacing(1)} ${IMAGE_HORIZONTAL_MARGIN} 0;
  114. width: ${imageWidth}px;
  115. `,
  116. info: css`
  117. background-color: ${theme.colors.background.canvas};
  118. border-bottom-left-radius: 4px;
  119. border-bottom-right-radius: 4px;
  120. display: flex;
  121. flex-direction: column;
  122. min-height: ${theme.spacing(7)};
  123. gap: ${theme.spacing(1)};
  124. padding: ${theme.spacing(1)} ${theme.spacing(2)};
  125. z-index: 1;
  126. `,
  127. infoHeader: css`
  128. display: flex;
  129. gap: ${theme.spacing(1)};
  130. justify-content: space-between;
  131. `,
  132. tagList: css`
  133. justify-content: flex-start;
  134. `,
  135. titleContainer: css`
  136. display: flex;
  137. flex-direction: column;
  138. gap: ${theme.spacing(0.5)};
  139. `,
  140. updateContainer: css`
  141. align-items: flex-end;
  142. display: flex;
  143. flex-direction: column;
  144. flex-shrink: 0;
  145. font-size: ${theme.typography.bodySmall.fontSize};
  146. gap: ${theme.spacing(0.5)};
  147. `,
  148. update: css`
  149. color: ${theme.colors.text.secondary};
  150. text-align: right;
  151. `,
  152. };
  153. };