chore: alex - ai demo
This commit is contained in:
@@ -43,6 +43,7 @@ export const availableIconsIndex = {
|
||||
'expand-arrows-alt': true,
|
||||
at: true,
|
||||
ai: true,
|
||||
'ai-pointer': true,
|
||||
backward: true,
|
||||
bars: true,
|
||||
bell: true,
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
"unicons/record-audio",
|
||||
"unicons/scim",
|
||||
"solid/bookmark",
|
||||
"unicons/ai-pointer",
|
||||
"unicons/ai-sparkle",
|
||||
"unicons/dollar-alt",
|
||||
"unicons/window-grid",
|
||||
|
||||
@@ -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<HTMLDivElement>(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<HTMLDivElement>) => {
|
||||
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 (
|
||||
<div className={styles.container}>
|
||||
{/* Context Pills Section */}
|
||||
{hasContext && (
|
||||
<div className={styles.contextSection}>
|
||||
<Stack direction="row" gap={1} wrap="wrap">
|
||||
{selectedContexts.map((context) => (
|
||||
<div key={context.id} className={styles.badge} style={styles.getBadgeStyle(context.type)}>
|
||||
<Icon name={context.icon} size="sm" style={styles.getBadgeColor(context.type)} />
|
||||
<span className={styles.badgeLabel} style={styles.getBadgeColor(context.type)}>
|
||||
{context.label}
|
||||
</span>
|
||||
<button
|
||||
className={styles.badgeRemove}
|
||||
style={styles.getBadgeColor(context.type)}
|
||||
onClick={() => onRemoveContext(context.id)}
|
||||
aria-label={t('dashboard-scene.ai-mode-card.remove-context', 'Remove context')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prompt Input Section */}
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<div
|
||||
ref={editorRef}
|
||||
className={styles.input}
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
data-placeholder={t('dashboard-scene.ai-mode-card.prompt-placeholder', 'Describe what you want to do...')}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
aria-label={t('dashboard-scene.ai-mode-card.aria-label', 'AI prompt input')}
|
||||
tabIndex={0}
|
||||
/>
|
||||
|
||||
<div className={styles.bottomRow}>
|
||||
<div className={styles.leftContent}>
|
||||
{!hasContext && (
|
||||
<div className={styles.emptyState}>
|
||||
<Icon name="ai-pointer" />
|
||||
{t('dashboard-scene.ai-mode-card.add-nodes-to-context', 'Add nodes to context')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<IconButton
|
||||
name="message"
|
||||
type="submit"
|
||||
disabled={!prompt.trim()}
|
||||
tooltip={t('dashboard-scene.ai-mode-card.submit', 'Send message')}
|
||||
className={styles.submitButton}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, colors: ReturnType<typeof usePanelDataPaneColors>) => {
|
||||
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,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -303,7 +303,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
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<PanelDataPane>) {
|
||||
|
||||
/** TRANSFORMS **/
|
||||
const handleAddTransform = useCallback(
|
||||
(selected: SelectableValue<string>) => {
|
||||
(selected: SelectableValue<string>, customOptions?: Record<string, unknown>) => {
|
||||
if (!selected.value) {
|
||||
return;
|
||||
}
|
||||
@@ -334,7 +334,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
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<PanelDataPane>) {
|
||||
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<PanelDataPane>) {
|
||||
onToggleTransformVisibility={handleToggleTransformVisibility}
|
||||
onReorderDataSources={handleReorderDataSources}
|
||||
onReorderTransforms={handleReorderTransforms}
|
||||
onAddOrganizeFieldsTransform={() =>
|
||||
handleAddTransform(
|
||||
{ value: 'organize' },
|
||||
{
|
||||
excludeByName: {},
|
||||
indexByName: {},
|
||||
renameByName: {},
|
||||
includeByName: {},
|
||||
orderByMode: 'auto',
|
||||
orderBy: [
|
||||
{
|
||||
type: 'name',
|
||||
desc: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
+219
-21
@@ -1,14 +1,15 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
|
||||
import { DataTransformerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { SceneDataQuery } from '@grafana/scenes';
|
||||
import { Icon, ScrollContainer, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { Button, Icon, ScrollContainer, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { ExpressionQueryType } from 'app/features/expressions/types';
|
||||
|
||||
import { AddDataItemMenu } from './AddDataItemMenu';
|
||||
import { AiModeCard } from './AiModeCard';
|
||||
import { ConnectionLines } from './ConnectionLines';
|
||||
import { QueryTransformCard } from './QueryTransformCard';
|
||||
|
||||
@@ -38,6 +39,7 @@ interface QueryTransformListProps {
|
||||
onToggleTransformVisibility?: (index: number) => 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<string[]>([]);
|
||||
const [hovered, setHovered] = useState<string | null>(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 (
|
||||
<div className={styles.container} onMouseLeave={() => setHovered(null)}>
|
||||
<div className={styles.header}>
|
||||
@@ -181,14 +246,57 @@ export const QueryTransformList = memo(
|
||||
<span className={styles.headerTitle}>
|
||||
{t('dashboard-scene.query-transform-list.header', 'Pipeline flow')}
|
||||
</span>
|
||||
<Button size="sm" onClick={handleToggleAiMode} className={styles.aiModeButton}>
|
||||
<Stack direction="row" gap={0.5} alignItems="center">
|
||||
<Icon name={isAiMode ? 'times' : 'ai'} size="sm" />
|
||||
{isAiMode
|
||||
? t('dashboard-scene.query-transform-list.close', 'Close')
|
||||
: t('dashboard-scene.query-transform-list.ai-mode', 'AI Mode')}
|
||||
</Stack>
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
{isAiMode && (
|
||||
<div className={cx(styles.aiModeContent, isClosing && styles.aiModeClosing)}>
|
||||
<AiModeCard
|
||||
selectedContexts={selectedContexts}
|
||||
onRemoveContext={handleRemoveContext}
|
||||
onSubmit={handleAiSubmit}
|
||||
onDemoWorkflow={
|
||||
onAddOrganizeFieldsTransform
|
||||
? {
|
||||
availableCardIds: allItems.map((item) => item.id),
|
||||
onSelectContext: (id) => {
|
||||
setSelectedContextIds((prev) => [...prev, id]);
|
||||
},
|
||||
onAddOrganizeFieldsTransformation: onAddOrganizeFieldsTransform,
|
||||
onCloseAiMode: () => {
|
||||
setIsClosing(true);
|
||||
setTimeout(() => {
|
||||
setIsAiMode(false);
|
||||
setIsClosing(false);
|
||||
setSelectedContextIds([]);
|
||||
}, 300);
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.scrollWrapper} data-testid="query-transform-list-scroll-wrapper">
|
||||
<ScrollContainer data-scrollcontainer height="100%">
|
||||
<div className={styles.contentWrapper}>
|
||||
<ConnectionLines connections={visibleConnections} isDragging={isDragging} />
|
||||
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||
<div className={styles.content} data-testid="query-transform-list-content">
|
||||
<div
|
||||
className={cx(
|
||||
styles.content,
|
||||
isAiMode && !isClosing && styles.contentGradientBorder,
|
||||
isClosing && styles.contentGradientBorderClosing
|
||||
)}
|
||||
data-testid="query-transform-list-content"
|
||||
>
|
||||
<Stack direction="column" gap={3}>
|
||||
{/* Data Sources Section (Queries + Expressions) */}
|
||||
{dataSourceItems.length > 0 && (
|
||||
@@ -206,7 +314,7 @@ export const QueryTransformList = memo(
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
onMouseMove={ev => {
|
||||
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
|
||||
)}
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
{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)}
|
||||
/>
|
||||
</div>
|
||||
@@ -279,7 +395,6 @@ export const QueryTransformList = memo(
|
||||
);
|
||||
}}
|
||||
</Droppable>
|
||||
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -299,18 +414,24 @@ export const QueryTransformList = memo(
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={cx(styles.cardList, isDraggingFromOtherSection ? styles.droppableInvalid : undefined)}
|
||||
onMouseMove={ev => {
|
||||
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)}
|
||||
/>
|
||||
</div>
|
||||
@@ -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)',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2221_8678)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.709 16.2723L9.0591 11.3225L9.05674 11.3202C8.99857 11.1443 8.99043 10.9556 9.03322 10.7753C9.07602 10.595 9.16806 10.4302 9.29909 10.2992C9.43011 10.1681 9.59496 10.0761 9.77525 10.0333C9.95554 9.99051 10.1442 9.99865 10.3201 10.0568L15.2699 11.7067C15.4175 11.7573 15.552 11.8402 15.6635 11.9495C15.7813 12.0674 15.8678 12.2128 15.9151 12.3726C15.9624 12.5324 15.969 12.7014 15.9343 12.8644C15.8997 13.0274 15.8248 13.1792 15.7166 13.3059C15.6084 13.4326 15.4702 13.5303 15.3146 13.59L13.4832 14.29C13.4398 14.3078 13.4003 14.334 13.3671 14.3672C13.334 14.4004 13.3077 14.4399 13.29 14.4833L12.5899 16.3147C12.5158 16.5066 12.3843 16.671 12.2133 16.7854C12.0423 16.8998 11.8402 16.9587 11.6346 16.9541C11.4289 16.9494 11.2297 16.8815 11.064 16.7595C10.8984 16.6375 10.7744 16.4673 10.709 16.2723ZM15.0601 12.3408L10.1103 10.6909C10.0519 10.672 9.98949 10.6697 9.92985 10.6841C9.87021 10.6985 9.8157 10.729 9.77232 10.7724C9.72894 10.8158 9.69838 10.8703 9.68399 10.9299C9.6696 10.9896 9.67195 11.052 9.69078 11.1104L11.3407 16.0602C11.3632 16.1243 11.4048 16.18 11.4599 16.2198C11.515 16.2597 11.5809 16.2817 11.6489 16.283C11.7169 16.2843 11.7836 16.2647 11.8402 16.227C11.8967 16.1893 11.9404 16.1352 11.9653 16.0719L12.6677 14.2429C12.6773 14.2187 12.6883 14.1951 12.7007 14.1722C12.7304 14.1171 12.8939 13.8942 12.8939 13.8942C12.8939 13.8942 13.0955 13.7422 13.1721 13.7008C13.195 13.6884 13.2186 13.6774 13.2428 13.6678L15.0719 12.9654C15.1351 12.9405 15.1892 12.8968 15.2269 12.8402C15.2646 12.7837 15.2842 12.717 15.2829 12.649C15.2816 12.581 15.2596 12.5151 15.2198 12.46C15.1799 12.4049 15.1242 12.3633 15.0601 12.3408Z" fill="currentColor"/>
|
||||
</g>
|
||||
<path d="M9.70686 3.04459C9.80477 2.67921 10.165 2.45827 10.5115 2.55111C10.858 2.64395 11.0595 3.01541 10.9616 3.38079L10.2525 6.02711C10.1546 6.39249 9.79434 6.61343 9.44787 6.5206C9.10139 6.42776 8.89988 6.0563 8.99778 5.69092L9.70686 3.04459Z" fill="currentColor"/>
|
||||
<path d="M14.4934 7.08119C14.8424 6.9789 15.197 7.19013 15.2855 7.55297C15.3739 7.91582 15.1627 8.29289 14.8137 8.39518L12.286 9.13603C11.937 9.23832 11.5823 9.0271 11.4939 8.66425C11.4054 8.3014 11.6166 7.92433 11.9657 7.82204L14.4934 7.08119Z" fill="currentColor"/>
|
||||
<path d="M7.59344 15.9508C7.49553 16.3162 7.13529 16.5371 6.78882 16.4443C6.44234 16.3515 6.24083 15.98 6.33873 15.6146L7.04781 12.9683C7.14572 12.6029 7.50596 12.382 7.85244 12.4748C8.19891 12.5677 8.40042 12.9391 8.30252 13.3045L7.59344 15.9508Z" fill="currentColor"/>
|
||||
<path d="M2.8069 11.9142C2.45789 12.0165 2.10326 11.8053 2.01481 11.4424C1.92636 11.0796 2.13758 10.7025 2.48659 10.6002L5.01434 9.85938C5.36335 9.75709 5.71798 9.96832 5.80643 10.3312C5.89488 10.694 5.68366 11.0711 5.33465 11.1734L2.8069 11.9142Z" fill="currentColor"/>
|
||||
<path d="M3.86361 5.46111C3.61251 5.19802 3.61812 4.76586 3.87614 4.49585C4.13417 4.22584 4.5469 4.22023 4.79801 4.48332L6.61668 6.38879C6.86778 6.65188 6.86217 7.08404 6.60415 7.35405C6.34612 7.62406 5.93339 7.62967 5.68228 7.36658L3.86361 5.46111Z" fill="currentColor"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_2221_8678">
|
||||
<rect width="8" height="8" fill="white" transform="translate(11.6567 18.3135) rotate(-135)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -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</1> ).",
|
||||
@@ -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</1> and <4>$__interval_ms</4>. This value is not exactly equal to <6>Time range / max data points</6>, 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</1> 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</1> 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"
|
||||
|
||||
Reference in New Issue
Block a user