) => {
+ event.target.select();
+ }, []);
+
+ // Action handlers
+ const onCopyQuery = useCallback(() => {
+ if (selectedItem.index === undefined) {
+ return;
+ }
+
+ updateQueries((queries) => {
+ const queryToCopy = queries[selectedItem.index];
+ return queryToCopy ? [...queries, { ...queryToCopy }] : queries;
+ });
+ }, [selectedItem, updateQueries]);
+
+ const onRemoveQuery = useCallback(() => {
+ if (selectedItem.index === undefined) {
+ return;
+ }
+
+ updateQueries((queries) => queries.filter((_, idx) => idx !== selectedItem.index));
+ }, [selectedItem, updateQueries]);
+
+ const onToggleHideQuery = useCallback(() => {
+ if ((selectedItem.type !== 'query' && selectedItem.type !== 'expression') || selectedItem.index === undefined) {
+ return;
+ }
+
+ updateQueries(
+ (queries) => queries.map((q, idx) => (idx === selectedItem.index ? { ...q, hide: !q.hide } : q)),
+ true // Run queries after update
+ );
+ }, [selectedItem, updateQueries]);
+
+ const onRunQuery = useCallback(() => {
+ const queryRunner = getQueryRunnerFor(panel);
+ queryRunner?.runQueries();
+ }, [panel]);
+
+ const refId = 'refId' in selectedItem.data ? selectedItem.data.refId : '';
+ const isHidden =
+ (selectedItem.type === 'query' || selectedItem.type === 'expression') &&
+ 'hide' in selectedItem.data &&
+ selectedItem.data.hide;
+
+ return (
+
+
+ {/* Left side: Icon, Datasource, Name */}
+
+
+
+ {/* Datasource picker for queries */}
+ {selectedItem.type === 'query' && datasourceSettings && (
+
+ )}
+
+ {/* Editable query/expression name */}
+ {(selectedItem.type === 'query' || selectedItem.type === 'expression') && refId && (
+ <>
+ {!isEditing ? (
+
+ ) : (
+ <>
+
+ {validationError && {validationError}}
+ >
+ )}
+ >
+ )}
+
+
+ {/* Right side: Run Query + Actions Menu */}
+ {(selectedItem.type === 'query' || selectedItem.type === 'expression') && (
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+ )}
+
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme2, config: { color: string }) => {
+ return {
+ header: css({
+ padding: theme.spacing(1, 2),
+ borderLeft: `4px solid ${config.color}`,
+ borderBottom: `1px solid ${theme.colors.border.weak}`,
+ background: theme.colors.background.secondary,
+ minHeight: theme.spacing(6),
+ }),
+ icon: css({
+ color: theme.colors.text.secondary,
+ fontSize: theme.typography.h5.fontSize,
+ }),
+ queryNameWrapper: css({
+ display: 'flex',
+ cursor: 'pointer',
+ border: '1px solid transparent',
+ borderRadius: theme.shape.radius.default,
+ alignItems: 'center',
+ padding: theme.spacing(0.5, 1),
+ margin: 0,
+ background: 'transparent',
+ overflow: 'hidden',
+
+ '&:hover': {
+ background: theme.colors.action.hover,
+ border: `1px dashed ${theme.colors.border.strong}`,
+ },
+
+ '&:focus': {
+ border: `2px solid ${theme.colors.primary.border}`,
+ },
+
+ '&:hover, &:focus': {
+ '.query-name-edit-icon': {
+ visibility: 'visible',
+ },
+ },
+ }),
+ queryName: css({
+ fontWeight: theme.typography.fontWeightMedium,
+ color: theme.colors.primary.text,
+ cursor: 'pointer',
+ overflow: 'hidden',
+ marginLeft: theme.spacing(0.5),
+ }),
+ queryEditIcon: cx(
+ css({
+ marginLeft: theme.spacing(1),
+ visibility: 'hidden',
+ }),
+ 'query-name-edit-icon'
+ ),
+ queryNameInput: css({
+ maxWidth: '300px',
+ margin: '-4px 0',
+ }),
+ };
+};
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryDetailView.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryDetailView.tsx
index 5067a343dd0..9ada0445e35 100644
--- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryDetailView.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryDetailView.tsx
@@ -1,21 +1,13 @@
import { css } from '@emotion/css';
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useCallback, useMemo } from 'react';
import { useAsync } from 'react-use';
-import {
- CoreApp,
- DataQuery,
- DataSourceInstanceSettings,
- DataSourcePluginContextProvider,
- GrafanaTheme2,
- TimeRange,
-} from '@grafana/data';
-import { Trans, t } from '@grafana/i18n';
+import { CoreApp, DataQuery, DataSourcePluginContextProvider, GrafanaTheme2, TimeRange } from '@grafana/data';
+import { Trans } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { SceneDataQuery, VizPanel } from '@grafana/scenes';
import { ErrorBoundaryAlert, useStyles2 } from '@grafana/ui';
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
-import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { QueryErrorAlert } from 'app/features/query/components/QueryErrorAlert';
import { getQueryRunnerFor } from '../../utils/utils';
@@ -28,20 +20,14 @@ interface QueryDetailViewProps {
export function QueryDetailView({ panel, query, queryIndex }: QueryDetailViewProps) {
const styles = useStyles2(getStyles);
- const [datasourceRef, setDatasourceRef] = useState(query.datasource);
- let initialDsSettings: DataSourceInstanceSettings | undefined = useMemo(() => {
+ const dsSettings = useMemo(() => {
try {
return getDataSourceSrv().getInstanceSettings(query.datasource);
} catch {
return getDataSourceSrv().getInstanceSettings(null);
}
- // we want to run this on mount and then never again
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const [dsSettings, setDsSettings] = useState(initialDsSettings);
- const [needsQueryRun, setNeedsQueryRun] = useState(false);
+ }, [query.datasource]);
const queryRunner = getQueryRunnerFor(panel);
const queryRunnerState = queryRunner?.useState();
@@ -51,11 +37,11 @@ export function QueryDetailView({ panel, query, queryIndex }: QueryDetailViewPro
// FIXME: handle loading and error cases
const { value: datasource } = useAsync(async () => {
try {
- return await getDataSourceSrv().get(datasourceRef);
+ return await getDataSourceSrv().get(query.datasource);
} catch {
return await getDataSourceSrv().get();
}
- }, [datasourceRef]);
+ }, [query.datasource]);
// Subscribe to panel data
const data = useMemo(() => {
@@ -72,51 +58,6 @@ export function QueryDetailView({ panel, query, queryIndex }: QueryDetailViewPro
};
}, [queryRunnerState?.data, query.refId]);
- useEffect(() => {
- // Update the query with the new datasource
- if (queryRunner && datasource) {
- const queries = queryRunner.state.queries || [];
- const newQueries = queries.map((q, idx) => {
- if (idx === queryIndex) {
- // Get default query for new datasource
- const defaultQuery = datasource.getDefaultQuery?.(CoreApp.PanelEditor) || {};
- return {
- ...defaultQuery,
- ...q,
- datasource: { uid: datasource.uid, type: datasource.type },
- refId: q.refId,
- };
- }
- return q;
- });
-
- queryRunner.setState({
- datasource: { uid: datasource.uid, type: datasource.type },
- queries: newQueries,
- });
-
- if (needsQueryRun) {
- queryRunner.runQueries();
- setNeedsQueryRun(false);
- }
- }
- }, [datasource, queryIndex, queryRunner, needsQueryRun]);
-
- const handleDataSourceChange = useCallback(
- async (newDsSettings: DataSourceInstanceSettings) => {
- setDatasourceRef({
- // FIXME: apiVersion isn't on the datasources we looked at.
- uid: newDsSettings.uid,
- type: newDsSettings.type,
- });
- setDsSettings(newDsSettings);
- if (newDsSettings.uid !== dsSettings?.uid) {
- setNeedsQueryRun(true);
- }
- },
- [dsSettings]
- );
-
const handleQueryChange = useCallback(
(updatedQuery: DataQuery) => {
if (queryRunner) {
@@ -175,14 +116,6 @@ export function QueryDetailView({ panel, query, queryIndex }: QueryDetailViewPro
return (
-
-
-
-
{
padding: theme.spacing(2),
width: '100%',
}),
- datasourceSection: css({
- paddingBottom: theme.spacing(2),
- }),
queryContent: css({
display: 'flex',
flexDirection: 'column',