mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
	
	
		
			238 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
		
		
			
		
	
	
			238 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
|  | import { css } from '@emotion/css'; | ||
|  | 
 | ||
|  | import { GrafanaTheme2, renderMarkdown } from '@grafana/data'; | ||
|  | import { Trans } from '@grafana/i18n'; | ||
|  | import { CodeEditor, Drawer, useStyles2, Stack, Button, Card, Text, ClipboardButton } from '@grafana/ui'; | ||
|  | 
 | ||
|  | import { parseSuggestion } from './utils'; | ||
|  | 
 | ||
|  | interface AISuggestionsDrawerProps { | ||
|  |   isOpen: boolean; | ||
|  |   onApplySuggestion: (suggestion: string) => void; | ||
|  |   onClose: () => void; | ||
|  |   suggestions: string[]; | ||
|  | } | ||
|  | 
 | ||
|  | export const GenAISuggestionsDrawer = ({ | ||
|  |   isOpen, | ||
|  |   onApplySuggestion, | ||
|  |   onClose, | ||
|  |   suggestions, | ||
|  | }: AISuggestionsDrawerProps) => { | ||
|  |   const styles = useStyles2(getStyles); | ||
|  | 
 | ||
|  |   if (!isOpen) { | ||
|  |     return null; | ||
|  |   } | ||
|  | 
 | ||
|  |   return ( | ||
|  |     <Drawer | ||
|  |       onClose={onClose} | ||
|  |       size="lg" | ||
|  |       title={<Trans i18nKey="sql-expressions.sql-suggestion-history">SQL Suggestion History</Trans>} | ||
|  |     > | ||
|  |       <div className={styles.content} data-testid="suggestions-drawer"> | ||
|  |         <Stack direction="column" gap={3}> | ||
|  |           <div className={styles.timelineContainer}> | ||
|  |             {/* Vertical timeline line */} | ||
|  |             <div className={styles.timelineLine} /> | ||
|  | 
 | ||
|  |             <div className={styles.suggestionsList}> | ||
|  |               {suggestions.map((suggestion, index) => { | ||
|  |                 const parsedSuggestion = parseSuggestion(suggestion); | ||
|  |                 const isLatest = index === 0; | ||
|  | 
 | ||
|  |                 return ( | ||
|  |                   <div key={index} className={styles.timelineItem}> | ||
|  |                     {/* Timeline node */} | ||
|  |                     <div | ||
|  |                       className={`${styles.timelineNode} ${isLatest ? styles.timelineNodeActive : styles.timelineNodeInactive}`} | ||
|  |                     /> | ||
|  |                     <Card noMargin key={index} className={isLatest ? styles.latestSuggestion : ''}> | ||
|  |                       <div className={styles.suggestionContent}> | ||
|  |                         {parsedSuggestion.map(({ type, content, language }, partIndex) => ( | ||
|  |                           <div key={partIndex} className={styles.suggestionPart}> | ||
|  |                             {type === 'code' ? ( | ||
|  |                               <div className={styles.codeBlock}> | ||
|  |                                 <div className={styles.codeHeader}> | ||
|  |                                   <Stack direction="row" justifyContent="space-between" alignItems="center"> | ||
|  |                                     <Text variant="bodySmall" weight="bold"> | ||
|  |                                       <Trans | ||
|  |                                         i18nKey="sql-expressions.code-label" | ||
|  |                                         values={{ language: language?.toUpperCase() || 'CODE' }} | ||
|  |                                       > | ||
|  |                                         {'{{ language }}'} | ||
|  |                                       </Trans> | ||
|  |                                     </Text> | ||
|  |                                     <Stack direction="row" gap={1}> | ||
|  |                                       <ClipboardButton | ||
|  |                                         size="sm" | ||
|  |                                         icon="copy" | ||
|  |                                         variant="secondary" | ||
|  |                                         getText={() => content} | ||
|  |                                       > | ||
|  |                                         <Trans i18nKey="sql-expressions.copy">Copy</Trans> | ||
|  |                                       </ClipboardButton> | ||
|  |                                       <Button | ||
|  |                                         size="sm" | ||
|  |                                         variant="primary" | ||
|  |                                         icon="ai-sparkle" | ||
|  |                                         onClick={() => onApplySuggestion(content)} | ||
|  |                                       > | ||
|  |                                         <Trans i18nKey="sql-expressions.apply">Apply</Trans> | ||
|  |                                       </Button> | ||
|  |                                     </Stack> | ||
|  |                                   </Stack> | ||
|  |                                 </div> | ||
|  |                                 <CodeEditor | ||
|  |                                   value={content} | ||
|  |                                   language={language === 'sql' || language === 'mysql' ? 'mysql' : 'sql'} | ||
|  |                                   width="100%" | ||
|  |                                   height={Math.max(80, Math.min(300, (content.split('\n').length + 1) * 20))} | ||
|  |                                   readOnly={true} | ||
|  |                                   showMiniMap={false} | ||
|  |                                   showLineNumbers={true} | ||
|  |                                   monacoOptions={{ | ||
|  |                                     lineNumbers: 'on', | ||
|  |                                     folding: false, | ||
|  |                                     minimap: { enabled: false }, | ||
|  |                                     scrollBeyondLastLine: false, | ||
|  |                                     renderLineHighlight: 'none', | ||
|  |                                     wordWrap: 'on', | ||
|  |                                     readOnly: true, | ||
|  |                                     contextmenu: false, | ||
|  |                                     padding: { top: 8, bottom: 8 }, | ||
|  |                                     automaticLayout: true, | ||
|  |                                     fontSize: 13, | ||
|  |                                     lineHeight: 20, | ||
|  |                                   }} | ||
|  |                                 /> | ||
|  |                               </div> | ||
|  |                             ) : ( | ||
|  |                               <div | ||
|  |                                 className="markdown-html" | ||
|  |                                 dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }} | ||
|  |                               /> | ||
|  |                             )} | ||
|  |                           </div> | ||
|  |                         ))} | ||
|  |                       </div> | ||
|  |                     </Card> | ||
|  |                   </div> | ||
|  |                 ); | ||
|  |               })} | ||
|  |             </div> | ||
|  |           </div> | ||
|  |         </Stack> | ||
|  |       </div> | ||
|  |     </Drawer> | ||
|  |   ); | ||
|  | }; | ||
|  | 
 | ||
|  | const getStyles = (theme: GrafanaTheme2) => ({ | ||
|  |   content: css({ | ||
|  |     height: '100%', | ||
|  |     display: 'flex', | ||
|  |     flexDirection: 'column', | ||
|  |   }), | ||
|  |   emptyState: css({ | ||
|  |     display: 'flex', | ||
|  |     flexDirection: 'column', | ||
|  |     alignItems: 'center', | ||
|  |     justifyContent: 'center', | ||
|  |     height: '100%', | ||
|  |     textAlign: 'center', | ||
|  |     gap: theme.spacing(2), | ||
|  |   }), | ||
|  |   timelineContainer: css({ | ||
|  |     position: 'relative', | ||
|  |     display: 'flex', | ||
|  |     flexDirection: 'column', | ||
|  |     overflow: 'auto', | ||
|  |     flex: 1, | ||
|  |     paddingLeft: theme.spacing(4.5), // Space for timeline line and nodes
 | ||
|  |   }), | ||
|  |   timelineLine: css({ | ||
|  |     position: 'absolute', | ||
|  |     // Offset the 2px width of the timeline line
 | ||
|  |     left: `calc(${theme.spacing(1)} + 2px)`, | ||
|  |     top: theme.spacing(1), | ||
|  |     bottom: 0, | ||
|  |     width: '2px', | ||
|  |     backgroundColor: theme.colors.border.strong, | ||
|  |     zIndex: 1, | ||
|  |   }), | ||
|  |   suggestionsList: css({ | ||
|  |     display: 'flex', | ||
|  |     flexDirection: 'column', | ||
|  |     gap: theme.spacing(2), | ||
|  |     position: 'relative', | ||
|  |   }), | ||
|  |   timelineItem: css({ | ||
|  |     position: 'relative', | ||
|  |     display: 'flex', | ||
|  |     alignItems: 'flex-start', | ||
|  |     gap: theme.spacing(2), | ||
|  |   }), | ||
|  |   timelineNode: css({ | ||
|  |     position: 'absolute', | ||
|  |     left: theme.spacing(-4.5), // Position on the timeline line
 | ||
|  |     top: theme.spacing(1), // Align with card content
 | ||
|  |     width: theme.spacing(3), | ||
|  |     height: theme.spacing(3), | ||
|  |     borderRadius: theme.shape.radius.pill, | ||
|  |     border: `2px solid ${theme.colors.primary.main}`, | ||
|  |     backgroundColor: theme.colors.background.primary, | ||
|  |     zIndex: 2, | ||
|  |     flexShrink: 0, | ||
|  |   }), | ||
|  |   timelineNodeActive: css({ | ||
|  |     backgroundColor: theme.colors.primary.main, // Filled circle for current/latest
 | ||
|  |     boxShadow: `0 0 0 4px ${theme.colors.background.primary}`, // White ring around filled circle
 | ||
|  |   }), | ||
|  |   timelineNodeInactive: css({ | ||
|  |     backgroundColor: theme.colors.background.primary, // Empty circle for others
 | ||
|  |     boxShadow: `0 0 0 4px ${theme.colors.background.primary}`, // White ring around filled circle
 | ||
|  |   }), | ||
|  |   latestSuggestion: css({ | ||
|  |     border: `2px solid ${theme.colors.primary.main}`, | ||
|  |     position: 'relative', | ||
|  |     '&::before': { | ||
|  |       content: '"Latest"', | ||
|  |       position: 'absolute', | ||
|  |       top: theme.spacing(-0.5), | ||
|  |       right: theme.spacing(1), | ||
|  |       backgroundColor: theme.colors.primary.main, | ||
|  |       color: theme.colors.primary.contrastText, | ||
|  |       padding: theme.spacing(0.25, 1), | ||
|  |       borderRadius: theme.shape.radius.default, | ||
|  |       fontSize: theme.typography.bodySmall.fontSize, | ||
|  |       fontWeight: theme.typography.fontWeightMedium, | ||
|  |     }, | ||
|  |   }), | ||
|  |   suggestionContent: css({ | ||
|  |     display: 'flex', | ||
|  |     flexDirection: 'column', | ||
|  |     gap: theme.spacing(1.5), | ||
|  |     width: '100%', | ||
|  |     overflowX: 'auto', | ||
|  |   }), | ||
|  |   suggestionPart: css({ | ||
|  |     display: 'block', | ||
|  |     width: '100%', | ||
|  |   }), | ||
|  |   codeBlock: css({ | ||
|  |     border: `1px solid ${theme.colors.border.medium}`, | ||
|  |     borderRadius: theme.shape.radius.default, | ||
|  |     overflow: 'hidden', | ||
|  |     marginBottom: theme.spacing(1), | ||
|  |     width: '100%', | ||
|  |     minWidth: '600px', | ||
|  |   }), | ||
|  |   codeHeader: css({ | ||
|  |     backgroundColor: theme.colors.background.secondary, | ||
|  |     padding: theme.spacing(1, 1.5), | ||
|  |     borderBottom: `1px solid ${theme.colors.border.weak}`, | ||
|  |   }), | ||
|  | }); |