init: focus query feature
This commit is contained in:
@@ -41,6 +41,7 @@ export const availableIconsIndex = {
|
||||
asserts: true,
|
||||
'expand-arrows': true,
|
||||
'expand-arrows-alt': true,
|
||||
'expand-screen': true,
|
||||
at: true,
|
||||
ai: true,
|
||||
backward: true,
|
||||
@@ -85,6 +86,7 @@ export const availableIconsIndex = {
|
||||
'comments-alt': true,
|
||||
compass: true,
|
||||
'compress-arrows': true,
|
||||
'compress-screen': true,
|
||||
copy: true,
|
||||
'corner-up-left': true,
|
||||
'corner-up-right': true,
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"unicons/comment-alt",
|
||||
"unicons/comment-alt-share",
|
||||
"unicons/comments-alt",
|
||||
"unicons/compress-screen",
|
||||
"unicons/compass",
|
||||
"unicons/copy",
|
||||
"unicons/corner-down-right-alt",
|
||||
@@ -73,6 +74,7 @@
|
||||
"unicons/exclamation-circle",
|
||||
"unicons/exclamation-triangle",
|
||||
"unicons/external-link-alt",
|
||||
"unicons/expand-screen",
|
||||
"unicons/eye",
|
||||
"unicons/eye-slash",
|
||||
"unicons/file-alt",
|
||||
|
||||
+38
-14
@@ -1,3 +1,6 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { CoreApp, DataSourceApi, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
@@ -13,7 +16,7 @@ import {
|
||||
SceneDataQuery,
|
||||
} from '@grafana/scenes';
|
||||
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||
import { Button, Stack, Tab } from '@grafana/ui';
|
||||
import { Button, Stack, Tab, useStyles2 } from '@grafana/ui';
|
||||
import { addQuery } from 'app/core/utils/query';
|
||||
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
|
||||
import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils';
|
||||
@@ -91,7 +94,7 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
||||
|
||||
// do we have a last used datasource for this dashboard
|
||||
if (lastUsedDatasource?.datasourceUid !== null) {
|
||||
// get datasource from dashbopard uid
|
||||
// get datasource from dashboard uid
|
||||
dsSettings = getDataSourceSrv().getInstanceSettings({ uid: lastUsedDatasource?.datasourceUid });
|
||||
if (dsSettings) {
|
||||
datasource = await getDataSourceSrv().get({
|
||||
@@ -336,12 +339,16 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel
|
||||
const { datasource, dsSettings } = model.useState();
|
||||
const { data, queries } = model.queryRunner.useState();
|
||||
const { openDrawer: openQueryLibraryDrawer, queryLibraryEnabled } = useQueryLibraryContext();
|
||||
const [hasFocusedQuery, setHasFocusedQuery] = useState(false);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (!datasource || !dsSettings || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showAddButton = !isSharedDashboardQuery(dsSettings.name);
|
||||
const showAddButton = !isSharedDashboardQuery(dsSettings.name) && !hasFocusedQuery;
|
||||
const showExpressionButton =
|
||||
config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && !hasFocusedQuery;
|
||||
const onSelectQueryFromLibrary = async (query: DataQuery) => {
|
||||
// ensure all queries explicitly define a datasource
|
||||
const enrichedQueries = queries.map((q) =>
|
||||
@@ -368,22 +375,28 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid={selectors.components.QueryTab.content}>
|
||||
<QueryGroupTopSection
|
||||
data={data}
|
||||
dsSettings={dsSettings}
|
||||
dataSource={datasource}
|
||||
options={model.buildQueryOptions()}
|
||||
onDataSourceChange={model.onChangeDataSource}
|
||||
onOptionsChange={model.onQueryOptionsChange}
|
||||
onOpenQueryInspector={model.onOpenInspector}
|
||||
/>
|
||||
<div
|
||||
data-testid={selectors.components.QueryTab.content}
|
||||
className={cx(styles.container, { [styles.focused]: hasFocusedQuery })}
|
||||
>
|
||||
{!hasFocusedQuery && (
|
||||
<QueryGroupTopSection
|
||||
data={data}
|
||||
dsSettings={dsSettings}
|
||||
dataSource={datasource}
|
||||
options={model.buildQueryOptions()}
|
||||
onDataSourceChange={model.onChangeDataSource}
|
||||
onOptionsChange={model.onQueryOptionsChange}
|
||||
onOpenQueryInspector={model.onOpenInspector}
|
||||
/>
|
||||
)}
|
||||
|
||||
<QueryEditorRows
|
||||
data={data}
|
||||
queries={queries}
|
||||
dsSettings={dsSettings}
|
||||
onAddQuery={model.onAddQuery}
|
||||
onFocusQuery={setHasFocusedQuery}
|
||||
onQueriesChange={model.onQueriesChange}
|
||||
onRunQueries={model.onRunQueries}
|
||||
onUpdateDatasources={queryLibraryEnabled ? model.updateDatasourceIfNeeded : undefined}
|
||||
@@ -420,7 +433,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<Panel
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && (
|
||||
{showExpressionButton && (
|
||||
<ExpressionTypeDropdown handleOnSelect={model.onAddExpressionOfType}>
|
||||
<Button icon="plus" variant="secondary" data-testid={selectors.components.QueryTab.addExpression}>
|
||||
<Trans i18nKey="dashboard-scene.panel-data-queries-tab-rendered.expression">Expression </Trans>
|
||||
@@ -452,3 +465,14 @@ function QueriesTab(props: QueriesTabProps) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = () => ({
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
focused: css({
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import classNames from 'classnames';
|
||||
import { cloneDeep, filter, uniqBy, uniqueId } from 'lodash';
|
||||
import pluralize from 'pluralize';
|
||||
@@ -73,6 +74,8 @@ export interface Props<TQuery extends DataQuery> {
|
||||
queryLibraryRef?: string;
|
||||
onCancelQueryLibraryEdit?: () => void;
|
||||
isOpen?: boolean;
|
||||
isFocused?: boolean;
|
||||
onFocusQuery?: () => void;
|
||||
}
|
||||
|
||||
interface State<TQuery extends DataQuery> {
|
||||
@@ -391,7 +394,14 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
};
|
||||
|
||||
renderActions = (props: QueryOperationRowRenderProps) => {
|
||||
const { query, hideHideQueryButton: hideHideQueryButton = false, queryLibraryRef, app } = this.props;
|
||||
const {
|
||||
query,
|
||||
hideHideQueryButton: hideHideQueryButton = false,
|
||||
queryLibraryRef,
|
||||
app,
|
||||
isFocused,
|
||||
onFocusQuery,
|
||||
} = this.props;
|
||||
const { datasource, showingHelp } = this.state;
|
||||
const isHidden = !!query.hide;
|
||||
|
||||
@@ -452,6 +462,19 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
onClick={this.onRemoveQuery}
|
||||
/>
|
||||
)}
|
||||
{onFocusQuery && (
|
||||
<QueryOperationToggleAction
|
||||
dataTestId={selectors.components.QueryEditorRow.actionButton('Focus query')}
|
||||
title={
|
||||
isFocused
|
||||
? t('query-operation.header.collapse', 'Show all queries')
|
||||
: t('query-operation.header.focus', 'Focus query')
|
||||
}
|
||||
icon={isFocused ? 'compress-screen' : 'expand-screen'}
|
||||
active={Boolean(isFocused)}
|
||||
onClick={onFocusQuery}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -487,14 +510,19 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
app,
|
||||
queryLibraryRef,
|
||||
onCancelQueryLibraryEdit,
|
||||
isFocused,
|
||||
} = this.props;
|
||||
const { datasource, showingHelp, data } = this.state;
|
||||
const isHidden = query.hide;
|
||||
const error =
|
||||
data?.error && data.error.refId === query.refId ? data.error : data?.errors?.find((e) => e.refId === query.refId);
|
||||
// Note: We can't use hooks in class components, so we use static styles
|
||||
const focusedRowStyle = isFocused ? getFocusedRowStyle() : undefined;
|
||||
const focusedWrapperStyle = isFocused ? getFocusedWrapperStyle() : undefined;
|
||||
const rowClasses = classNames('query-editor-row', {
|
||||
'query-editor-row--disabled': isHidden,
|
||||
'gf-form-disabled': isHidden,
|
||||
[focusedRowStyle || '']: isFocused,
|
||||
});
|
||||
|
||||
if (!datasource) {
|
||||
@@ -535,7 +563,11 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-testid="query-editor-row" aria-label={selectors.components.QueryEditorRows.rows}>
|
||||
<div
|
||||
data-testid="query-editor-row"
|
||||
aria-label={selectors.components.QueryEditorRows.rows}
|
||||
className={focusedWrapperStyle}
|
||||
>
|
||||
{queryLibraryRef && (
|
||||
<MaybeQueryLibraryEditingHeader
|
||||
query={query}
|
||||
@@ -652,3 +684,24 @@ function AdaptiveTelemetryQueryActions({ query }: { query: DataQuery }) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Static styles for focused state - used in class component
|
||||
// Transitions are handled in parent components where we have theme access
|
||||
const getFocusedWrapperStyle = () =>
|
||||
css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
});
|
||||
|
||||
const getFocusedRowStyle = () =>
|
||||
css({
|
||||
flex: '1 1 100%',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// Remove margins when focused to maximize space
|
||||
marginBottom: 0,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { DragDropContext, DragStart, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
import { PureComponent, ReactNode } from 'react';
|
||||
|
||||
@@ -44,9 +45,15 @@ export interface Props {
|
||||
queryLibraryRef?: string;
|
||||
onCancelQueryLibraryEdit?: () => void;
|
||||
isOpen?: boolean;
|
||||
onFocusQuery?: (arg0: boolean) => void;
|
||||
}
|
||||
|
||||
export class QueryEditorRows extends PureComponent<Props> {
|
||||
export class QueryEditorRows extends PureComponent<Props, { focusedQueryRefId: string | null }> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { focusedQueryRefId: null };
|
||||
}
|
||||
|
||||
onRemoveQuery = (query: DataQuery) => {
|
||||
this.props.onQueriesChange(this.props.queries.filter((item) => item !== query));
|
||||
};
|
||||
@@ -167,6 +174,11 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
});
|
||||
};
|
||||
|
||||
setFocusedQueryRefId = (refId: string | null) => {
|
||||
this.setState({ focusedQueryRefId: refId });
|
||||
this.props.onFocusQuery?.(Boolean(refId));
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
dsSettings,
|
||||
@@ -187,14 +199,26 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
onCancelQueryLibraryEdit,
|
||||
isOpen,
|
||||
} = this.props;
|
||||
const { focusedQueryRefId } = this.state;
|
||||
const isFocused = Boolean(focusedQueryRefId);
|
||||
const containerStyle = getContainerStyle(isFocused);
|
||||
|
||||
return (
|
||||
<DragDropContext onDragStart={this.onDragStart} onDragEnd={this.onDragEnd}>
|
||||
<Droppable droppableId="transformations-list" direction="vertical">
|
||||
{(provided) => {
|
||||
return (
|
||||
<div data-testid="query-editor-rows" ref={provided.innerRef} {...provided.droppableProps}>
|
||||
<div
|
||||
data-testid="query-editor-rows"
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
className={containerStyle}
|
||||
>
|
||||
{queries.map((query, index) => {
|
||||
// If a query is focused, don't render any other queries
|
||||
if (focusedQueryRefId && focusedQueryRefId !== query.refId) {
|
||||
return null;
|
||||
}
|
||||
const dataSourceSettings = getDataSourceSettings(query, dsSettings);
|
||||
const onChangeDataSourceSettings = dsSettings.meta.mixed
|
||||
? (settings: DataSourceInstanceSettings) => this.onDataSourceChange(settings, index)
|
||||
@@ -227,6 +251,10 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
queryLibraryRef={queryLibraryRef}
|
||||
onCancelQueryLibraryEdit={onCancelQueryLibraryEdit}
|
||||
isOpen={isOpen}
|
||||
isFocused={focusedQueryRefId === query.refId}
|
||||
onFocusQuery={() =>
|
||||
this.setFocusedQueryRefId(focusedQueryRefId === query.refId ? null : query.refId)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -252,3 +280,17 @@ const getDataSourceSettings = (
|
||||
const querySettings = getDataSourceSrv().getInstanceSettings(query.datasource);
|
||||
return querySettings || groupSettings;
|
||||
};
|
||||
|
||||
// Styles for focused container - using static styles since this is a class component
|
||||
// Transitions are handled in the parent functional component where we have theme access
|
||||
const getContainerStyle = (isFocused: boolean) => {
|
||||
if (!isFocused) {
|
||||
return undefined;
|
||||
}
|
||||
return css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
flex: '1 1 auto',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M256 128C256 110.3 241.7 96 224 96C206.3 96 192 110.3 192 128L192 192L128 192C110.3 192 96 206.3 96 224C96 241.7 110.3 256 128 256L224 256C241.7 256 256 241.7 256 224L256 128zM128 384C110.3 384 96 398.3 96 416C96 433.7 110.3 448 128 448L192 448L192 512C192 529.7 206.3 544 224 544C241.7 544 256 529.7 256 512L256 416C256 398.3 241.7 384 224 384L128 384zM448 128C448 110.3 433.7 96 416 96C398.3 96 384 110.3 384 128L384 224C384 241.7 398.3 256 416 256L512 256C529.7 256 544 241.7 544 224C544 206.3 529.7 192 512 192L448 192L448 128zM416 384C398.3 384 384 398.3 384 416L384 512C384 529.7 398.3 544 416 544C433.7 544 448 529.7 448 512L448 448L512 448C529.7 448 544 433.7 544 416C544 398.3 529.7 384 512 384L416 384z"/></svg>
|
||||
|
After Width: | Height: | Size: 943 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128 96C110.3 96 96 110.3 96 128L96 224C96 241.7 110.3 256 128 256C145.7 256 160 241.7 160 224L160 160L224 160C241.7 160 256 145.7 256 128C256 110.3 241.7 96 224 96L128 96zM160 416C160 398.3 145.7 384 128 384C110.3 384 96 398.3 96 416L96 512C96 529.7 110.3 544 128 544L224 544C241.7 544 256 529.7 256 512C256 494.3 241.7 480 224 480L160 480L160 416zM416 96C398.3 96 384 110.3 384 128C384 145.7 398.3 160 416 160L480 160L480 224C480 241.7 494.3 256 512 256C529.7 256 544 241.7 544 224L544 128C544 110.3 529.7 96 512 96L416 96zM544 416C544 398.3 529.7 384 512 384C494.3 384 480 398.3 480 416L480 480L416 480C398.3 480 384 494.3 384 512C384 529.7 398.3 544 416 544L512 544C529.7 544 544 529.7 544 512L544 416z"/></svg>
|
||||
|
After Width: | Height: | Size: 937 B |
Reference in New Issue
Block a user