From 5cf28fd8d6ebc77c9889dbb57e73b9c5b02ed710 Mon Sep 17 00:00:00 2001 From: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:06:54 -0800 Subject: [PATCH] chore: alex - ai demo --- packages/grafana-data/src/types/icon.ts | 1 + public/app/core/icons/cached.json | 1 + .../panel-edit/PanelDataPane/AiModeCard.tsx | 360 ++++++++++++++++++ .../PanelDataPane/PanelDataPane.tsx | 26 +- .../PanelDataPane/QueryTransformList.tsx | 240 +++++++++++- public/img/icons/unicons/ai-pointer.svg | 15 + public/locales/en-US/grafana.json | 46 ++- 7 files changed, 658 insertions(+), 31 deletions(-) create mode 100644 public/app/features/dashboard-scene/panel-edit/PanelDataPane/AiModeCard.tsx create mode 100644 public/img/icons/unicons/ai-pointer.svg diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 20bac62604e..1cb0b3db98a 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -43,6 +43,7 @@ export const availableIconsIndex = { 'expand-arrows-alt': true, at: true, ai: true, + 'ai-pointer': true, backward: true, bars: true, bell: true, diff --git a/public/app/core/icons/cached.json b/public/app/core/icons/cached.json index 82dcbcf114d..a771248ee9f 100644 --- a/public/app/core/icons/cached.json +++ b/public/app/core/icons/cached.json @@ -189,6 +189,7 @@ "unicons/record-audio", "unicons/scim", "solid/bookmark", + "unicons/ai-pointer", "unicons/ai-sparkle", "unicons/dollar-alt", "unicons/window-grid", diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AiModeCard.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AiModeCard.tsx new file mode 100644 index 00000000000..5ce5be1520e --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AiModeCard.tsx @@ -0,0 +1,360 @@ +import { css } from '@emotion/css'; +import { FormEvent, useEffect, useRef, useState } from 'react'; + +import { GrafanaTheme2, IconName } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { Icon, IconButton, Stack, useStyles2 } from '@grafana/ui'; + +import { usePanelDataPaneColors } from './theme'; + +interface ContextPill { + id: string; + label: string; + type: 'query' | 'transform' | 'expression'; + icon: IconName; +} + +interface AiModeCardProps { + selectedContexts: ContextPill[]; + onRemoveContext: (id: string) => void; + onSubmit: (prompt: string) => void; + onDemoWorkflow?: { + availableCardIds: string[]; + onSelectContext: (id: string) => void; + onAddOrganizeFieldsTransformation: () => void; + onCloseAiMode: () => void; + }; +} + +export const AiModeCard = ({ selectedContexts, onRemoveContext, onSubmit, onDemoWorkflow }: AiModeCardProps) => { + const colors = usePanelDataPaneColors(); + const styles = useStyles2(getStyles, colors); + const [prompt, setPrompt] = useState(''); + const editorRef = useRef(null); + const [isRunningDemo, setIsRunningDemo] = useState(false); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + if (prompt.trim()) { + onSubmit(prompt); + setPrompt(''); + if (editorRef.current) { + editorRef.current.textContent = ''; + } + } + }; + + const handleInput = () => { + if (editorRef.current) { + const content = editorRef.current.textContent || ''; + setPrompt(content); + + // Clear the innerHTML if there's no actual text content to show the placeholder + if (!content.trim()) { + editorRef.current.innerHTML = ''; + } + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + if (prompt.trim()) { + handleSubmit(e); + } + } + }; + + // Demo workflow effect - only runs if URL has ?aiDemo=true + useEffect(() => { + if (!onDemoWorkflow || isRunningDemo || onDemoWorkflow.availableCardIds.length === 0) { + return; + } + + // Check for demo URL parameter + const urlParams = new URLSearchParams(window.location.search); + const isDemoEnabled = urlParams.get('aiDemo') === 'true'; + + if (!isDemoEnabled) { + return; + } + + setIsRunningDemo(true); + + const runDemoWorkflow = async () => { + // Wait a bit before starting + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Step 1: Select random cards (1-3) + const numCards = Math.min( + Math.floor(Math.random() * 3) + 1, // Random 1-3 + onDemoWorkflow.availableCardIds.length + ); + + const shuffled = [...onDemoWorkflow.availableCardIds].sort(() => Math.random() - 0.5); + const cardsToSelect = shuffled.slice(0, numCards); + + for (const cardId of cardsToSelect) { + onDemoWorkflow.onSelectContext(cardId); + await new Promise((resolve) => setTimeout(resolve, 300)); + } + + // Wait a bit after selecting cards + await new Promise((resolve) => setTimeout(resolve, 800)); + + // Step 2: Type the message character by character + const message = "I'd like to auto-organize my fields by name in ascending order!"; + for (let i = 0; i <= message.length; i++) { + const partial = message.slice(0, i); + setPrompt(partial); + if (editorRef.current) { + editorRef.current.textContent = partial; + } + await new Promise((resolve) => setTimeout(resolve, 30 + Math.random() * 40)); // Vary speed + } + + // Wait a bit before submitting + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Step 3: Submit and clear input + if (editorRef.current) { + editorRef.current.textContent = ''; + } + setPrompt(''); + + // Wait a moment + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Find the transformations section or last transform card to scroll to first + const transformCards = document.querySelectorAll('[data-card-id^="transform-"]'); + const transformSection = document.querySelector('[data-testid="query-transform-list-content"]'); + + if (transformCards.length > 0) { + // Scroll to the last transform card + const lastTransform = transformCards[transformCards.length - 1]; + lastTransform.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } else if (transformSection) { + // If no transforms yet, scroll to bottom of the section + transformSection.scrollIntoView({ behavior: 'smooth', block: 'end' }); + } + + // Wait for scroll to complete, then add the transformation + await new Promise((resolve) => setTimeout(resolve, 800)); + onDemoWorkflow.onAddOrganizeFieldsTransformation(); + + // Wait a moment to show the new transformation, then close AI mode + await new Promise((resolve) => setTimeout(resolve, 1000)); + onDemoWorkflow.onCloseAiMode(); + }; + + runDemoWorkflow(); + }, [onDemoWorkflow, isRunningDemo]); + + const hasContext = selectedContexts.length > 0; + + return ( +
+ {/* Context Pills Section */} + {hasContext && ( +
+ + {selectedContexts.map((context) => ( +
+ + + {context.label} + + +
+ ))} +
+
+ )} + + {/* Prompt Input Section */} +
+
+ +
+
+ {!hasContext && ( +
+ + {t('dashboard-scene.ai-mode-card.add-nodes-to-context', 'Add nodes to context')} +
+ )} +
+ +
+ +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2, colors: ReturnType) => { + const getColorForType = (type: 'query' | 'transform' | 'expression') => { + switch (type) { + case 'query': + return colors.query.accent; + case 'expression': + return colors.expression.accent; + case 'transform': + return colors.transform.accent; + } + }; + + return { + container: css({ + background: theme.colors.background.primary, + border: 'none', + borderRadius: 'unset', + padding: `0 0 ${theme.spacing(1)} 0`, + width: '100%', + maxWidth: '100%', + overflow: 'hidden', + boxSizing: 'border-box', + }), + contextSection: css({ + marginBottom: theme.spacing(2), + padding: `0 ${theme.spacing(1.5)}`, + }), + badge: css({ + display: 'inline-flex', + alignItems: 'center', + gap: theme.spacing(0.5), + border: 'none', + borderRadius: theme.shape.radius.default, + padding: theme.spacing(0.5, 1.5), + fontSize: theme.typography.bodySmall.fontSize, + fontWeight: theme.typography.fontWeightMedium, + }), + getBadgeStyle: (type: 'query' | 'transform' | 'expression') => ({ + background: `${getColorForType(type)}26`, // 26 in hex = ~15% opacity + }), + badgeLabel: css({}), + getBadgeColor: (type: 'query' | 'transform' | 'expression') => ({ + color: getColorForType(type), + }), + badgeRemove: css({ + background: 'none', + border: 'none', + cursor: 'pointer', + padding: 0, + marginLeft: theme.spacing(0.5), + fontSize: '16px', + lineHeight: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + opacity: 0.7, + '&:hover': { + opacity: 1, + }, + }), + form: css({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1.5), + width: '100%', + minWidth: 0, + }), + input: css({ + width: '100%', + maxWidth: '100%', + minWidth: 0, + boxSizing: 'border-box', + maxHeight: theme.spacing(38), + minHeight: theme.spacing(4), + height: 'auto', + overflow: 'auto', + padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`, + border: 'none', + background: 'transparent', + whiteSpace: 'pre-wrap', + outline: 'none', + wordBreak: 'break-word', + fontSize: '14px', + lineHeight: 1.5, + fontFamily: theme.typography.fontFamily, + color: theme.colors.text.primary, + + '&::-webkit-scrollbar': { + width: '4px', + height: '4px', + }, + '&::-webkit-scrollbar-track': { + background: 'transparent', + }, + '&::-webkit-scrollbar-thumb': { + background: theme.colors.border.weak, + borderRadius: theme.shape.radius.default, + '&:hover': { + background: theme.colors.border.strong, + }, + }, + '&::-webkit-scrollbar-corner': { + background: 'transparent', + }, + scrollbarWidth: 'thin', + scrollbarColor: `${theme.colors.border.weak} transparent`, + + '&:empty:before': { + content: 'attr(data-placeholder)', + color: theme.colors.text.disabled, + pointerEvents: 'none', + display: 'block', + }, + + '&:focus:empty:before': { + color: theme.colors.text.secondary, + }, + }), + bottomRow: css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: theme.spacing(1), + padding: `0 ${theme.spacing(1.5)}`, + }), + leftContent: css({ + flex: 1, + }), + emptyState: css({ + fontSize: theme.typography.fontSize - 2, + color: theme.colors.text.secondary, + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.5), + fontFamily: theme.typography.fontFamilyMonospace, + letterSpacing: '0.05em', + }), + submitButton: css({ + flexShrink: 0, + }), + }; +}; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx index bd583382d60..f93eb89d4df 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -303,7 +303,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { let nextRefId = 'A'; const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; for (let i = 0; i < alphabet.length; i++) { - if (!queries?.some(q => q.refId === alphabet[i])) { + if (!queries?.some((q) => q.refId === alphabet[i])) { nextRefId = alphabet[i]; break; } @@ -325,7 +325,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { /** TRANSFORMS **/ const handleAddTransform = useCallback( - (selected: SelectableValue) => { + (selected: SelectableValue, customOptions?: Record) => { if (!selected.value) { return; } @@ -334,7 +334,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { const selectedIndex = transformDrawerState.index ?? transformations?.length ?? 0; const newTransformation: DataTransformerConfig = { id: selected.value, - options: {}, + options: customOptions ?? {}, }; const unsub = transformer.subscribeToState((newState) => { @@ -436,7 +436,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { selectedId={effectiveSelectedId} onSelect={handleSelect} onAddQuery={handleAddQuery} - onAddFromSavedQueries={index => setSavedQueriesDrawerState({ open: true, index: index ?? null })} + onAddFromSavedQueries={(index) => setSavedQueriesDrawerState({ open: true, index: index ?? null })} onAddTransform={(index) => setTransformDrawerState({ open: true, index: index ?? null })} onAddExpression={handleAddExpression} onDuplicateQuery={handleDuplicateQuery} @@ -446,6 +446,24 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { onToggleTransformVisibility={handleToggleTransformVisibility} onReorderDataSources={handleReorderDataSources} onReorderTransforms={handleReorderTransforms} + onAddOrganizeFieldsTransform={() => + handleAddTransform( + { value: 'organize' }, + { + excludeByName: {}, + indexByName: {}, + renameByName: {}, + includeByName: {}, + orderByMode: 'auto', + orderBy: [ + { + type: 'name', + desc: false, + }, + ], + } + ) + } />
void; onReorderDataSources?: (startIndex: number, endIndex: number) => void; onReorderTransforms?: (startIndex: number, endIndex: number) => void; + onAddOrganizeFieldsTransform?: () => void; } export const QueryTransformList = memo( @@ -58,15 +60,15 @@ export const QueryTransformList = memo( onToggleTransformVisibility, onReorderDataSources, onReorderTransforms, + onAddOrganizeFieldsTransform, }: QueryTransformListProps) => { const styles = useStyles2(getStyles); const [isDragging, setIsDragging] = useState(false); + const [isAiMode, setIsAiMode] = useState(false); + const [isClosing, setIsClosing] = useState(false); + const [selectedContextIds, setSelectedContextIds] = useState([]); const [hovered, setHovered] = useState(null); - useEffect(() => { - console.log('hovered changed:', hovered); - }, [hovered]); - const onDragStart = () => { setIsDragging(true); }; @@ -174,6 +176,69 @@ export const QueryTransformList = memo( }; }, [allItems, dataSourceItems, transformItems]); + const handleCardClick = (id: string) => { + if (isAiMode) { + // Toggle context selection in AI mode + setSelectedContextIds((prev) => (prev.includes(id) ? prev.filter((cid) => cid !== id) : [...prev, id])); + } else { + // Normal selection behavior + onSelect(id); + } + }; + + const selectedContexts = useMemo(() => { + return selectedContextIds + .map((id) => { + const item = allItems.find((i) => i.id === id); + if (!item) { + return null; + } + + let label = ''; + if ((item.type === 'query' || item.type === 'expression') && 'refId' in item.data) { + label = item.data.refId || `${item.type === 'expression' ? 'Expression' : 'Query'} ${item.index + 1}`; + } else if ('id' in item.data) { + label = item.data.id.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase()); + } + + const icon: 'database' | 'code' | 'pivot' = + item.type === 'query' ? 'database' : item.type === 'expression' ? 'code' : 'pivot'; + + return { + id, + label, + type: item.type, + icon, + }; + }) + .filter((c) => c !== null); + }, [selectedContextIds, allItems]); + + const handleRemoveContext = (id: string) => { + setSelectedContextIds((prev) => prev.filter((cid) => cid !== id)); + }; + + const handleAiSubmit = (prompt: string) => { + // TODO: Implement AI prompt submission + console.log('AI Prompt:', prompt); + console.log('Selected contexts:', selectedContexts); + }; + + const handleToggleAiMode = () => { + if (isAiMode) { + // Trigger closing animation + setIsClosing(true); + // Wait for animation to complete before actually closing + setTimeout(() => { + setIsAiMode(false); + setIsClosing(false); + setSelectedContextIds([]); + }, 300); // Match animation duration + } else { + setIsAiMode(true); + } + }; + return (
setHovered(null)}>
@@ -181,14 +246,57 @@ export const QueryTransformList = memo( {t('dashboard-scene.query-transform-list.header', 'Pipeline flow')} +
+ {isAiMode && ( +
+ item.id), + onSelectContext: (id) => { + setSelectedContextIds((prev) => [...prev, id]); + }, + onAddOrganizeFieldsTransformation: onAddOrganizeFieldsTransform, + onCloseAiMode: () => { + setIsClosing(true); + setTimeout(() => { + setIsAiMode(false); + setIsClosing(false); + setSelectedContextIds([]); + }, 300); + }, + } + : undefined + } + /> +
+ )}
-
+
{/* Data Sources Section (Queries + Expressions) */} {dataSourceItems.length > 0 && ( @@ -206,7 +314,7 @@ export const QueryTransformList = memo(
{ + onMouseMove={(ev) => { const rect = ev.currentTarget.getBoundingClientRect(); const y = ev.clientY - rect.top; let hoveredIdx = Math.floor(y / CARD_HEIGHT); @@ -216,10 +324,16 @@ export const QueryTransformList = memo( if (hoveredIdx > dataSourceItems.length) { hoveredIdx = dataSourceItems.length; } - const hoveredId = hoveredIdx === dataSourceItems.length ? 'queries-last' : dataSourceItems[hoveredIdx].id; + const hoveredId = + hoveredIdx === dataSourceItems.length + ? 'queries-last' + : dataSourceItems[hoveredIdx].id; setHovered(hoveredId); }} - className={cx(styles.cardList, isDraggingFromOtherSection ? styles.droppableInvalid : undefined)} + className={cx( + styles.cardList, + isDraggingFromOtherSection ? styles.droppableInvalid : undefined + )} > {dataSourceItems.map((item, index) => ( @@ -236,11 +350,13 @@ export const QueryTransformList = memo( item={item.data} type={item.type} index={item.index} - isSelected={selectedId === item.id} - onClick={() => onSelect(item.id)} - onAddExpression={onAddExpression} + isSelected={ + isAiMode ? selectedContextIds.includes(item.id) : selectedId === item.id + } + onClick={() => handleCardClick(item.id)} onAddQuery={onAddQuery} onAddTransform={onAddTransform} + onAddExpression={onAddExpression} {...getHandlers(item)} />
@@ -279,7 +395,6 @@ export const QueryTransformList = memo( ); }} -
)} @@ -299,18 +414,24 @@ export const QueryTransformList = memo(
{ + className={cx( + styles.cardList, + isDraggingFromOtherSection ? styles.droppableInvalid : undefined + )} + onMouseMove={(ev) => { const rect = ev.currentTarget.getBoundingClientRect(); const y = ev.clientY - rect.top; - let hoveredIdx = Math.floor(((y - 16 + (CARD_HEIGHT / 2)) / CARD_HEIGHT)); + let hoveredIdx = Math.floor((y - 16 + CARD_HEIGHT / 2) / CARD_HEIGHT); if (hoveredIdx < 0) { hoveredIdx = 0; } if (hoveredIdx > transformItems.length) { hoveredIdx = transformItems.length; } - const hoveredId = hoveredIdx === transformItems.length ? 'transformations-last' : transformItems[hoveredIdx].id; + const hoveredId = + hoveredIdx === transformItems.length + ? 'transformations-last' + : transformItems[hoveredIdx].id; setHovered(hoveredId); }} > @@ -329,11 +450,13 @@ export const QueryTransformList = memo( item={item.data} type={item.type} index={item.index} - isSelected={selectedId === item.id} - onClick={() => onSelect(item.id)} - onAddExpression={onAddExpression} + isSelected={ + isAiMode ? selectedContextIds.includes(item.id) : selectedId === item.id + } + onClick={() => handleCardClick(item.id)} onAddQuery={onAddQuery} onAddTransform={onAddTransform} + onAddExpression={onAddExpression} {...getHandlers(item)} />
@@ -427,6 +550,10 @@ const getStyles = (theme: GrafanaTheme2) => { ...barBase, height: headerHeight, borderBottom: `1px solid ${theme.colors.border.weak}`, + + '& > div:first-child': { + width: '100%', + }, }), headerTitle: css({ fontFamily: theme.typography.fontFamilyMonospace, @@ -456,10 +583,63 @@ const getStyles = (theme: GrafanaTheme2) => { position: 'relative', minHeight: '100%', }), + aiModeContent: css({ + padding: theme.spacing(1, 1), + position: 'relative', + zIndex: 15, + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + animation: 'slideDown 0.3s ease-out', + '@keyframes slideDown': { + from: { + opacity: 0, + transform: 'translateY(-20px)', + }, + to: { + opacity: 1, + transform: 'translateY(0)', + }, + }, + }, + }), + aiModeClosing: css({ + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + animation: 'slideUp 0.3s ease-out forwards', + '@keyframes slideUp': { + from: { + opacity: 1, + transform: 'translateY(0)', + }, + to: { + opacity: 0, + transform: 'translateY(-20px)', + }, + }, + }, + }), content: css({ padding: theme.spacing(2, 8, 2, 2), position: 'relative', }), + contentGradientBorder: css({ + borderTop: '2px solid transparent', + borderImage: 'linear-gradient(90deg, #FF9830 0%, #B877D9 100%) 1', + paddingTop: theme.spacing(3), + }), + contentGradientBorderClosing: css({ + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + animation: 'fadeBorderOut 0.3s ease-out forwards', + '@keyframes fadeBorderOut': { + from: { + borderTopColor: 'rgba(255, 152, 48, 1)', + paddingTop: theme.spacing(3), + }, + to: { + borderTopColor: 'transparent', + paddingTop: theme.spacing(2), + }, + }, + }, + }), dragging: css({ opacity: 0.8, cursor: 'grabbing !important', @@ -524,5 +704,23 @@ const getStyles = (theme: GrafanaTheme2) => { top: theme.spacing(-2), left: theme.spacing(-2.5), }), + aiModeButton: css({ + background: 'linear-gradient(90deg, #FF9830 0%, #B877D9 100%)', + border: 'none', + borderRadius: theme.shape.radius.default, + color: '#ffffff', + fontWeight: theme.typography.fontWeightMedium, + fontFamily: theme.typography.fontFamilyMonospace, + textTransform: 'uppercase', + letterSpacing: '0.05em', + '&:hover': { + background: 'linear-gradient(90deg, #FFB050 0%, #C88FE5 100%)', + boxShadow: '0 4px 12px rgba(255, 152, 48, 0.4)', + }, + '&:focus, &:active, &:focus:active': { + background: 'linear-gradient(90deg, #FF9830 0%, #B877D9 100%)', + boxShadow: '0 4px 12px rgba(255, 152, 48, 0.4)', + }, + }), }; }; diff --git a/public/img/icons/unicons/ai-pointer.svg b/public/img/icons/unicons/ai-pointer.svg new file mode 100644 index 00000000000..32aae5491ea --- /dev/null +++ b/public/img/icons/unicons/ai-pointer.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 580e3ac2f79..a2ce95fe6a5 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5799,6 +5799,7 @@ }, "add-data-item-menu": { "add-button": "Add", + "add-from-saved-queries": "From saved queries", "add-query": "Query", "add-transformation": "Transformation", "expression-classic": "Classic condition", @@ -5837,6 +5838,13 @@ "new-panel": "New panel" } }, + "ai-mode-card": { + "add-nodes-to-context": "Add nodes to context", + "aria-label": "AI prompt input", + "prompt-placeholder": "Describe what you want to do...", + "remove-context": "Remove context", + "submit": "Send message" + }, "annotation-settings-edit": { "back-to-list": "Back to list", "delete": "Delete", @@ -5977,11 +5985,15 @@ }, "detail-view-header": { "actions": "Actions", + "disable-transform": "Disable", "duplicate-query": "Duplicate", "edit-query-name": "Edit query name", + "enable-transform": "Enable", "hide-response": "Hide response", "remove-query": "Remove", + "remove-transform": "Remove", "run-query": "RUN QUERY", + "save-query": "SAVE", "show-response": "Show response" }, "edit-link-view": { @@ -6264,21 +6276,28 @@ "query-detail-view": { "loading": "Loading data source...", "no-editor": "This data source does not have a query editor", - "options": "Options" + "options": "Options", + "query-options": "Query Options" }, "query-editor": { "query": "Query" }, "query-transform-card": { + "disable-transform": "Disable transformation", "duplicate": "Duplicate query", + "enable-transform": "Enable transformation", "hide-response": "Hide response", "remove-query": "Remove query", "remove-transform": "Remove transformation", "show-response": "Show response" }, "query-transform-list": { + "ai-mode": "AI Mode", + "close": "Close", "header": "Pipeline flow", - "nodes": "nodes" + "nodes": "nodes", + "queries-expressions": "Queries & Expressions", + "transformations": "Transformations" }, "query-variable-editor-form": { "description-examples": "Named capture groups can be used to separate the display text and value (<1>see examples ).", @@ -6390,6 +6409,23 @@ "title-same-as-folder": "Dashboard name cannot be the same as the folder name", "title-validation-failed": "Dashboard title validation failed." }, + "saved-queries-drawer": { + "browse-tab": "Browse", + "description-label": "Description", + "description-placeholder": "Enter description...", + "name-label": "Name", + "name-placeholder": "Enter query name...", + "no-results": "No queries found", + "query-preview": "Query Preview", + "save-button": "Save Query", + "save-success": "Query saved successfully! (stub)", + "save-tab": "Save Current", + "save-title": "Save Query", + "search-placeholder": "Search saved queries...", + "stub-notice": "This is a stub implementation. Enable the queryLibrary feature toggle for full functionality.", + "title": "Saved Queries", + "use-query": "Use" + }, "scenes-new-rule-from-panel-button": { "body-no-alerting-capable-query-found": "Cannot create alerts from this panel because no query to an alerting capable datasource is found.", "new-alert-rule": "New alert rule", @@ -12358,13 +12394,11 @@ "interval": "Interval", "interval-tooltip": "The evaluated interval that is sent to data source and is used in <1>$__interval and <4>$__interval_ms. This value is not exactly equal to <6>Time range / max data points, it will approximate a series of magic number.", "min-interval": "Min interval", - "min-interval-tooltip": "A lower limit for the interval. Recommended to be set to write frequency, for example <1>1m if your data is written every minute. Default value can be set in data source settings for most data sources.", - "time-range-max-data-points": "Time range / max data points" + "min-interval-tooltip": "A lower limit for the interval. Recommended to be set to write frequency, for example <1>1m if your data is written every minute. Default value can be set in data source settings for most data sources." }, "render-max-data-points-option": { "max-data-points": "Max data points", - "max-data-points-tooltip": "The maximum data points per series. Used directly by some data sources and used in calculation of auto interval. With streaming data this value is used for the rolling buffer.", - "width-of-panel": "Width of panel" + "max-data-points-tooltip": "The maximum data points per series. Used directly by some data sources and used in calculation of auto interval. With streaming data this value is used for the rolling buffer." }, "render-query-caching-ttloption": { "cache-ttl": "Cache TTL"