diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx index b0d0d29e046..70680966f91 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx @@ -8,72 +8,80 @@ import { ExpressionQueryType } from 'app/features/expressions/types'; interface AddDataItemMenuProps { onAddQuery: () => void; + onAddFromSavedQueries: () => void; onAddTransform: () => void; onAddExpression: (type: ExpressionQueryType) => void; } -export const AddDataItemMenu = memo(({ onAddQuery, onAddTransform, onAddExpression }: AddDataItemMenuProps) => { - const expressionTypes = [ - { type: ExpressionQueryType.math, label: t('dashboard-scene.add-data-item-menu.expression-math', 'Math') }, - { type: ExpressionQueryType.reduce, label: t('dashboard-scene.add-data-item-menu.expression-reduce', 'Reduce') }, - { - type: ExpressionQueryType.resample, - label: t('dashboard-scene.add-data-item-menu.expression-resample', 'Resample'), - }, - { - type: ExpressionQueryType.classic, - label: t('dashboard-scene.add-data-item-menu.expression-classic', 'Classic condition'), - }, - { - type: ExpressionQueryType.threshold, - label: t('dashboard-scene.add-data-item-menu.expression-threshold', 'Threshold'), - }, - ]; +export const AddDataItemMenu = memo( + ({ onAddQuery, onAddFromSavedQueries, onAddTransform, onAddExpression }: AddDataItemMenuProps) => { + const expressionTypes = [ + { type: ExpressionQueryType.math, label: t('dashboard-scene.add-data-item-menu.expression-math', 'Math') }, + { type: ExpressionQueryType.reduce, label: t('dashboard-scene.add-data-item-menu.expression-reduce', 'Reduce') }, + { + type: ExpressionQueryType.resample, + label: t('dashboard-scene.add-data-item-menu.expression-resample', 'Resample'), + }, + { + type: ExpressionQueryType.classic, + label: t('dashboard-scene.add-data-item-menu.expression-classic', 'Classic condition'), + }, + { + type: ExpressionQueryType.threshold, + label: t('dashboard-scene.add-data-item-menu.expression-threshold', 'Threshold'), + }, + ]; - // Add SQL if feature flag is enabled - if (config.featureToggles.sqlExpressions) { - expressionTypes.push({ - type: ExpressionQueryType.sql, - label: t('dashboard-scene.add-data-item-menu.expression-sql', 'SQL'), - }); - } + // Add SQL if feature flag is enabled + if (config.featureToggles.sqlExpressions) { + expressionTypes.push({ + type: ExpressionQueryType.sql, + label: t('dashboard-scene.add-data-item-menu.expression-sql', 'SQL'), + }); + } - const expressionSubItems = expressionTypes.map(({ type, label }) => ( - onAddExpression(type)} /> - )); + const expressionSubItems = expressionTypes.map(({ type, label }) => ( + onAddExpression(type)} /> + )); - const menu = ( - - - - - - ); - - return ( - - - + - - - ); -}); + + + + + ); + + return ( + + + + + + ); + } +); AddDataItemMenu.displayName = 'AddDataItemMenu'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx index 58e09e1077c..9bfd02d0f87 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx @@ -28,6 +28,7 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat import { getQueryRunnerFor } from '../../utils/utils'; import { QueryTransformItem } from './QueryTransformList'; +import { SavedQueriesDrawer } from './SavedQueriesDrawer'; interface DetailViewHeaderProps { selectedItem: QueryTransformItem; @@ -63,6 +64,7 @@ export const DetailViewHeader = ({ const [isEditing, setIsEditing] = useState(false); const [validationError, setValidationError] = useState(null); + const [isSavedQueriesDrawerOpen, setIsSavedQueriesDrawerOpen] = useState(false); // Helper to update queries with consistent pattern const updateQueries = useCallback( @@ -265,6 +267,23 @@ export const DetailViewHeader = ({ onToggleTransformVisibility?.(selectedItem.index); }, [selectedItem, onToggleTransformVisibility]); + // Handler for selecting a query from the library (replaces current query) + const onSelectQueryFromLibrary = useCallback( + (newQuery: DataQuery) => { + if (selectedItem.type !== 'query' || selectedItem.index === undefined) { + return; + } + updateQueries( + (queries) => + queries.map((q, idx) => + idx === selectedItem.index ? { ...newQuery, refId: q.refId, datasource: q.datasource } : q + ), + true + ); + }, + [selectedItem, updateQueries] + ); + const refId = 'refId' in selectedItem.data ? selectedItem.data.refId : ''; const isHidden = (selectedItem.type === 'query' || selectedItem.type === 'expression') && @@ -342,6 +361,19 @@ export const DetailViewHeader = ({ {/* Right side: Run Query + Actions Menu for queries/expressions */} {(selectedItem.type === 'query' || selectedItem.type === 'expression') && ( + {/* Save Query Button (only for queries, not expressions) */} + {selectedItem.type === 'query' && 'refId' in selectedItem.data && ( + + )} + + + + {isSaveMode ? ( + /* Save Mode */ +
+ +
+ + setSaveName(e.currentTarget.value)} + placeholder={t('dashboard-scene.saved-queries-drawer.name-placeholder', 'Enter query name...')} + /> +
+
+ + setSaveDescription(e.currentTarget.value)} + placeholder={t( + 'dashboard-scene.saved-queries-drawer.description-placeholder', + 'Enter description...' + )} + /> +
+
+ + Query Preview + +
{JSON.stringify(currentQuery, null, 2)}
+
+ +
+
+ ) : ( + /* Browse Mode */ + <> +
+ } + placeholder={t('dashboard-scene.saved-queries-drawer.search-placeholder', 'Search saved queries...')} + value={searchTerm} + onChange={(e) => setSearchTerm(e.currentTarget.value)} + /> +
+ +
+ {filteredQueries.length === 0 ? ( +
+ No queries found +
+ ) : ( + filteredQueries.map((savedQuery) => ( +
+
+
{savedQuery.name}
+
{savedQuery.description}
+
+ +
+ )) + )} +
+ +
+ + + + This is a stub implementation. Enable the queryLibrary feature toggle for full functionality. + + +
+ + )} + + + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + height: '100%', + }), + search: css({ + marginTop: theme.spacing(1), + }), + queryList: css({ + flex: 1, + overflow: 'auto', + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + }), + queryItem: css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: theme.spacing(1.5), + background: theme.colors.background.secondary, + borderRadius: theme.shape.radius.default, + border: `1px solid ${theme.colors.border.weak}`, + '&:hover': { + background: theme.colors.action.hover, + borderColor: theme.colors.border.medium, + }, + }), + queryInfo: css({ + flex: 1, + minWidth: 0, + }), + queryName: css({ + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.text.primary, + marginBottom: theme.spacing(0.5), + }), + queryDescription: css({ + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.text.secondary, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }), + emptyState: css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: theme.spacing(4), + color: theme.colors.text.secondary, + }), + stubNotice: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + padding: theme.spacing(1), + background: theme.colors.info.transparent, + borderRadius: theme.shape.radius.default, + color: theme.colors.info.text, + fontSize: theme.typography.bodySmall.fontSize, + }), + saveForm: css({ + marginTop: theme.spacing(2), + }), + label: css({ + display: 'block', + marginBottom: theme.spacing(0.5), + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.text.primary, + }), + queryPreview: css({ + marginTop: theme.spacing(1), + }), + queryCode: css({ + background: theme.colors.background.secondary, + padding: theme.spacing(1), + borderRadius: theme.shape.radius.default, + fontSize: theme.typography.bodySmall.fontSize, + overflow: 'auto', + maxHeight: '200px', + }), +});