layout update with height issue

This commit is contained in:
Paul Marbach
2025-12-05 11:59:38 -05:00
parent c2b8e7fcd5
commit d2f44c3859
8 changed files with 398 additions and 448 deletions
@@ -1,229 +0,0 @@
import { css } from '@emotion/css';
import { memo, useCallback, useRef } from 'react';
import { DataFrame, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { SceneDataQuery, VizPanel } from '@grafana/scenes';
import { Container, ScrollContainer, useStyles2 } from '@grafana/ui';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { DetailViewHeader } from './DetailViewHeader';
import { ExpressionDetailView } from './ExpressionDetailView';
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
import { QueryDetailView } from './QueryDetailView';
import { QueryLibraryView, QueryLibraryViewRef } from './QueryLibraryView';
import { TransformationPickerView } from './TransformationPickerView';
import { TabId, QueryTransformItem } from './types';
export interface QueryLibraryMode {
active: boolean;
mode: 'browse' | 'save';
currentQuery?: SceneDataQuery;
}
interface DetailViewProps {
selectedItem: QueryTransformItem | undefined;
panel: VizPanel;
tabs: Array<{ tabId: TabId }>;
onRemoveTransform?: (index: number) => void;
onToggleTransformVisibility?: (index: number) => void;
isAddingTransform?: boolean;
onAddTransformation?: (selectedItem: SelectableValue<string>, customOptions?: Record<string, unknown>) => void;
onCancelAddTransform?: () => void;
transformationData?: DataFrame[];
onGoToQueries?: () => void;
queryLibraryMode?: QueryLibraryMode;
onQueryLibrarySelect?: (query: SceneDataQuery) => void;
onQueryLibrarySave?: (name: string, description: string) => void;
onQueryLibraryClose?: () => void;
onOpenQueryLibrary?: (mode: 'browse' | 'save', index?: number) => void;
onOpenQueryInspector?: () => void;
isDebugMode?: boolean;
debugPosition?: number;
}
export const DetailView = memo(
({
selectedItem,
panel,
tabs,
onRemoveTransform,
onToggleTransformVisibility,
isAddingTransform,
onAddTransformation,
onCancelAddTransform,
transformationData,
onGoToQueries,
queryLibraryMode,
onQueryLibrarySelect,
onQueryLibrarySave,
onQueryLibraryClose,
onOpenQueryLibrary,
onOpenQueryInspector,
isDebugMode,
debugPosition,
}: DetailViewProps) => {
const styles = useStyles2(getStyles);
const queryLibraryRef = useRef<QueryLibraryViewRef>(null);
const handleSelectQueryFromHeader = useCallback(() => {
queryLibraryRef.current?.selectCurrentQuery();
}, []);
const handleSaveQueryFromHeader = useCallback(() => {
queryLibraryRef.current?.saveQuery();
}, []);
const renderContent = useCallback(() => {
// Show transformation picker when in add mode
if (isAddingTransform && onAddTransformation && onCancelAddTransform) {
return (
<TransformationPickerView
data={transformationData || []}
onAddTransformation={onAddTransformation}
onCancel={onCancelAddTransform}
onGoToQueries={onGoToQueries}
/>
);
}
// Show QueryLibraryView when in query library mode
if (queryLibraryMode?.active && onQueryLibraryClose) {
return (
<>
<DetailViewHeader
queryLibraryMode={queryLibraryMode.mode}
onSelectQuery={queryLibraryMode.mode === 'browse' ? handleSelectQueryFromHeader : undefined}
onSaveQuery={queryLibraryMode.mode === 'save' ? handleSaveQueryFromHeader : undefined}
onClose={onQueryLibraryClose}
/>
<QueryLibraryView
ref={queryLibraryRef}
mode={queryLibraryMode.mode}
currentQuery={queryLibraryMode.currentQuery}
onSelectQuery={onQueryLibrarySelect}
onSaveQuery={onQueryLibrarySave}
onClose={onQueryLibraryClose}
/>
</>
);
}
if (!selectedItem) {
return (
<div className={styles.emptyState}>
<p>
<Trans i18nKey="dashboard-scene.panel-data-pane.empty-state">
Select a query or transformation to edit
</Trans>
</p>
</div>
);
}
if (selectedItem.type === 'query') {
const query = selectedItem.data;
return (
<>
<DetailViewHeader
selectedItem={selectedItem}
panel={panel}
onOpenQueryLibrary={onOpenQueryLibrary}
onOpenQueryInspector={onOpenQueryInspector}
/>
<ScrollContainer>
<QueryDetailView panel={panel} query={query} queryIndex={selectedItem.index} />
</ScrollContainer>
</>
);
} else if (selectedItem.type === 'expression') {
const data = selectedItem.data;
if (isExpressionQuery(data)) {
return (
<>
<DetailViewHeader selectedItem={selectedItem} panel={panel} onOpenQueryInspector={onOpenQueryInspector} />
<ScrollContainer>
<ExpressionDetailView panel={panel} expression={data} expressionIndex={selectedItem.index} />
</ScrollContainer>
</>
);
}
} else {
const transformsTab = tabs.find((t): t is PanelDataTransformationsTab => t.tabId === TabId.Transformations);
if (transformsTab) {
return (
<>
<DetailViewHeader
selectedItem={selectedItem}
panel={panel}
onRemoveTransform={onRemoveTransform}
onToggleTransformVisibility={onToggleTransformVisibility}
isDebugMode={isDebugMode}
debugPosition={debugPosition}
/>
<ScrollContainer>
<Container>
<PanelDataTransformationsTabRendered model={transformsTab} selectedIdx={selectedItem.index} />
</Container>
</ScrollContainer>
</>
);
}
}
return null;
}, [
isAddingTransform,
onAddTransformation,
onCancelAddTransform,
queryLibraryMode?.active,
queryLibraryMode?.mode,
queryLibraryMode?.currentQuery,
onQueryLibraryClose,
selectedItem,
transformationData,
onGoToQueries,
handleSelectQueryFromHeader,
handleSaveQueryFromHeader,
onQueryLibrarySelect,
onQueryLibrarySave,
styles.emptyState,
panel,
onOpenQueryLibrary,
onOpenQueryInspector,
tabs,
onRemoveTransform,
onToggleTransformVisibility,
isDebugMode,
debugPosition,
]);
return <div className={styles.container}>{renderContent()}</div>;
}
);
DetailView.displayName = 'DetailView';
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
background: theme.colors.background.primary,
border: `1px solid ${theme.colors.border.weak}`,
borderTopLeftRadius: theme.shape.radius.md,
borderTopRightRadius: theme.shape.radius.md,
overflow: 'hidden',
}),
emptyState: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
color: theme.colors.text.secondary,
fontSize: theme.typography.h5.fontSize,
}),
};
};
@@ -1,8 +1,6 @@
import { css, cx } from '@emotion/css';
import { useCallback, useMemo } from 'react';
import { DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { DataTransformerConfig, SelectableValue } from '@grafana/data';
import {
SceneComponentProps,
SceneDataQuery,
@@ -13,7 +11,6 @@ import {
SceneObjectUrlValues,
VizPanel,
} from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import { getConfig } from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv';
import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control';
@@ -22,11 +19,10 @@ import { ExpressionQueryType } from 'app/features/expressions/types';
import { getQueryRunnerFor } from '../../utils/utils';
import { DetailView, QueryLibraryMode } from './DetailView';
import { PanelDataAlertingTab } from './PanelDataAlertingTab';
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
import { PanelDataSidebarRendered } from './PanelDataSidebar';
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
import { QueryTransformDetailView, QueryLibraryMode } from './QueryTransformDetailView';
import { useQueryTransformItems } from './hooks';
import { PanelDataPaneTab, TabId } from './types';
import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
@@ -34,7 +30,6 @@ import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
export interface PanelDataPaneState extends SceneObjectState {
tabs: PanelDataPaneTab[];
tab: TabId;
sidebarCollapsed: boolean;
selectedQueryTransform: string | null;
panelRef: SceneObjectRef<VizPanel>;
transformPickerIndex?: number | null;
@@ -45,7 +40,6 @@ export interface PanelDataPaneState extends SceneObjectState {
export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
static Component = PanelDataPaneRendered;
static FooterComponent = PanelDataSidebarRendered;
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab', 'selectedQueryTransform'] });
@@ -67,7 +61,6 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
panelRef,
tabs,
tab,
sidebarCollapsed: false,
queryLibraryMode: {
active: false,
mode: 'browse',
@@ -86,10 +79,6 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
this.setState({ selectedQueryTransform: selectedId });
};
public onCollapseSidebar = (newState: boolean) => {
this.setState({ sidebarCollapsed: newState });
};
public onTransformPicker = (index?: number | null) => {
this.setState({ transformPickerIndex: index });
};
@@ -130,7 +119,6 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
isDebugMode = false,
debugPosition = 0,
} = model.useState();
const styles = useStyles2(getStyles);
// Subscribe to query runner and tab state changes
const panel = panelRef.resolve();
@@ -325,30 +313,26 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
const series = sourceData?.data?.series || [];
return (
<div className={styles.dataPane} data-testid={selectors.components.PanelEditor.DataPane.content}>
<div className={cx(styles.unifiedLayout)}>
<DetailView
selectedItem={selectedItem}
panel={panel}
tabs={tabs}
onRemoveTransform={handleRemoveTransform}
onToggleTransformVisibility={handleToggleTransformVisibility}
isAddingTransform={transformPickerIndex != null}
onAddTransformation={handleAddTransform}
onCancelAddTransform={() => model.onTransformPicker(null)}
transformationData={series}
onGoToQueries={handleGoToQueries}
queryLibraryMode={queryLibraryMode}
onQueryLibrarySelect={handleQueryLibrarySelect}
onQueryLibrarySave={handleQueryLibrarySave}
onQueryLibraryClose={handleQueryLibraryClose}
onOpenQueryLibrary={handleOpenQueryLibrary}
onOpenQueryInspector={handleOpenQueryInspector}
isDebugMode={isDebugMode}
debugPosition={debugPosition}
/>
</div>
</div>
<QueryTransformDetailView
selectedItem={selectedItem}
panel={panel}
tabs={tabs}
onRemoveTransform={handleRemoveTransform}
onToggleTransformVisibility={handleToggleTransformVisibility}
isAddingTransform={transformPickerIndex != null}
onAddTransformation={handleAddTransform}
onCancelAddTransform={() => model.onTransformPicker(null)}
transformationData={series}
onGoToQueries={handleGoToQueries}
queryLibraryMode={queryLibraryMode}
onQueryLibrarySelect={handleQueryLibrarySelect}
onQueryLibrarySave={handleQueryLibrarySave}
onQueryLibraryClose={handleQueryLibraryClose}
onOpenQueryLibrary={handleOpenQueryLibrary}
onOpenQueryInspector={handleOpenQueryInspector}
isDebugMode={isDebugMode}
debugPosition={debugPosition}
/>
);
}
@@ -365,23 +349,3 @@ export function shouldShowAlertingTab(pluginId: string) {
return isGraph || isTimeseries;
}
function getStyles(theme: GrafanaTheme2) {
return {
dataPane: css({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
minHeight: 0,
height: '100%',
width: '100%',
paddingLeft: theme.spacing(2),
}),
unifiedLayout: css({
flex: 1,
minHeight: 0,
background: theme.colors.background.primary,
overflow: 'hidden',
}),
};
}
@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useCallback, useEffect, useMemo } from 'react';
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo } from 'react';
import { DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n';
@@ -13,18 +13,35 @@ import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/d
import { ExpressionQueryType } from '../../../expressions/types';
import { getQueryRunnerFor } from '../../utils/utils';
import { QueryLibraryMode } from './DetailView';
import { PanelDataPane } from './PanelDataPane';
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
import { QueryLibraryMode } from './QueryTransformDetailView';
import { QueryTransformList } from './QueryTransformList';
import { useQueryTransformItems } from './hooks';
import { TabId } from './types';
import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
export function PanelDataSidebarRendered({ model }: SceneComponentProps<PanelDataPane>) {
const { panelRef, tabs, selectedQueryTransform, sidebarCollapsed, transformPickerIndex } = model.useState();
const styles = useStyles2(getStyles, sidebarCollapsed);
export enum SidebarSize {
Mini = 'mini',
Full = 'full',
}
export interface SidebarState {
size: SidebarSize;
collapsed: boolean;
}
export function PanelDataSidebar({
model,
sidebarState,
setSidebarState,
}: SceneComponentProps<PanelDataPane> & {
sidebarState: SidebarState;
setSidebarState: Dispatch<SetStateAction<SidebarState>>;
}) {
const { panelRef, tabs, selectedQueryTransform, transformPickerIndex } = model.useState();
const styles = useStyles2(getStyles, sidebarState);
const panel = panelRef.resolve();
@@ -291,13 +308,13 @@ export function PanelDataSidebarRendered({ model }: SceneComponentProps<PanelDat
// const sourceData = queryRunner?.useState();
// const series = sourceData?.data?.series || [];
if (sidebarCollapsed) {
if (sidebarState.collapsed) {
return (
<div className={styles.sidebarPane}>
<Button
icon="angle-right"
variant="secondary"
onClick={() => model.onCollapseSidebar(false)}
onClick={() => setSidebarState((prevState) => ({ ...prevState, collapsed: true }))}
tooltip={t('app.features.dashboardScene.panelEdit.panelDataPane.expandSidebar', 'Expand sidebar')}
/>
</div>
@@ -311,7 +328,9 @@ export function PanelDataSidebarRendered({ model }: SceneComponentProps<PanelDat
dataSourceItems={queryExpressionItems}
transformItems={transformItems}
selectedId={selectedId}
onCollapseSidebar={() => model.onCollapseSidebar(true)}
sidebarSize={sidebarState.size}
onResizeSidebar={(size: SidebarSize) => setSidebarState((prevState) => ({ ...prevState, size }))}
onCollapseSidebar={() => setSidebarState((prevState) => ({ ...prevState, collapsed: true }))}
onSelect={(id) => {
model.onChangeSelected(id);
model.onTransformPicker(null);
@@ -368,17 +387,24 @@ export function shouldShowAlertingTab(pluginId: string) {
return isGraph || isTimeseries;
}
function getStyles(theme: GrafanaTheme2, collapsed: boolean) {
function getStyles(theme: GrafanaTheme2, sidebarState: SidebarState) {
return {
sidebarPane: css({
overflow: 'hidden',
height: '100%',
width: collapsed ? 49 : 300,
padding: collapsed ? theme.spacing(1) : 'unset',
width: sidebarState.collapsed ? 49 : '100%',
padding: sidebarState.collapsed ? theme.spacing(1) : 'unset',
borderTopRightRadius: theme.shape.radius.md,
borderTop: `1px solid ${theme.colors.border.weak}`,
borderRight: `1px solid ${theme.colors.border.weak}`,
border: `1px solid ${theme.colors.border.weak}`,
borderBottom: 'none',
background: theme.colors.background.primary,
...(sidebarState.size === SidebarSize.Mini
? {
borderTopLeftRadius: theme.shape.radius.md,
}
: {
borderLeft: 'none',
}),
}),
};
}
@@ -0,0 +1,209 @@
import { css } from '@emotion/css';
import { memo, useCallback, useRef } from 'react';
import { DataFrame, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans } from '@grafana/i18n';
import { SceneDataQuery, VizPanel } from '@grafana/scenes';
import { Container, ScrollContainer, useStyles2 } from '@grafana/ui';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { ExpressionDetailView } from './ExpressionDetailView';
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
import { QueryDetailView } from './QueryDetailView';
import { QueryLibraryView, QueryLibraryViewRef } from './QueryLibraryView';
import { QueryTransformDetailViewHeader } from './QueryTransformDetailViewHeader';
import { TransformationPickerView } from './TransformationPickerView';
import { TabId, QueryTransformItem } from './types';
export interface QueryLibraryMode {
active: boolean;
mode: 'browse' | 'save';
currentQuery?: SceneDataQuery;
}
interface QueryTransformDetailViewProps {
selectedItem: QueryTransformItem | undefined;
panel: VizPanel;
tabs: Array<{ tabId: TabId }>;
onRemoveTransform?: (index: number) => void;
onToggleTransformVisibility?: (index: number) => void;
isAddingTransform?: boolean;
onAddTransformation?: (selectedItem: SelectableValue<string>, customOptions?: Record<string, unknown>) => void;
onCancelAddTransform?: () => void;
transformationData?: DataFrame[];
onGoToQueries?: () => void;
queryLibraryMode?: QueryLibraryMode;
onQueryLibrarySelect?: (query: SceneDataQuery) => void;
onQueryLibrarySave?: (name: string, description: string) => void;
onQueryLibraryClose?: () => void;
onOpenQueryLibrary?: (mode: 'browse' | 'save', index?: number) => void;
onOpenQueryInspector?: () => void;
isDebugMode?: boolean;
debugPosition?: number;
}
const QueryTransformDetailViewContent = ({
selectedItem,
panel,
tabs,
onRemoveTransform,
onToggleTransformVisibility,
isAddingTransform,
onAddTransformation,
onCancelAddTransform,
transformationData,
onGoToQueries,
queryLibraryMode,
onQueryLibrarySelect,
onQueryLibrarySave,
onQueryLibraryClose,
onOpenQueryLibrary,
onOpenQueryInspector,
isDebugMode,
debugPosition,
styles,
}: QueryTransformDetailViewProps & { styles: ReturnType<typeof getStyles> }) => {
const queryLibraryRef = useRef<QueryLibraryViewRef>(null);
const handleSelectQueryFromHeader = useCallback(() => {
queryLibraryRef.current?.selectCurrentQuery();
}, []);
const handleSaveQueryFromHeader = useCallback(() => {
queryLibraryRef.current?.saveQuery();
}, []);
// Show transformation picker when in add mode
if (isAddingTransform && onAddTransformation && onCancelAddTransform) {
return (
<TransformationPickerView
data={transformationData || []}
onAddTransformation={onAddTransformation}
onCancel={onCancelAddTransform}
onGoToQueries={onGoToQueries}
/>
);
}
// Show QueryLibraryView when in query library mode
if (queryLibraryMode?.active && onQueryLibraryClose) {
return (
<>
<QueryTransformDetailViewHeader
queryLibraryMode={queryLibraryMode.mode}
onSelectQuery={queryLibraryMode.mode === 'browse' ? handleSelectQueryFromHeader : undefined}
onSaveQuery={queryLibraryMode.mode === 'save' ? handleSaveQueryFromHeader : undefined}
onClose={onQueryLibraryClose}
/>
<QueryLibraryView
ref={queryLibraryRef}
mode={queryLibraryMode.mode}
currentQuery={queryLibraryMode.currentQuery}
onSelectQuery={onQueryLibrarySelect}
onSaveQuery={onQueryLibrarySave}
onClose={onQueryLibraryClose}
/>
</>
);
}
if (!selectedItem) {
return (
<div className={styles.emptyState}>
<p>
<Trans i18nKey="dashboard-scene.panel-data-pane.empty-state">Select a query or transformation to edit</Trans>
</p>
</div>
);
}
if (selectedItem.type === 'query') {
const query = selectedItem.data;
return (
<>
<QueryTransformDetailViewHeader
selectedItem={selectedItem}
panel={panel}
onOpenQueryLibrary={onOpenQueryLibrary}
onOpenQueryInspector={onOpenQueryInspector}
/>
<ScrollContainer>
<QueryDetailView panel={panel} query={query} queryIndex={selectedItem.index} />
</ScrollContainer>
</>
);
} else if (selectedItem.type === 'expression') {
const data = selectedItem.data;
if (isExpressionQuery(data)) {
return (
<>
<QueryTransformDetailViewHeader
selectedItem={selectedItem}
panel={panel}
onOpenQueryInspector={onOpenQueryInspector}
/>
<ScrollContainer>
<ExpressionDetailView panel={panel} expression={data} expressionIndex={selectedItem.index} />
</ScrollContainer>
</>
);
}
} else {
const transformsTab = tabs.find((t): t is PanelDataTransformationsTab => t.tabId === TabId.Transformations);
if (transformsTab) {
return (
<>
<QueryTransformDetailViewHeader
selectedItem={selectedItem}
panel={panel}
onRemoveTransform={onRemoveTransform}
onToggleTransformVisibility={onToggleTransformVisibility}
isDebugMode={isDebugMode}
debugPosition={debugPosition}
/>
<ScrollContainer>
<Container>
<PanelDataTransformationsTabRendered model={transformsTab} selectedIdx={selectedItem.index} />
</Container>
</ScrollContainer>
</>
);
}
}
return null;
};
export const QueryTransformDetailView = memo((props: QueryTransformDetailViewProps) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.container} data-testid={selectors.components.PanelEditor.DataPane.content}>
<QueryTransformDetailViewContent {...props} styles={styles} />
</div>
);
});
QueryTransformDetailView.displayName = 'QueryTransformDetailView';
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
background: theme.colors.background.primary,
border: `1px solid ${theme.colors.border.weak}`,
borderTopLeftRadius: theme.shape.radius.md,
borderTopRightRadius: theme.shape.radius.md,
overflow: 'hidden',
}),
emptyState: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
color: theme.colors.text.secondary,
fontSize: theme.typography.h5.fontSize,
}),
});
@@ -67,7 +67,7 @@ interface QueryLibraryModeProps {
onClose: () => void;
}
type DetailViewHeaderProps = ItemModeProps | QueryLibraryModeProps;
type QueryTransformDetailViewHeaderProps = ItemModeProps | QueryLibraryModeProps;
const ITEM_CONFIG = (theme: GrafanaTheme2) => ({
query: {
@@ -682,7 +682,7 @@ function ItemHeader({
}
// Main component that delegates to the appropriate sub-component
export const DetailViewHeader = (props: DetailViewHeaderProps) => {
export const QueryTransformDetailViewHeader = (props: QueryTransformDetailViewHeaderProps) => {
if (props.queryLibraryMode) {
return (
<QueryLibraryHeader
@@ -10,6 +10,7 @@ import { ExpressionQueryType } from 'app/features/expressions/types';
import { AddDataItemMenu } from './AddDataItemMenu';
import { AiModeCard } from './AiModeCard';
import { ConnectionLines } from './ConnectionLines';
import { SidebarSize } from './PanelDataSidebar';
import { QueryTransformCard } from './QueryTransformCard';
import { getItemHiddenState, restoreItemStates, syncItemsToDebugState } from './debugModeHelpers';
import { usePanelDataPaneColors } from './theme';
@@ -21,6 +22,7 @@ const CARD_HEIGHT = 70;
interface QueryTransformListProps {
allItems: QueryTransformItem[];
dataSourceItems: QueryItem[];
sidebarSize: SidebarSize;
transformItems: TransformItem[];
selectedId: string | null;
onSelect: (id: string) => void;
@@ -36,6 +38,7 @@ interface QueryTransformListProps {
onReorderDataSources?: (startIndex: number, endIndex: number) => void;
onReorderTransforms?: (startIndex: number, endIndex: number) => void;
onAddOrganizeFieldsTransform?: () => void;
onResizeSidebar: (size: SidebarSize) => void;
onCollapseSidebar: () => void;
onDebugStateChange?: (isDebugMode: boolean, debugPosition: number) => void;
}
@@ -46,6 +49,7 @@ export const QueryTransformList = memo(
transformItems,
allItems,
selectedId,
sidebarSize,
onSelect,
onAddQuery,
onAddFromSavedQueries,
@@ -59,6 +63,7 @@ export const QueryTransformList = memo(
onReorderDataSources,
onReorderTransforms,
onAddOrganizeFieldsTransform,
onResizeSidebar,
onCollapseSidebar,
onDebugStateChange,
}: QueryTransformListProps) => {
@@ -353,9 +358,12 @@ export const QueryTransformList = memo(
<div className={styles.header}>
<Stack justifyContent="space-between" alignItems="center" gap={2}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div className={styles.headerTitle} onClick={onCollapseSidebar}>
<div
className={styles.headerTitle}
onClick={() => onResizeSidebar(sidebarSize === SidebarSize.Mini ? SidebarSize.Full : SidebarSize.Mini)}
>
<Stack direction="row" alignItems="center" gap={1}>
<Icon name="angle-down" />
<Icon name={sidebarSize === SidebarSize.Mini ? 'expand-arrows' : 'compress-arrows'} />
<span>{t('dashboard-scene.query-transform-list.header', 'Pipeline flow')}</span>
</Stack>
</div>
@@ -1,11 +1,12 @@
import { css, cx } from '@emotion/css';
import { useEffect, useMemo } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { SceneComponentProps, VizPanel } from '@grafana/scenes';
import { Button, Spinner, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui';
import { SceneComponentProps } from '@grafana/scenes';
import { Spinner, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui';
import { MIN_SUGGESTIONS_PANE_WIDTH } from 'app/features/panel/suggestions/constants';
import { useEditPaneCollapsed } from '../edit-pane/shared';
@@ -13,7 +14,7 @@ import { NavToolbarActions } from '../scene/NavToolbarActions';
import { UnlinkModal } from '../scene/UnlinkModal';
import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils';
import { PanelDataSidebarRendered } from './PanelDataPane/PanelDataSidebar';
import { PanelDataSidebar, SidebarSize, SidebarState } from './PanelDataPane/PanelDataSidebar';
import { PanelEditor } from './PanelEditor';
import { SaveLibraryVizPanelModal } from './SaveLibraryVizPanelModal';
import { useSnappingSplitter } from './splitter/useSnappingSplitter';
@@ -22,7 +23,7 @@ import { scrollReflowMediaCondition, useScrollReflowLimit } from './useScrollRef
export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>) {
const dashboard = getDashboardSceneFor(model);
const { optionsPane } = model.useState();
const styles = useStyles2(getStyles);
const styles = useStyles2(getWrapperStyles);
const [isInitiallyCollapsed, setIsCollapsed] = useEditPaneCollapsed();
const isScrollingLayout = useScrollReflowLimit();
@@ -86,150 +87,93 @@ function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
const panel = model.getPanel();
const libraryPanel = getLibraryPanelBehavior(panel);
const { controls } = dashboard.useState();
const styles = useStyles2(getStyles);
const [sidebarState, setSidebarState] = useState<SidebarState>({ size: SidebarSize.Mini, collapsed: false });
const [containerRef, { height: containerHeight }] = useMeasure<HTMLDivElement>();
const [vizRef, { height: vizHeight }] = useMeasure<HTMLDivElement>();
const styles = useStyles2(getStyles, sidebarState);
const isScrollingLayout = useScrollReflowLimit();
const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } =
useSnappingSplitter({
direction: 'column',
dragPosition: 'start',
initialSize: 0.5,
collapseBelowPixels: 150,
disabled: isScrollingLayout,
});
const gridStyles = useMemo(() => {
const rows = [];
const grid = [];
containerProps.className = cx(containerProps.className, styles.container);
if (controls) {
rows.push('auto');
grid.push(['controls', 'controls']);
}
if (!dataPane && !isScrollingLayout) {
primaryProps.style.flexGrow = 1;
}
grid.push(['viz', 'viz']);
rows.push('1fr');
if (dataPane) {
// rows.push(`${(containerHeight - vizHeight) + 40}px`);
rows.push('auto');
grid.push(['sidebar', 'data-pane']);
if (sidebarState.size === SidebarSize.Full) {
for (let i = 0; i < grid.length; i++) {
grid[i][0] = 'sidebar';
}
}
}
return {
gridTemplateAreas: '\n' + grid.map((row) => `"${row.join(' ')}"`).join('\n'),
// gridTemplateRows: rows.map((r) => r).join(' '),
};
}, [controls, dataPane, sidebarState.size]);
return (
<div className={cx(styles.pageContainer, controls && styles.pageContainerWithControls)}>
<div ref={containerRef} className={styles.pageContainer} style={gridStyles}>
{controls && (
<div className={styles.controlsWrapper}>
<controls.Component model={controls} />
</div>
)}
{dataPane && (
<div className={styles.sidebar}>
<PanelDataSidebarRendered model={dataPane} />
</div>
)}
<div {...containerProps}>
<div {...primaryProps} className={cx(primaryProps.className, isScrollingLayout && styles.fixedSizeViz)}>
<VizWrapper panel={panel} tableView={tableView} />
</div>
{showLibraryPanelSaveModal && libraryPanel && (
<SaveLibraryVizPanelModal
libraryPanel={libraryPanel}
onDismiss={model.onDismissLibraryPanelSaveModal}
onConfirm={model.onConfirmSaveLibraryPanel}
onDiscard={model.onDiscard}
></SaveLibraryVizPanelModal>
)}
{showLibraryPanelUnlinkModal && libraryPanel && (
<UnlinkModal
onDismiss={model.onDismissUnlinkLibraryPanelModal}
onConfirm={model.onConfirmUnlinkLibraryPanel}
isOpen
/>
)}
{dataPane && (
<>
<div {...splitterProps} />
<div
{...secondaryProps}
className={cx(secondaryProps.className, isScrollingLayout && styles.fullSizeEditor)}
>
{splitterState.collapsed && (
<div className={styles.expandDataPane}>
<Button
tooltip={t('dashboard-scene.viz-and-data-pane.tooltip-open-query-pane', 'Open query pane')}
icon={'arrow-to-right'}
onClick={onToggleCollapse}
variant="secondary"
size="sm"
className={styles.openDataPaneButton}
aria-label={t('dashboard-scene.viz-and-data-pane.aria-label-open-query-pane', 'Open query pane')}
/>
</div>
)}
{!splitterState.collapsed && <dataPane.Component model={dataPane} />}
</div>
</>
)}
<div ref={vizRef} className={cx(styles.viz, isScrollingLayout && styles.fixedSizeViz)}>
{tableView ? <tableView.Component model={tableView} /> : <panel.Component model={panel} />}
</div>
{dataPane && (
<>
<div className={cx(styles.dataPane, isScrollingLayout && styles.fullSizeEditor)}>
<dataPane.Component model={dataPane} />
</div>
<div className={styles.sidebar}>
<PanelDataSidebar model={dataPane} sidebarState={sidebarState} setSidebarState={setSidebarState} />
</div>
</>
)}
{showLibraryPanelSaveModal && libraryPanel && (
<SaveLibraryVizPanelModal
libraryPanel={libraryPanel}
onDismiss={model.onDismissLibraryPanelSaveModal}
onConfirm={model.onConfirmSaveLibraryPanel}
onDiscard={model.onDiscard}
></SaveLibraryVizPanelModal>
)}
{showLibraryPanelUnlinkModal && libraryPanel && (
<UnlinkModal
onDismiss={model.onDismissUnlinkLibraryPanelModal}
onConfirm={model.onConfirmUnlinkLibraryPanel}
isOpen
/>
)}
</div>
);
}
interface VizWrapperProps {
panel: VizPanel;
tableView?: VizPanel;
}
function VizWrapper({ panel, tableView }: VizWrapperProps) {
const styles = useStyles2(getStyles);
const panelToShow = tableView ?? panel;
return (
<div className={styles.vizWrapper}>
<panelToShow.Component model={panelToShow} />
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
function getWrapperStyles(theme: GrafanaTheme2) {
const scrollReflowMediaQuery = '@media ' + scrollReflowMediaCondition;
return {
pageContainer: css({
display: 'grid',
gridTemplateAreas: `
"sidebar panels"`,
gridTemplateColumns: `auto 1fr`,
gridTemplateRows: '1fr',
height: '100%',
[scrollReflowMediaQuery]: {
gridTemplateColumns: `100%`,
},
}),
pageContainerWithControls: css({
gridTemplateAreas: `
"sidebar controls"
"sidebar panels"`,
gridTemplateRows: 'auto 1fr',
}),
container: css({
gridArea: 'panels',
height: '100%',
}),
sidebar: css({
gridArea: 'sidebar',
height: '100%',
maxHeight: '100%',
overflow: 'auto',
paddingTop: theme.spacing(2),
}),
expandDataSidebar: css({
width: theme.spacing(6),
padding: theme.spacing(1),
}),
canvasContent: css({
label: 'canvas-content',
display: 'flex',
flexDirection: 'column',
flexBasis: '100%',
flexGrow: 1,
minHeight: 0,
width: '100%',
}),
content: css({
position: 'absolute',
width: '100%',
height: '100%',
overflow: 'unset',
paddingTop: theme.spacing(2),
[scrollReflowMediaQuery]: {
height: 'auto',
display: 'grid',
@@ -251,7 +195,6 @@ function getStyles(theme: GrafanaTheme2) {
flexDirection: 'column',
borderLeft: `1px solid ${theme.colors.border.weak}`,
background: theme.colors.background.primary,
marginTop: theme.spacing(2),
borderTop: `1px solid ${theme.colors.border.weak}`,
borderTopLeftRadius: theme.shape.radius.default,
}),
@@ -260,24 +203,58 @@ function getStyles(theme: GrafanaTheme2) {
flexDirection: 'column',
padding: theme.spacing(2, 1),
}),
expandDataPane: css({
display: 'flex',
flexDirection: 'row',
padding: theme.spacing(1),
borderTop: `1px solid ${theme.colors.border.weak}`,
borderRight: `1px solid ${theme.colors.border.weak}`,
background: theme.colors.background.primary,
flexGrow: 1,
justifyContent: 'space-around',
}),
rotate180: css({
rotate: '180deg',
}),
};
}
function getStyles(theme: GrafanaTheme2, sidebarState: SidebarState) {
const scrollReflowMediaQuery = '@media ' + scrollReflowMediaCondition;
return {
pageContainer: css({
display: 'grid',
gap: theme.spacing(2),
gridTemplateColumns: `auto 1fr`,
gridTemplateRows: `auto 0.7fr 0.3fr`,
height: '100%',
minHeight: '100%',
maxHeight: '100%',
overflow: 'hidden',
[scrollReflowMediaQuery]: {
gridTemplateColumns: `100%`,
},
}),
sidebar: css({
gridArea: 'sidebar',
overflow: 'auto',
resize: 'horizontal',
minWidth: 285,
maxWidth: 400,
...(sidebarState.size === SidebarSize.Mini && {
paddingLeft: theme.spacing(2),
}),
}),
viz: css({
gridArea: 'viz',
overflow: 'auto',
resize: 'vertical',
minHeight: 200,
maxHeight: 700, // FIXME: needs a dynamic height
...(sidebarState.size === SidebarSize.Mini && {
paddingLeft: theme.spacing(2),
}),
}),
dataPane: css({
gridArea: 'data-pane',
}),
controlsWrapper: css({
gridArea: 'controls',
display: 'flex',
flexDirection: 'column',
flexGrow: 0,
gridArea: 'controls',
...(sidebarState.size === SidebarSize.Mini && {
paddingLeft: theme.spacing(2),
}),
}),
openDataPaneButton: css({
width: theme.spacing(8),
@@ -286,11 +263,6 @@ function getStyles(theme: GrafanaTheme2) {
rotate: '-90deg',
},
}),
vizWrapper: css({
height: '100%',
width: '100%',
paddingLeft: theme.spacing(2),
}),
fixedSizeViz: css({
height: '100vh',
}),
@@ -250,12 +250,12 @@ function getStyles(theme: GrafanaTheme2) {
return {
controls: css({
gap: theme.spacing(1),
padding: theme.spacing(2, 2, 1, 2),
flexDirection: 'row',
flexWrap: 'nowrap',
position: 'relative',
width: '100%',
marginLeft: 'auto',
marginBottom: theme.spacing(-1),
[theme.breakpoints.down('sm')]: {
flexDirection: 'column-reverse',
alignItems: 'stretch',