Merge branch 'data-manipulation-improvements' of github.com:grafana/grafana into data-manipulation-improvements
This commit is contained in:
@@ -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 { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { DataTransformerConfig, SelectableValue } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
|
||||||
import {
|
import {
|
||||||
SceneComponentProps,
|
SceneComponentProps,
|
||||||
SceneDataQuery,
|
SceneDataQuery,
|
||||||
@@ -13,7 +11,6 @@ import {
|
|||||||
SceneObjectUrlValues,
|
SceneObjectUrlValues,
|
||||||
VizPanel,
|
VizPanel,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
|
||||||
import { getConfig } from 'app/core/config';
|
import { getConfig } from 'app/core/config';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control';
|
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 { getQueryRunnerFor } from '../../utils/utils';
|
||||||
|
|
||||||
import { DetailView, QueryLibraryMode } from './DetailView';
|
|
||||||
import { PanelDataAlertingTab } from './PanelDataAlertingTab';
|
import { PanelDataAlertingTab } from './PanelDataAlertingTab';
|
||||||
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
||||||
import { PanelDataSidebarRendered } from './PanelDataSidebar';
|
|
||||||
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
||||||
|
import { QueryTransformDetailView, QueryLibraryMode } from './QueryTransformDetailView';
|
||||||
import { useQueryTransformItems } from './hooks';
|
import { useQueryTransformItems } from './hooks';
|
||||||
import { PanelDataPaneTab, TabId } from './types';
|
import { PanelDataPaneTab, TabId } from './types';
|
||||||
import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
|
import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
|
||||||
@@ -34,7 +30,6 @@ import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
|
|||||||
export interface PanelDataPaneState extends SceneObjectState {
|
export interface PanelDataPaneState extends SceneObjectState {
|
||||||
tabs: PanelDataPaneTab[];
|
tabs: PanelDataPaneTab[];
|
||||||
tab: TabId;
|
tab: TabId;
|
||||||
sidebarCollapsed: boolean;
|
|
||||||
selectedQueryTransform: string | null;
|
selectedQueryTransform: string | null;
|
||||||
panelRef: SceneObjectRef<VizPanel>;
|
panelRef: SceneObjectRef<VizPanel>;
|
||||||
transformPickerIndex?: number | null;
|
transformPickerIndex?: number | null;
|
||||||
@@ -45,7 +40,6 @@ export interface PanelDataPaneState extends SceneObjectState {
|
|||||||
|
|
||||||
export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||||
static Component = PanelDataPaneRendered;
|
static Component = PanelDataPaneRendered;
|
||||||
static FooterComponent = PanelDataSidebarRendered;
|
|
||||||
|
|
||||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab', 'selectedQueryTransform'] });
|
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab', 'selectedQueryTransform'] });
|
||||||
|
|
||||||
@@ -67,7 +61,6 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
|||||||
panelRef,
|
panelRef,
|
||||||
tabs,
|
tabs,
|
||||||
tab,
|
tab,
|
||||||
sidebarCollapsed: false,
|
|
||||||
queryLibraryMode: {
|
queryLibraryMode: {
|
||||||
active: false,
|
active: false,
|
||||||
mode: 'browse',
|
mode: 'browse',
|
||||||
@@ -86,10 +79,6 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
|||||||
this.setState({ selectedQueryTransform: selectedId });
|
this.setState({ selectedQueryTransform: selectedId });
|
||||||
};
|
};
|
||||||
|
|
||||||
public onCollapseSidebar = (newState: boolean) => {
|
|
||||||
this.setState({ sidebarCollapsed: newState });
|
|
||||||
};
|
|
||||||
|
|
||||||
public onTransformPicker = (index?: number | null) => {
|
public onTransformPicker = (index?: number | null) => {
|
||||||
this.setState({ transformPickerIndex: index });
|
this.setState({ transformPickerIndex: index });
|
||||||
};
|
};
|
||||||
@@ -130,7 +119,6 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
|||||||
isDebugMode = false,
|
isDebugMode = false,
|
||||||
debugPosition = 0,
|
debugPosition = 0,
|
||||||
} = model.useState();
|
} = model.useState();
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
// Subscribe to query runner and tab state changes
|
// Subscribe to query runner and tab state changes
|
||||||
const panel = panelRef.resolve();
|
const panel = panelRef.resolve();
|
||||||
@@ -325,30 +313,26 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
|||||||
const series = sourceData?.data?.series || [];
|
const series = sourceData?.data?.series || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.dataPane} data-testid={selectors.components.PanelEditor.DataPane.content}>
|
<QueryTransformDetailView
|
||||||
<div className={cx(styles.unifiedLayout)}>
|
selectedItem={selectedItem}
|
||||||
<DetailView
|
panel={panel}
|
||||||
selectedItem={selectedItem}
|
tabs={tabs}
|
||||||
panel={panel}
|
onRemoveTransform={handleRemoveTransform}
|
||||||
tabs={tabs}
|
onToggleTransformVisibility={handleToggleTransformVisibility}
|
||||||
onRemoveTransform={handleRemoveTransform}
|
isAddingTransform={transformPickerIndex != null}
|
||||||
onToggleTransformVisibility={handleToggleTransformVisibility}
|
onAddTransformation={handleAddTransform}
|
||||||
isAddingTransform={transformPickerIndex != null}
|
onCancelAddTransform={() => model.onTransformPicker(null)}
|
||||||
onAddTransformation={handleAddTransform}
|
transformationData={series}
|
||||||
onCancelAddTransform={() => model.onTransformPicker(null)}
|
onGoToQueries={handleGoToQueries}
|
||||||
transformationData={series}
|
queryLibraryMode={queryLibraryMode}
|
||||||
onGoToQueries={handleGoToQueries}
|
onQueryLibrarySelect={handleQueryLibrarySelect}
|
||||||
queryLibraryMode={queryLibraryMode}
|
onQueryLibrarySave={handleQueryLibrarySave}
|
||||||
onQueryLibrarySelect={handleQueryLibrarySelect}
|
onQueryLibraryClose={handleQueryLibraryClose}
|
||||||
onQueryLibrarySave={handleQueryLibrarySave}
|
onOpenQueryLibrary={handleOpenQueryLibrary}
|
||||||
onQueryLibraryClose={handleQueryLibraryClose}
|
onOpenQueryInspector={handleOpenQueryInspector}
|
||||||
onOpenQueryLibrary={handleOpenQueryLibrary}
|
isDebugMode={isDebugMode}
|
||||||
onOpenQueryInspector={handleOpenQueryInspector}
|
debugPosition={debugPosition}
|
||||||
isDebugMode={isDebugMode}
|
/>
|
||||||
debugPosition={debugPosition}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,23 +349,3 @@ export function shouldShowAlertingTab(pluginId: string) {
|
|||||||
|
|
||||||
return isGraph || isTimeseries;
|
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 { 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 { DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { t } from '@grafana/i18n';
|
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 { ExpressionQueryType } from '../../../expressions/types';
|
||||||
import { getQueryRunnerFor } from '../../utils/utils';
|
import { getQueryRunnerFor } from '../../utils/utils';
|
||||||
|
|
||||||
import { QueryLibraryMode } from './DetailView';
|
|
||||||
import { PanelDataPane } from './PanelDataPane';
|
import { PanelDataPane } from './PanelDataPane';
|
||||||
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
||||||
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
||||||
|
import { QueryLibraryMode } from './QueryTransformDetailView';
|
||||||
import { QueryTransformList } from './QueryTransformList';
|
import { QueryTransformList } from './QueryTransformList';
|
||||||
import { useQueryTransformItems } from './hooks';
|
import { useQueryTransformItems } from './hooks';
|
||||||
import { TabId } from './types';
|
import { TabId } from './types';
|
||||||
import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
|
import { isDataTransformerConfig, queryItemId, transformItemId } from './utils';
|
||||||
|
|
||||||
export function PanelDataSidebarRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
export enum SidebarSize {
|
||||||
const { panelRef, tabs, selectedQueryTransform, sidebarCollapsed, transformPickerIndex } = model.useState();
|
Mini = 'mini',
|
||||||
const styles = useStyles2(getStyles, sidebarCollapsed);
|
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();
|
const panel = panelRef.resolve();
|
||||||
|
|
||||||
@@ -291,13 +308,13 @@ export function PanelDataSidebarRendered({ model }: SceneComponentProps<PanelDat
|
|||||||
// const sourceData = queryRunner?.useState();
|
// const sourceData = queryRunner?.useState();
|
||||||
// const series = sourceData?.data?.series || [];
|
// const series = sourceData?.data?.series || [];
|
||||||
|
|
||||||
if (sidebarCollapsed) {
|
if (sidebarState.collapsed) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.sidebarPane}>
|
<div className={styles.sidebarPane}>
|
||||||
<Button
|
<Button
|
||||||
icon="angle-right"
|
icon="angle-right"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => model.onCollapseSidebar(false)}
|
onClick={() => setSidebarState((prevState) => ({ ...prevState, collapsed: true }))}
|
||||||
tooltip={t('app.features.dashboardScene.panelEdit.panelDataPane.expandSidebar', 'Expand sidebar')}
|
tooltip={t('app.features.dashboardScene.panelEdit.panelDataPane.expandSidebar', 'Expand sidebar')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,7 +328,9 @@ export function PanelDataSidebarRendered({ model }: SceneComponentProps<PanelDat
|
|||||||
dataSourceItems={queryExpressionItems}
|
dataSourceItems={queryExpressionItems}
|
||||||
transformItems={transformItems}
|
transformItems={transformItems}
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
onCollapseSidebar={() => model.onCollapseSidebar(true)}
|
sidebarSize={sidebarState.size}
|
||||||
|
onResizeSidebar={(size: SidebarSize) => setSidebarState((prevState) => ({ ...prevState, size }))}
|
||||||
|
onCollapseSidebar={() => setSidebarState((prevState) => ({ ...prevState, collapsed: true }))}
|
||||||
onSelect={(id) => {
|
onSelect={(id) => {
|
||||||
model.onChangeSelected(id);
|
model.onChangeSelected(id);
|
||||||
model.onTransformPicker(null);
|
model.onTransformPicker(null);
|
||||||
@@ -368,17 +387,24 @@ export function shouldShowAlertingTab(pluginId: string) {
|
|||||||
return isGraph || isTimeseries;
|
return isGraph || isTimeseries;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2, collapsed: boolean) {
|
function getStyles(theme: GrafanaTheme2, sidebarState: SidebarState) {
|
||||||
return {
|
return {
|
||||||
sidebarPane: css({
|
sidebarPane: css({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: collapsed ? 49 : 300,
|
width: sidebarState.collapsed ? 49 : '100%',
|
||||||
padding: collapsed ? theme.spacing(1) : 'unset',
|
padding: sidebarState.collapsed ? theme.spacing(1) : 'unset',
|
||||||
borderTopRightRadius: theme.shape.radius.md,
|
borderTopRightRadius: theme.shape.radius.md,
|
||||||
borderTop: `1px solid ${theme.colors.border.weak}`,
|
border: `1px solid ${theme.colors.border.weak}`,
|
||||||
borderRight: `1px solid ${theme.colors.border.weak}`,
|
borderBottom: 'none',
|
||||||
background: theme.colors.background.primary,
|
background: theme.colors.background.primary,
|
||||||
|
...(sidebarState.size === SidebarSize.Mini
|
||||||
|
? {
|
||||||
|
borderTopLeftRadius: theme.shape.radius.md,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
borderLeft: 'none',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+209
@@ -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,
|
||||||
|
}),
|
||||||
|
});
|
||||||
+2
-2
@@ -67,7 +67,7 @@ interface QueryLibraryModeProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DetailViewHeaderProps = ItemModeProps | QueryLibraryModeProps;
|
type QueryTransformDetailViewHeaderProps = ItemModeProps | QueryLibraryModeProps;
|
||||||
|
|
||||||
const ITEM_CONFIG = (theme: GrafanaTheme2) => ({
|
const ITEM_CONFIG = (theme: GrafanaTheme2) => ({
|
||||||
query: {
|
query: {
|
||||||
@@ -682,7 +682,7 @@ function ItemHeader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main component that delegates to the appropriate sub-component
|
// Main component that delegates to the appropriate sub-component
|
||||||
export const DetailViewHeader = (props: DetailViewHeaderProps) => {
|
export const QueryTransformDetailViewHeader = (props: QueryTransformDetailViewHeaderProps) => {
|
||||||
if (props.queryLibraryMode) {
|
if (props.queryLibraryMode) {
|
||||||
return (
|
return (
|
||||||
<QueryLibraryHeader
|
<QueryLibraryHeader
|
||||||
+10
-2
@@ -10,6 +10,7 @@ import { ExpressionQueryType } from 'app/features/expressions/types';
|
|||||||
import { AddDataItemMenu } from './AddDataItemMenu';
|
import { AddDataItemMenu } from './AddDataItemMenu';
|
||||||
import { AiModeCard } from './AiModeCard';
|
import { AiModeCard } from './AiModeCard';
|
||||||
import { ConnectionLines } from './ConnectionLines';
|
import { ConnectionLines } from './ConnectionLines';
|
||||||
|
import { SidebarSize } from './PanelDataSidebar';
|
||||||
import { QueryTransformCard } from './QueryTransformCard';
|
import { QueryTransformCard } from './QueryTransformCard';
|
||||||
import { getItemHiddenState, restoreItemStates, syncItemsToDebugState } from './debugModeHelpers';
|
import { getItemHiddenState, restoreItemStates, syncItemsToDebugState } from './debugModeHelpers';
|
||||||
import { usePanelDataPaneColors } from './theme';
|
import { usePanelDataPaneColors } from './theme';
|
||||||
@@ -21,6 +22,7 @@ const CARD_HEIGHT = 70;
|
|||||||
interface QueryTransformListProps {
|
interface QueryTransformListProps {
|
||||||
allItems: QueryTransformItem[];
|
allItems: QueryTransformItem[];
|
||||||
dataSourceItems: QueryItem[];
|
dataSourceItems: QueryItem[];
|
||||||
|
sidebarSize: SidebarSize;
|
||||||
transformItems: TransformItem[];
|
transformItems: TransformItem[];
|
||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
@@ -36,6 +38,7 @@ interface QueryTransformListProps {
|
|||||||
onReorderDataSources?: (startIndex: number, endIndex: number) => void;
|
onReorderDataSources?: (startIndex: number, endIndex: number) => void;
|
||||||
onReorderTransforms?: (startIndex: number, endIndex: number) => void;
|
onReorderTransforms?: (startIndex: number, endIndex: number) => void;
|
||||||
onAddOrganizeFieldsTransform?: () => void;
|
onAddOrganizeFieldsTransform?: () => void;
|
||||||
|
onResizeSidebar: (size: SidebarSize) => void;
|
||||||
onCollapseSidebar: () => void;
|
onCollapseSidebar: () => void;
|
||||||
onDebugStateChange?: (isDebugMode: boolean, debugPosition: number) => void;
|
onDebugStateChange?: (isDebugMode: boolean, debugPosition: number) => void;
|
||||||
}
|
}
|
||||||
@@ -46,6 +49,7 @@ export const QueryTransformList = memo(
|
|||||||
transformItems,
|
transformItems,
|
||||||
allItems,
|
allItems,
|
||||||
selectedId,
|
selectedId,
|
||||||
|
sidebarSize,
|
||||||
onSelect,
|
onSelect,
|
||||||
onAddQuery,
|
onAddQuery,
|
||||||
onAddFromSavedQueries,
|
onAddFromSavedQueries,
|
||||||
@@ -59,6 +63,7 @@ export const QueryTransformList = memo(
|
|||||||
onReorderDataSources,
|
onReorderDataSources,
|
||||||
onReorderTransforms,
|
onReorderTransforms,
|
||||||
onAddOrganizeFieldsTransform,
|
onAddOrganizeFieldsTransform,
|
||||||
|
onResizeSidebar,
|
||||||
onCollapseSidebar,
|
onCollapseSidebar,
|
||||||
onDebugStateChange,
|
onDebugStateChange,
|
||||||
}: QueryTransformListProps) => {
|
}: QueryTransformListProps) => {
|
||||||
@@ -353,9 +358,12 @@ export const QueryTransformList = memo(
|
|||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<Stack justifyContent="space-between" alignItems="center" gap={2}>
|
<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 */}
|
{/* 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}>
|
<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>
|
<span>{t('dashboard-scene.query-transform-list.header', 'Pipeline flow')}</span>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
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 { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
import { SceneComponentProps, VizPanel } from '@grafana/scenes';
|
import { SceneComponentProps } from '@grafana/scenes';
|
||||||
import { Button, Spinner, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui';
|
import { Spinner, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
import { MIN_SUGGESTIONS_PANE_WIDTH } from 'app/features/panel/suggestions/constants';
|
import { MIN_SUGGESTIONS_PANE_WIDTH } from 'app/features/panel/suggestions/constants';
|
||||||
|
|
||||||
import { useEditPaneCollapsed } from '../edit-pane/shared';
|
import { useEditPaneCollapsed } from '../edit-pane/shared';
|
||||||
@@ -13,7 +14,7 @@ import { NavToolbarActions } from '../scene/NavToolbarActions';
|
|||||||
import { UnlinkModal } from '../scene/UnlinkModal';
|
import { UnlinkModal } from '../scene/UnlinkModal';
|
||||||
import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils';
|
import { getDashboardSceneFor, getLibraryPanelBehavior } from '../utils/utils';
|
||||||
|
|
||||||
import { PanelDataSidebarRendered } from './PanelDataPane/PanelDataSidebar';
|
import { PanelDataSidebar, SidebarSize, SidebarState } from './PanelDataPane/PanelDataSidebar';
|
||||||
import { PanelEditor } from './PanelEditor';
|
import { PanelEditor } from './PanelEditor';
|
||||||
import { SaveLibraryVizPanelModal } from './SaveLibraryVizPanelModal';
|
import { SaveLibraryVizPanelModal } from './SaveLibraryVizPanelModal';
|
||||||
import { useSnappingSplitter } from './splitter/useSnappingSplitter';
|
import { useSnappingSplitter } from './splitter/useSnappingSplitter';
|
||||||
@@ -22,8 +23,13 @@ import { scrollReflowMediaCondition, useScrollReflowLimit } from './useScrollRef
|
|||||||
export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>) {
|
export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>) {
|
||||||
const dashboard = getDashboardSceneFor(model);
|
const dashboard = getDashboardSceneFor(model);
|
||||||
const { optionsPane } = model.useState();
|
const { optionsPane } = model.useState();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getWrapperStyles);
|
||||||
const [isInitiallyCollapsed, setIsCollapsed] = useEditPaneCollapsed();
|
const [isInitiallyCollapsed, setIsCollapsed] = useEditPaneCollapsed();
|
||||||
|
const [containerRef, { height: containerHeight }] = useMeasure<HTMLDivElement>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('PanelEditorRenderer containerHeight', containerHeight);
|
||||||
|
}, [containerHeight]);
|
||||||
|
|
||||||
const isScrollingLayout = useScrollReflowLimit();
|
const isScrollingLayout = useScrollReflowLimit();
|
||||||
|
|
||||||
@@ -45,7 +51,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
|||||||
}, [splitterState.collapsed, setIsCollapsed]);
|
}, [splitterState.collapsed, setIsCollapsed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ height: '100%' }} ref={containerRef}>
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
<NavToolbarActions dashboard={dashboard} />
|
||||||
<div
|
<div
|
||||||
{...containerProps}
|
{...containerProps}
|
||||||
@@ -53,7 +59,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
|||||||
data-testid={selectors.components.PanelEditor.General.content}
|
data-testid={selectors.components.PanelEditor.General.content}
|
||||||
>
|
>
|
||||||
<div {...primaryProps} className={cx(primaryProps.className, styles.body)}>
|
<div {...primaryProps} className={cx(primaryProps.className, styles.body)}>
|
||||||
<VizAndDataPane model={model} />
|
<VizAndDataPane model={model} containerHeight={Math.max(containerHeight, 500)} />
|
||||||
</div>
|
</div>
|
||||||
<div {...splitterProps} />
|
<div {...splitterProps} />
|
||||||
<div {...secondaryProps} className={cx(secondaryProps.className, styles.optionsPane)}>
|
<div {...secondaryProps} className={cx(secondaryProps.className, styles.optionsPane)}>
|
||||||
@@ -76,160 +82,114 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
|||||||
{!splitterState.collapsed && !optionsPane && <Spinner />}
|
{!splitterState.collapsed && !optionsPane && <Spinner />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VizAndDataPane({ model }: SceneComponentProps<PanelEditor>) {
|
function VizAndDataPane({
|
||||||
|
model,
|
||||||
|
containerHeight = 800,
|
||||||
|
}: SceneComponentProps<PanelEditor> & { containerHeight?: number }) {
|
||||||
const dashboard = getDashboardSceneFor(model);
|
const dashboard = getDashboardSceneFor(model);
|
||||||
const { dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal, tableView } = model.useState();
|
const { dataPane, showLibraryPanelSaveModal, showLibraryPanelUnlinkModal, tableView } = model.useState();
|
||||||
const panel = model.getPanel();
|
const panel = model.getPanel();
|
||||||
const libraryPanel = getLibraryPanelBehavior(panel);
|
const libraryPanel = getLibraryPanelBehavior(panel);
|
||||||
const { controls } = dashboard.useState();
|
const { controls } = dashboard.useState();
|
||||||
const styles = useStyles2(getStyles);
|
const [sidebarState, setSidebarState] = useState<SidebarState>({ size: SidebarSize.Mini, collapsed: false });
|
||||||
|
const [vizRef, { height: vizHeight }] = useMeasure<HTMLDivElement>();
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles, sidebarState);
|
||||||
|
|
||||||
const isScrollingLayout = useScrollReflowLimit();
|
const isScrollingLayout = useScrollReflowLimit();
|
||||||
|
|
||||||
const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } =
|
const gridStyles = useMemo(() => {
|
||||||
useSnappingSplitter({
|
const rows = [];
|
||||||
direction: 'column',
|
const grid = [];
|
||||||
dragPosition: 'start',
|
|
||||||
initialSize: 0.5,
|
|
||||||
collapseBelowPixels: 150,
|
|
||||||
disabled: isScrollingLayout,
|
|
||||||
});
|
|
||||||
|
|
||||||
containerProps.className = cx(containerProps.className, styles.container);
|
if (controls) {
|
||||||
|
rows.push('32px');
|
||||||
|
grid.push(['controls', 'controls']);
|
||||||
|
}
|
||||||
|
|
||||||
if (!dataPane && !isScrollingLayout) {
|
grid.push(['viz', 'viz']);
|
||||||
primaryProps.style.flexGrow = 1;
|
rows.push(`${vizHeight}px`);
|
||||||
|
|
||||||
|
if (dataPane) {
|
||||||
|
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 {
|
||||||
|
height: containerHeight,
|
||||||
|
maxHeight: containerHeight,
|
||||||
|
gridTemplateAreas: '\n' + grid.map((row) => `"${row.join(' ')}"`).join('\n'),
|
||||||
|
gridTemplateRows: rows.map((r) => r).join(' '),
|
||||||
|
};
|
||||||
|
}, [controls, dataPane, sidebarState.size, vizHeight, containerHeight]);
|
||||||
|
|
||||||
|
if (!containerHeight) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.pageContainer, controls && styles.pageContainerWithControls)}>
|
<div className={styles.pageContainer} style={gridStyles}>
|
||||||
{controls && (
|
{controls && (
|
||||||
<div className={styles.controlsWrapper}>
|
<div className={styles.controlsWrapper}>
|
||||||
<controls.Component model={controls} />
|
<controls.Component model={controls} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{dataPane && (
|
|
||||||
<div className={styles.sidebar}>
|
<div
|
||||||
<PanelDataSidebarRendered model={dataPane} />
|
className={cx(styles.viz, isScrollingLayout && styles.fixedSizeViz)}
|
||||||
</div>
|
ref={vizRef}
|
||||||
)}
|
style={{ height: containerHeight / 2, maxHeight: containerHeight - 80 }}
|
||||||
<div {...containerProps}>
|
>
|
||||||
<div {...primaryProps} className={cx(primaryProps.className, isScrollingLayout && styles.fixedSizeViz)}>
|
{tableView ? <tableView.Component model={tableView} /> : <panel.Component model={panel} />}
|
||||||
<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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VizWrapperProps {
|
function getWrapperStyles(theme: GrafanaTheme2) {
|
||||||
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) {
|
|
||||||
const scrollReflowMediaQuery = '@media ' + scrollReflowMediaCondition;
|
const scrollReflowMediaQuery = '@media ' + scrollReflowMediaCondition;
|
||||||
return {
|
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({
|
content: css({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflow: 'unset',
|
overflow: 'unset',
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
[scrollReflowMediaQuery]: {
|
[scrollReflowMediaQuery]: {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@@ -251,7 +211,6 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
borderLeft: `1px solid ${theme.colors.border.weak}`,
|
borderLeft: `1px solid ${theme.colors.border.weak}`,
|
||||||
background: theme.colors.background.primary,
|
background: theme.colors.background.primary,
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
borderTop: `1px solid ${theme.colors.border.weak}`,
|
borderTop: `1px solid ${theme.colors.border.weak}`,
|
||||||
borderTopLeftRadius: theme.shape.radius.default,
|
borderTopLeftRadius: theme.shape.radius.default,
|
||||||
}),
|
}),
|
||||||
@@ -260,24 +219,55 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: theme.spacing(2, 1),
|
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({
|
rotate180: css({
|
||||||
rotate: '180deg',
|
rotate: '180deg',
|
||||||
}),
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2, sidebarState: SidebarState) {
|
||||||
|
const scrollReflowMediaQuery = '@media ' + scrollReflowMediaCondition;
|
||||||
|
return {
|
||||||
|
pageContainer: css({
|
||||||
|
display: 'grid',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
gridTemplateColumns: `auto 1fr`,
|
||||||
|
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',
|
||||||
|
height: '100%',
|
||||||
|
minHeight: 100,
|
||||||
|
...(sidebarState.size === SidebarSize.Mini && {
|
||||||
|
paddingLeft: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
dataPane: css({
|
||||||
|
gridArea: 'data-pane',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}),
|
||||||
controlsWrapper: css({
|
controlsWrapper: css({
|
||||||
|
gridArea: 'controls',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexGrow: 0,
|
...(sidebarState.size === SidebarSize.Mini && {
|
||||||
gridArea: 'controls',
|
paddingLeft: theme.spacing(2),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
openDataPaneButton: css({
|
openDataPaneButton: css({
|
||||||
width: theme.spacing(8),
|
width: theme.spacing(8),
|
||||||
@@ -286,11 +276,6 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
rotate: '-90deg',
|
rotate: '-90deg',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
vizWrapper: css({
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
}),
|
|
||||||
fixedSizeViz: css({
|
fixedSizeViz: css({
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -250,12 +250,12 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
return {
|
return {
|
||||||
controls: css({
|
controls: css({
|
||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
padding: theme.spacing(2, 2, 1, 2),
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
|
marginBottom: theme.spacing(-1),
|
||||||
[theme.breakpoints.down('sm')]: {
|
[theme.breakpoints.down('sm')]: {
|
||||||
flexDirection: 'column-reverse',
|
flexDirection: 'column-reverse',
|
||||||
alignItems: 'stretch',
|
alignItems: 'stretch',
|
||||||
|
|||||||
Reference in New Issue
Block a user