init POC commit
This commit is contained in:
@@ -1873,11 +1873,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.test.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface QueryOperationRowProps {
|
||||
collapsable?: boolean;
|
||||
disabled?: boolean;
|
||||
expanderMessages?: ExpanderMessages;
|
||||
hideHeader?: boolean;
|
||||
}
|
||||
|
||||
export type QueryOperationRowRenderProp = ((props: QueryOperationRowRenderProps) => React.ReactNode) | React.ReactNode;
|
||||
@@ -48,9 +49,10 @@ export function QueryOperationRow({
|
||||
index,
|
||||
id,
|
||||
expanderMessages,
|
||||
hideHeader = false,
|
||||
}: QueryOperationRowProps) {
|
||||
const [isContentVisible, setIsContentVisible] = useState(isOpen !== undefined ? isOpen : true);
|
||||
const styles = useStyles2(getQueryOperationRowStyles);
|
||||
const styles = useStyles2(getQueryOperationRowStyles, hideHeader);
|
||||
const onRowToggle = useCallback(() => {
|
||||
setIsContentVisible(!isContentVisible);
|
||||
}, [isContentVisible, setIsContentVisible]);
|
||||
@@ -113,22 +115,24 @@ export function QueryOperationRow({
|
||||
return (
|
||||
<>
|
||||
<div ref={provided.innerRef} className={styles.wrapper} {...provided.draggableProps}>
|
||||
<div>
|
||||
<QueryOperationRowHeader
|
||||
id={id}
|
||||
actionsElement={actionsElement}
|
||||
disabled={disabled}
|
||||
draggable
|
||||
collapsable={collapsable}
|
||||
dragHandleProps={provided.dragHandleProps}
|
||||
headerElement={headerElementRendered}
|
||||
isContentVisible={isContentVisible}
|
||||
onRowToggle={onRowToggle}
|
||||
reportDragMousePosition={reportDragMousePosition}
|
||||
title={title}
|
||||
expanderMessages={expanderMessages}
|
||||
/>
|
||||
</div>
|
||||
{!hideHeader && (
|
||||
<div>
|
||||
<QueryOperationRowHeader
|
||||
id={id}
|
||||
actionsElement={actionsElement}
|
||||
disabled={disabled}
|
||||
draggable
|
||||
collapsable={collapsable}
|
||||
dragHandleProps={provided.dragHandleProps}
|
||||
headerElement={headerElementRendered}
|
||||
isContentVisible={isContentVisible}
|
||||
onRowToggle={onRowToggle}
|
||||
reportDragMousePosition={reportDragMousePosition}
|
||||
title={title}
|
||||
expanderMessages={expanderMessages}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isContentVisible && <div className={styles.content}>{children}</div>}
|
||||
</div>
|
||||
</>
|
||||
@@ -140,32 +144,34 @@ export function QueryOperationRow({
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<QueryOperationRowHeader
|
||||
id={id}
|
||||
actionsElement={actionsElement}
|
||||
disabled={disabled}
|
||||
draggable={false}
|
||||
collapsable={collapsable}
|
||||
headerElement={headerElementRendered}
|
||||
isContentVisible={isContentVisible}
|
||||
onRowToggle={onRowToggle}
|
||||
reportDragMousePosition={reportDragMousePosition}
|
||||
title={title}
|
||||
expanderMessages={expanderMessages}
|
||||
/>
|
||||
{!hideHeader && (
|
||||
<QueryOperationRowHeader
|
||||
id={id}
|
||||
actionsElement={actionsElement}
|
||||
disabled={disabled}
|
||||
draggable={false}
|
||||
collapsable={collapsable}
|
||||
headerElement={headerElementRendered}
|
||||
isContentVisible={isContentVisible}
|
||||
onRowToggle={onRowToggle}
|
||||
reportDragMousePosition={reportDragMousePosition}
|
||||
title={title}
|
||||
expanderMessages={expanderMessages}
|
||||
/>
|
||||
)}
|
||||
{isContentVisible && <div className={styles.content}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getQueryOperationRowStyles = (theme: GrafanaTheme2) => {
|
||||
const getQueryOperationRowStyles = (theme: GrafanaTheme2, hideHeader?: boolean) => {
|
||||
return {
|
||||
wrapper: css({
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
content: css({
|
||||
marginTop: theme.spacing(0.5),
|
||||
marginLeft: theme.spacing(3),
|
||||
marginTop: hideHeader ? 0 : theme.spacing(0.5),
|
||||
marginLeft: hideHeader ? 0 : theme.spacing(3),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import {
|
||||
SceneComponentProps,
|
||||
SceneObjectBase,
|
||||
@@ -10,16 +12,22 @@ import {
|
||||
SceneObjectUrlSyncConfig,
|
||||
SceneObjectUrlValues,
|
||||
VizPanel,
|
||||
SceneDataTransformer,
|
||||
} from '@grafana/scenes';
|
||||
import { Container, ScrollContainer, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
import { Container, ScrollContainer, 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';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
import { PanelDataAlertingTab } from './PanelDataAlertingTab';
|
||||
import { PanelDataQueriesTab } from './PanelDataQueriesTab';
|
||||
import { PanelDataTransformationsTab } from './PanelDataTransformationsTab';
|
||||
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
|
||||
import { QueryTransformList, QueryTransformItem } from './QueryTransformList';
|
||||
import { SceneQueryDetailView } from './SceneQueryDetailView';
|
||||
import { TransformationsDrawer } from './TransformationsDrawer';
|
||||
import { PanelDataPaneTab, TabId } from './types';
|
||||
|
||||
export interface PanelDataPaneState extends SceneObjectState {
|
||||
@@ -63,33 +71,291 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
|
||||
return;
|
||||
}
|
||||
if (typeof values.tab === 'string') {
|
||||
this.setState({ tab: values.tab as TabId });
|
||||
const tabValue = values.tab;
|
||||
// Check if the value is a valid TabId
|
||||
if (tabValue === TabId.Queries || tabValue === TabId.Transformations || tabValue === TabId.Alert) {
|
||||
this.setState({ tab: tabValue });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
const { tab, tabs } = model.useState();
|
||||
const { tabs, panelRef } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [transformDrawerOpen, setTransformDrawerOpen] = useState(false);
|
||||
|
||||
if (!tabs || !tabs.length) {
|
||||
return;
|
||||
}
|
||||
const panel = panelRef.resolve();
|
||||
|
||||
const currentTab = tabs.find((t) => t.tabId === tab);
|
||||
// Subscribe to query runner state changes
|
||||
const queryRunner = getQueryRunnerFor(panel);
|
||||
const queryRunnerState = queryRunner?.useState();
|
||||
|
||||
// Subscribe to data transformer state changes
|
||||
const dataTransformer = panel.state.$data instanceof SceneDataTransformer ? panel.state.$data : null;
|
||||
const transformerState = dataTransformer?.useState();
|
||||
|
||||
// Build combined items list
|
||||
const items: QueryTransformItem[] = useMemo(() => {
|
||||
const result: QueryTransformItem[] = [];
|
||||
|
||||
// Add queries
|
||||
const queries = queryRunnerState?.queries || [];
|
||||
queries.forEach((query, index) => {
|
||||
if ('refId' in query) {
|
||||
result.push({
|
||||
id: `query-${query.refId}`,
|
||||
type: 'query',
|
||||
data: query,
|
||||
index,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add transformations
|
||||
const rawTransformations = transformerState?.transformations || [];
|
||||
const transformations = Array.isArray(rawTransformations)
|
||||
? rawTransformations.filter(
|
||||
(t): t is DataTransformerConfig =>
|
||||
t !== null && typeof t === 'object' && 'id' in t && typeof t.id === 'string'
|
||||
)
|
||||
: [];
|
||||
transformations.forEach((transform, index) => {
|
||||
result.push({
|
||||
id: `transform-${index}`,
|
||||
type: 'transform',
|
||||
data: transform,
|
||||
index,
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [queryRunnerState?.queries, transformerState?.transformations]);
|
||||
|
||||
// Auto-select first item if nothing is selected
|
||||
const effectiveSelectedId = useMemo(() => {
|
||||
if (selectedId === null && items.length > 0) {
|
||||
return items[0].id;
|
||||
}
|
||||
return selectedId;
|
||||
}, [selectedId, items]);
|
||||
|
||||
const selectedItem = useMemo(() => {
|
||||
return items.find((item) => item.id === effectiveSelectedId);
|
||||
}, [items, effectiveSelectedId]);
|
||||
|
||||
const handleSelect = useCallback((id: string) => {
|
||||
setSelectedId(id);
|
||||
}, []);
|
||||
|
||||
const handleAddQuery = useCallback(() => {
|
||||
const queriesTab = tabs.find((t) => t.tabId === TabId.Queries);
|
||||
if (queriesTab instanceof PanelDataQueriesTab) {
|
||||
queriesTab.addQueryClick();
|
||||
// Select the new query after a short delay
|
||||
setTimeout(() => {
|
||||
const newQueries = getQueryRunnerFor(panel)?.state.queries || [];
|
||||
if (newQueries.length > 0) {
|
||||
setSelectedId(`query-${newQueries[newQueries.length - 1].refId}`);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [tabs, panel]);
|
||||
|
||||
const handleAddTransform = useCallback(() => {
|
||||
setTransformDrawerOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleTransformationAdd = useCallback(
|
||||
(selected: SelectableValue<string>) => {
|
||||
if (!selected.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformsTab = tabs.find((t) => t.tabId === TabId.Transformations);
|
||||
if (transformsTab instanceof PanelDataTransformationsTab) {
|
||||
const transformer = transformsTab.getDataTransformer();
|
||||
const rawTransformations = transformer.state.transformations || [];
|
||||
const transformations = Array.isArray(rawTransformations)
|
||||
? rawTransformations.filter(
|
||||
(t): t is DataTransformerConfig =>
|
||||
t !== null && typeof t === 'object' && 'id' in t && typeof t.id === 'string'
|
||||
)
|
||||
: [];
|
||||
|
||||
const newTransformation: DataTransformerConfig = {
|
||||
id: selected.value,
|
||||
options: {},
|
||||
};
|
||||
|
||||
transformsTab.onChangeTransformations([...transformations, newTransformation]);
|
||||
setTransformDrawerOpen(false);
|
||||
|
||||
// Select the newly added transformation
|
||||
setTimeout(() => {
|
||||
setSelectedId(`transform-${transformations.length}`);
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
[tabs]
|
||||
);
|
||||
|
||||
const handleDuplicateQuery = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const queryToDuplicate = queries[index];
|
||||
if (queryToDuplicate) {
|
||||
// Create a copy with a new refId
|
||||
const existingRefIds = queries.map((q) => q.refId);
|
||||
let newRefId = queryToDuplicate.refId;
|
||||
let counter = 1;
|
||||
while (existingRefIds.includes(newRefId)) {
|
||||
newRefId = `${queryToDuplicate.refId}_${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
const duplicatedQuery = {
|
||||
...queryToDuplicate,
|
||||
refId: newRefId,
|
||||
};
|
||||
|
||||
const newQueries = [...queries, duplicatedQuery];
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
|
||||
// Select the new query
|
||||
setSelectedId(`query-${newRefId}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
const handleRemoveQuery = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const newQueries = queries.filter((_, i) => i !== index);
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
|
||||
// Clear selection if removing the selected query
|
||||
if (selectedId === `query-${queries[index]?.refId}`) {
|
||||
setSelectedId(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[queryRunner, selectedId]
|
||||
);
|
||||
|
||||
const handleToggleQueryVisibility = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const newQueries = queries.map((q, i) => (i === index ? { ...q, hide: !q.hide } : q));
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
},
|
||||
[queryRunner]
|
||||
);
|
||||
|
||||
const handleRemoveTransform = useCallback(
|
||||
(index: number) => {
|
||||
const transformsTab = tabs.find((t) => t.tabId === TabId.Transformations);
|
||||
if (transformsTab instanceof PanelDataTransformationsTab) {
|
||||
const transformer = transformsTab.getDataTransformer();
|
||||
const rawTransformations = transformer.state.transformations || [];
|
||||
const transformations = Array.isArray(rawTransformations)
|
||||
? rawTransformations.filter(
|
||||
(t): t is DataTransformerConfig =>
|
||||
t !== null && typeof t === 'object' && 'id' in t && typeof t.id === 'string'
|
||||
)
|
||||
: [];
|
||||
const newTransformations = transformations.filter((_, i) => i !== index);
|
||||
transformsTab.onChangeTransformations(newTransformations);
|
||||
|
||||
// Clear selection if removing the selected transformation
|
||||
if (selectedId === `transform-${index}`) {
|
||||
setSelectedId(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[tabs, selectedId]
|
||||
);
|
||||
|
||||
const renderDetailPanel = useCallback(() => {
|
||||
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' && 'refId' in selectedItem.data) {
|
||||
const query = selectedItem.data;
|
||||
return (
|
||||
<div className={styles.detailContent}>
|
||||
<ScrollContainer>
|
||||
<SceneQueryDetailView panel={panel} query={query} queryIndex={selectedItem.index} />
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const transformsTab = tabs.find((t) => t.tabId === TabId.Transformations);
|
||||
if (transformsTab instanceof PanelDataTransformationsTab && 'id' in selectedItem.data) {
|
||||
return (
|
||||
<div className={styles.detailContent}>
|
||||
<ScrollContainer>
|
||||
<Container>
|
||||
<PanelDataTransformationsTabRendered model={transformsTab} />
|
||||
</Container>
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [selectedItem, tabs, styles, panel]);
|
||||
|
||||
// Get data for transformations drawer
|
||||
const sourceData = queryRunner?.useState();
|
||||
const series = sourceData?.data?.series || [];
|
||||
|
||||
return (
|
||||
<div className={styles.dataPane} data-testid={selectors.components.PanelEditor.DataPane.content}>
|
||||
<TabsBar hideBorder className={styles.tabsBar}>
|
||||
{tabs.map((t) => t.renderTab({ active: t.tabId === tab, onChangeTab: () => model.onChangeTab(t) }))}
|
||||
</TabsBar>
|
||||
<div className={styles.tabBorder}>
|
||||
<ScrollContainer>
|
||||
<TabContent className={styles.tabContent}>
|
||||
<Container>{currentTab && <currentTab.Component model={currentTab} />}</Container>
|
||||
</TabContent>
|
||||
</ScrollContainer>
|
||||
<div className={styles.unifiedLayout}>
|
||||
<div className={styles.splitLayout}>
|
||||
<div className={styles.leftPanel}>
|
||||
<QueryTransformList
|
||||
items={items}
|
||||
selectedId={effectiveSelectedId}
|
||||
onSelect={handleSelect}
|
||||
onAddQuery={handleAddQuery}
|
||||
onAddTransform={handleAddTransform}
|
||||
onDuplicateQuery={handleDuplicateQuery}
|
||||
onRemoveQuery={handleRemoveQuery}
|
||||
onToggleQueryVisibility={handleToggleQueryVisibility}
|
||||
onRemoveTransform={handleRemoveTransform}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.rightPanel}>{renderDetailPanel()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<TransformationsDrawer
|
||||
isOpen={transformDrawerOpen}
|
||||
onClose={() => setTransformDrawerOpen(false)}
|
||||
onTransformationAdd={handleTransformationAdd}
|
||||
series={series}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -118,22 +384,67 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}),
|
||||
tabBorder: css({
|
||||
unifiedLayout: css({
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
background: theme.colors.background.primary,
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderLeft: 'none',
|
||||
borderBottom: 'none',
|
||||
borderTopRightRadius: theme.shape.radius.default,
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
tabContent: css({
|
||||
padding: theme.spacing(2),
|
||||
splitLayout: css({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}),
|
||||
tabsBar: css({
|
||||
leftPanel: css({
|
||||
width: '25%',
|
||||
minWidth: '200px',
|
||||
maxWidth: '400px',
|
||||
flexShrink: 0,
|
||||
paddingLeft: theme.spacing(2),
|
||||
borderRight: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
rightPanel: css({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
detailContent: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
background: theme.colors.background.primary,
|
||||
}),
|
||||
detailHeader: css({
|
||||
padding: theme.spacing(2),
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
background: theme.colors.background.secondary,
|
||||
'& h3': {
|
||||
margin: 0,
|
||||
marginBottom: theme.spacing(0.5),
|
||||
fontSize: theme.typography.h4.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
},
|
||||
}),
|
||||
detailHeaderSubtext: css({
|
||||
margin: 0,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
detailBody: css({
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
minHeight: 0,
|
||||
}),
|
||||
emptyState: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
color: theme.colors.text.secondary,
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
+32
-72
@@ -3,8 +3,7 @@ import { DragDropContext, DropResult, Droppable } from '@hello-pangea/dnd';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { DataTransformerConfig, GrafanaTheme2, PanelData } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { t } from '@grafana/i18n';
|
||||
import {
|
||||
SceneObjectBase,
|
||||
SceneComponentProps,
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
VizPanel,
|
||||
SceneObjectState,
|
||||
} from '@grafana/scenes';
|
||||
import { Button, ButtonGroup, ConfirmModal, Tab, useStyles2 } from '@grafana/ui';
|
||||
import { Tab, useStyles2 } from '@grafana/ui';
|
||||
import { TransformationOperationRows } from 'app/features/dashboard/components/TransformationsEditor/TransformationOperationRows';
|
||||
import { ExpressionQueryType } from 'app/features/expressions/types';
|
||||
|
||||
@@ -69,7 +68,6 @@ export class PanelDataTransformationsTab
|
||||
}
|
||||
|
||||
export function PanelDataTransformationsTabRendered({ model }: SceneComponentProps<PanelDataTransformationsTab>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const sourceData = model.getQueryRunner().useState();
|
||||
const { data, transformations: transformsWrongType } = model.getDataTransformer().useState();
|
||||
|
||||
@@ -84,7 +82,6 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
|
||||
}, [transformsWrongType]);
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
|
||||
const [confirmModalOpen, setConfirmModalOpen] = useState<boolean>(false);
|
||||
|
||||
const openDrawer = () => setDrawerOpen(true);
|
||||
const closeDrawer = () => setDrawerOpen(false);
|
||||
@@ -163,46 +160,6 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
|
||||
return (
|
||||
<>
|
||||
<TransformationsEditor data={sourceData.data} transformations={transformations} model={model} />
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="secondary"
|
||||
onClick={openDrawer}
|
||||
data-testid={selectors.components.Transforms.addTransformationButton}
|
||||
>
|
||||
<Trans i18nKey="dashboard-scene.panel-data-transformations-tab-rendered.add-another-transformation">
|
||||
Add another transformation
|
||||
</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
data-testid={selectors.components.Transforms.removeAllTransformationsButton}
|
||||
className={styles.removeAll}
|
||||
icon="times"
|
||||
variant="secondary"
|
||||
onClick={() => setConfirmModalOpen(true)}
|
||||
>
|
||||
<Trans i18nKey="dashboard-scene.panel-data-transformations-tab-rendered.delete-all-transformations">
|
||||
Delete all transformations
|
||||
</Trans>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ConfirmModal
|
||||
isOpen={confirmModalOpen}
|
||||
title={t(
|
||||
'dashboard-scene.panel-data-transformations-tab-rendered.title-delete-all-transformations',
|
||||
'Delete all transformations?'
|
||||
)}
|
||||
body={t(
|
||||
'dashboard-scene.panel-data-transformations-tab-rendered.body-delete-all-transformations',
|
||||
'By deleting all transformations, you will go back to the main selection screen.'
|
||||
)}
|
||||
confirmText={t('dashboard-scene.panel-data-transformations-tab-rendered.confirmText-delete-all', 'Delete all')}
|
||||
onConfirm={() => {
|
||||
model.onChangeTransformations([]);
|
||||
setConfirmModalOpen(false);
|
||||
}}
|
||||
onDismiss={() => setConfirmModalOpen(false)}
|
||||
/>
|
||||
{transformationsDrawer}
|
||||
</>
|
||||
);
|
||||
@@ -216,6 +173,7 @@ interface TransformationEditorProps {
|
||||
|
||||
function TransformationsEditor({ transformations, model, data }: TransformationEditorProps) {
|
||||
const transformationEditorRows = transformations.map((t, i) => ({ id: `${i} - ${t.id}`, transformation: t }));
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result || !result.destination) {
|
||||
@@ -234,37 +192,39 @@ function TransformationsEditor({ transformations, model, data }: TransformationE
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="transformations-list" direction="vertical">
|
||||
{(provided) => {
|
||||
return (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
<TransformationOperationRows
|
||||
onChange={(index, transformation) => {
|
||||
const newTransformations = transformations.slice();
|
||||
newTransformations[index] = transformation;
|
||||
model.onChangeTransformations(newTransformations);
|
||||
}}
|
||||
onRemove={(index) => {
|
||||
const newTransformations = transformations.slice();
|
||||
newTransformations.splice(index, 1);
|
||||
model.onChangeTransformations(newTransformations);
|
||||
}}
|
||||
configs={transformationEditorRows}
|
||||
data={data}
|
||||
></TransformationOperationRows>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<div className={styles.container}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="transformations-list" direction="vertical">
|
||||
{(provided) => {
|
||||
return (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
<TransformationOperationRows
|
||||
onChange={(index, transformation) => {
|
||||
const newTransformations = transformations.slice();
|
||||
newTransformations[index] = transformation;
|
||||
model.onChangeTransformations(newTransformations);
|
||||
}}
|
||||
onRemove={(index) => {
|
||||
const newTransformations = transformations.slice();
|
||||
newTransformations.splice(index, 1);
|
||||
model.onChangeTransformations(newTransformations);
|
||||
}}
|
||||
configs={transformationEditorRows}
|
||||
data={data}
|
||||
></TransformationOperationRows>
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
removeAll: css({
|
||||
marginLeft: theme.spacing(2),
|
||||
container: css({
|
||||
padding: theme.spacing(2),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { DataTransformerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { SceneDataQuery } from '@grafana/scenes';
|
||||
import { Icon, IconButton, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
interface QueryTransformCardProps {
|
||||
item: SceneDataQuery | DataTransformerConfig;
|
||||
type: 'query' | 'transform';
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
onDuplicate?: () => void;
|
||||
onRemove?: () => void;
|
||||
onToggleVisibility?: () => void;
|
||||
}
|
||||
|
||||
export const QueryTransformCard = memo(
|
||||
({ item, type, index, isSelected, onClick, onDuplicate, onRemove, onToggleVisibility }: QueryTransformCardProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const getName = (): string => {
|
||||
if (type === 'query' && 'refId' in item) {
|
||||
return item.refId || `Query ${index + 1}`;
|
||||
} else if ('id' in item) {
|
||||
return item.id.replace(/-/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase());
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getDatasourceName = (): string => {
|
||||
if (type === 'query' && 'datasource' in item && item.datasource) {
|
||||
try {
|
||||
const dsSettings = getDataSourceSrv().getInstanceSettings(item.datasource);
|
||||
return dsSettings?.name || 'Unknown datasource';
|
||||
} catch {
|
||||
return 'Unknown datasource';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const isHidden = type === 'query' && 'hide' in item && item.hide;
|
||||
const icon = type === 'query' ? 'database' : 'process';
|
||||
const typeLabel = type === 'query' ? 'Query' : 'Transformation';
|
||||
const name = getName();
|
||||
const datasourceName = getDatasourceName();
|
||||
|
||||
const handleAction = (e: React.MouseEvent, action: () => void) => {
|
||||
e.stopPropagation();
|
||||
action();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`${styles.card} ${isSelected ? styles.cardSelected : ''}`}
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
data-testid={`${type}-card-${index}`}
|
||||
>
|
||||
{/* Header with type and action icons */}
|
||||
<div className={type === 'query' ? styles.headerQuery : styles.headerTransform}>
|
||||
<div className={styles.headerLeft}>
|
||||
<Icon name={icon} className={styles.headerIcon} />
|
||||
<span className={styles.typeLabel}>{typeLabel}</span>
|
||||
</div>
|
||||
<div className={`${styles.actions} ${styles.actionsClass}`}>
|
||||
<Stack gap={0.5}>
|
||||
{type === 'query' && onToggleVisibility && (
|
||||
<IconButton
|
||||
name={isHidden ? 'eye-slash' : 'eye'}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
tooltip={
|
||||
isHidden
|
||||
? t('dashboard-scene.query-transform-card.show-response', 'Show response')
|
||||
: t('dashboard-scene.query-transform-card.hide-response', 'Hide response')
|
||||
}
|
||||
onClick={(e) => handleAction(e, onToggleVisibility)}
|
||||
className={styles.actionButton}
|
||||
/>
|
||||
)}
|
||||
{type === 'query' && onDuplicate && (
|
||||
<IconButton
|
||||
name="copy"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
tooltip={t('dashboard-scene.query-transform-card.duplicate', 'Duplicate query')}
|
||||
onClick={(e) => handleAction(e, onDuplicate)}
|
||||
className={styles.actionButton}
|
||||
/>
|
||||
)}
|
||||
{onRemove && (
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
tooltip={
|
||||
type === 'query'
|
||||
? t('dashboard-scene.query-transform-card.remove-query', 'Remove query')
|
||||
: t('dashboard-scene.query-transform-card.remove-transform', 'Remove transformation')
|
||||
}
|
||||
onClick={(e) => handleAction(e, onRemove)}
|
||||
className={styles.actionButton}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content: Name and datasource */}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
{datasourceName && <div className={styles.datasource}>{datasourceName}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QueryTransformCard.displayName = 'QueryTransformCard';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const actionsClass = 'actions-container';
|
||||
|
||||
return {
|
||||
card: css({
|
||||
cursor: 'pointer',
|
||||
marginBottom: theme.spacing(1),
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
overflow: 'hidden',
|
||||
background: theme.colors.background.primary,
|
||||
[`&:hover .${actionsClass}`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
'&:hover': {
|
||||
borderColor: theme.colors.border.strong,
|
||||
},
|
||||
}),
|
||||
cardSelected: css({
|
||||
borderColor: theme.colors.primary.border,
|
||||
boxShadow: `0 0 0 1px ${theme.colors.primary.border}`,
|
||||
}),
|
||||
headerQuery: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(1, 1.5),
|
||||
background: theme.colors.primary.transparent,
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
headerTransform: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(1, 1.5),
|
||||
background: theme.isDark
|
||||
? `${theme.visualization.getColorByName('orange')}20`
|
||||
: `${theme.visualization.getColorByName('orange')}15`,
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
}),
|
||||
headerLeft: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
}),
|
||||
headerIcon: css({
|
||||
color: theme.colors.text.secondary,
|
||||
flexShrink: 0,
|
||||
}),
|
||||
typeLabel: css({
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
color: theme.colors.text.secondary,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
}),
|
||||
actions: css({
|
||||
opacity: 0,
|
||||
flexShrink: 0,
|
||||
}),
|
||||
actionsClass,
|
||||
actionButton: css({
|
||||
'&:hover': {
|
||||
background: theme.colors.action.hover,
|
||||
},
|
||||
}),
|
||||
content: css({
|
||||
padding: theme.spacing(1.5),
|
||||
}),
|
||||
name: css({
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
color: theme.colors.text.primary,
|
||||
marginBottom: theme.spacing(0.5),
|
||||
}),
|
||||
datasource: css({
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { DataTransformerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { SceneDataQuery } from '@grafana/scenes';
|
||||
import { Button, ScrollContainer, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { QueryTransformCard } from './QueryTransformCard';
|
||||
|
||||
export interface QueryTransformItem {
|
||||
id: string;
|
||||
type: 'query' | 'transform';
|
||||
data: SceneDataQuery | DataTransformerConfig;
|
||||
index: number;
|
||||
}
|
||||
|
||||
interface QueryTransformListProps {
|
||||
items: QueryTransformItem[];
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
onAddQuery: () => void;
|
||||
onAddTransform: () => void;
|
||||
onDuplicateQuery?: (index: number) => void;
|
||||
onRemoveQuery?: (index: number) => void;
|
||||
onToggleQueryVisibility?: (index: number) => void;
|
||||
onRemoveTransform?: (index: number) => void;
|
||||
}
|
||||
|
||||
export const QueryTransformList = memo(
|
||||
({
|
||||
items,
|
||||
selectedId,
|
||||
onSelect,
|
||||
onAddQuery,
|
||||
onAddTransform,
|
||||
onDuplicateQuery,
|
||||
onRemoveQuery,
|
||||
onToggleQueryVisibility,
|
||||
onRemoveTransform,
|
||||
}: QueryTransformListProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const queries = items.filter((item) => item.type === 'query');
|
||||
const transforms = items.filter((item) => item.type === 'transform');
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.scrollContainer}>
|
||||
<ScrollContainer>
|
||||
<div className={styles.content}>
|
||||
{queries.length > 0 && (
|
||||
<div className={styles.section}>
|
||||
{queries.map((item) => (
|
||||
<QueryTransformCard
|
||||
key={item.id}
|
||||
item={item.data}
|
||||
type="query"
|
||||
index={item.index}
|
||||
isSelected={selectedId === item.id}
|
||||
onClick={() => onSelect(item.id)}
|
||||
onDuplicate={onDuplicateQuery ? () => onDuplicateQuery(item.index) : undefined}
|
||||
onRemove={onRemoveQuery ? () => onRemoveQuery(item.index) : undefined}
|
||||
onToggleVisibility={
|
||||
onToggleQueryVisibility ? () => onToggleQueryVisibility(item.index) : undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transforms.length > 0 && (
|
||||
<div className={styles.section}>
|
||||
{transforms.map((item) => (
|
||||
<QueryTransformCard
|
||||
key={item.id}
|
||||
item={item.data}
|
||||
type="transform"
|
||||
index={item.index}
|
||||
isSelected={selectedId === item.id}
|
||||
onClick={() => onSelect(item.id)}
|
||||
onRemove={onRemoveTransform ? () => onRemoveTransform(item.index) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Stack direction="column" gap={1}>
|
||||
<Button icon="plus" variant="secondary" onClick={onAddQuery} fullWidth>
|
||||
<Trans i18nKey="dashboard-scene.query-transform-list.add-query">Add query</Trans>
|
||||
</Button>
|
||||
<Button icon="plus" variant="secondary" onClick={onAddTransform} fullWidth>
|
||||
<Trans i18nKey="dashboard-scene.query-transform-list.add-transformation">Add transformation</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QueryTransformList.displayName = 'QueryTransformList';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
background: theme.colors.background.primary,
|
||||
borderRight: `1px solid ${theme.colors.border.weak}`,
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
scrollContainer: css({
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflow: 'auto',
|
||||
}),
|
||||
content: css({
|
||||
padding: theme.spacing(2),
|
||||
}),
|
||||
section: css({
|
||||
'&:not(:first-child)': {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
actions: css({
|
||||
padding: theme.spacing(2),
|
||||
borderTop: `1px solid ${theme.colors.border.weak}`,
|
||||
background: theme.colors.background.canvas,
|
||||
flexShrink: 0,
|
||||
}),
|
||||
};
|
||||
};
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
CoreApp,
|
||||
DataQuery,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourcePluginContextProvider,
|
||||
GrafanaTheme2,
|
||||
PanelData,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { SceneDataQuery, VizPanel } from '@grafana/scenes';
|
||||
import { ErrorBoundaryAlert, useStyles2 } from '@grafana/ui';
|
||||
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
import { QueryErrorAlert } from 'app/features/query/components/QueryErrorAlert';
|
||||
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
interface SceneQueryDetailViewProps {
|
||||
panel: VizPanel;
|
||||
query: SceneDataQuery;
|
||||
queryIndex: number;
|
||||
}
|
||||
|
||||
export function SceneQueryDetailView({ panel, query, queryIndex }: SceneQueryDetailViewProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [datasource, setDatasource] = useState<DataSourceApi | null>(null);
|
||||
const [dsSettings, setDsSettings] = useState<DataSourceInstanceSettings | null>(null);
|
||||
const [data, setData] = useState<PanelData | undefined>();
|
||||
|
||||
const queryRunner = getQueryRunnerFor(panel);
|
||||
const queryRunnerState = queryRunner?.useState();
|
||||
const timeRange: TimeRange | undefined = queryRunner?.state.$timeRange?.state.value;
|
||||
|
||||
// Load datasource
|
||||
useEffect(() => {
|
||||
const loadDatasource = async () => {
|
||||
try {
|
||||
const ds = await getDataSourceSrv().get(query.datasource);
|
||||
const settings = getDataSourceSrv().getInstanceSettings(query.datasource);
|
||||
setDatasource(ds);
|
||||
setDsSettings(settings || null);
|
||||
} catch (error) {
|
||||
console.error('Failed to load datasource:', error);
|
||||
// Fallback to default datasource
|
||||
const defaultDs = await getDataSourceSrv().get();
|
||||
const defaultSettings = getDataSourceSrv().getInstanceSettings(null);
|
||||
setDatasource(defaultDs);
|
||||
setDsSettings(defaultSettings || null);
|
||||
}
|
||||
};
|
||||
|
||||
loadDatasource();
|
||||
}, [query.datasource]);
|
||||
|
||||
// Subscribe to panel data
|
||||
useEffect(() => {
|
||||
if (!queryRunnerState?.data) {
|
||||
return;
|
||||
}
|
||||
// Filter data for this specific query
|
||||
const panelData = queryRunnerState.data;
|
||||
const filteredSeries = panelData.series.filter((s) => s.refId === query.refId);
|
||||
const filteredData = {
|
||||
...panelData,
|
||||
series: filteredSeries,
|
||||
error: panelData.errors?.find((e) => e.refId === query.refId),
|
||||
};
|
||||
setData(filteredData);
|
||||
}, [queryRunnerState?.data, query.refId]);
|
||||
|
||||
const handleDataSourceChange = useCallback(
|
||||
async (newDsSettings: DataSourceInstanceSettings) => {
|
||||
try {
|
||||
const newDs = await getDataSourceSrv().get(newDsSettings.uid);
|
||||
setDatasource(newDs);
|
||||
setDsSettings(newDsSettings);
|
||||
|
||||
// Update the query with the new datasource
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const newQueries = queries.map((q, idx) => {
|
||||
if (idx === queryIndex) {
|
||||
// Get default query for new datasource
|
||||
const defaultQuery = newDs.getDefaultQuery?.(CoreApp.PanelEditor) || {};
|
||||
return {
|
||||
...defaultQuery,
|
||||
...q,
|
||||
datasource: { uid: newDsSettings.uid, type: newDsSettings.type },
|
||||
refId: q.refId,
|
||||
};
|
||||
}
|
||||
return q;
|
||||
});
|
||||
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to change datasource:', error);
|
||||
}
|
||||
},
|
||||
[queryRunner, queryIndex]
|
||||
);
|
||||
|
||||
const handleQueryChange = useCallback(
|
||||
(updatedQuery: DataQuery) => {
|
||||
if (queryRunner) {
|
||||
const queries = queryRunner.state.queries || [];
|
||||
const newQueries = queries.map((q, idx) => (idx === queryIndex ? updatedQuery : q));
|
||||
queryRunner.setState({ queries: newQueries });
|
||||
}
|
||||
},
|
||||
[queryRunner, queryIndex]
|
||||
);
|
||||
|
||||
const handleRunQuery = useCallback(() => {
|
||||
if (queryRunner) {
|
||||
queryRunner.runQueries();
|
||||
}
|
||||
}, [queryRunner]);
|
||||
|
||||
const renderQueryEditor = () => {
|
||||
if (!datasource || !dsSettings) {
|
||||
return (
|
||||
<div className={styles.noEditor}>
|
||||
<Trans i18nKey="dashboard-scene.scene-query-detail-view.loading">Loading data source...</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const QueryEditor = datasource.components?.QueryEditor;
|
||||
if (!QueryEditor) {
|
||||
return (
|
||||
<div className={styles.noEditor}>
|
||||
<Trans i18nKey="dashboard-scene.scene-query-detail-view.no-editor">
|
||||
This data source does not have a query editor
|
||||
</Trans>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DataSourcePluginContextProvider instanceSettings={dsSettings}>
|
||||
<ErrorBoundaryAlert boundaryName="query-editor">
|
||||
<QueryEditor
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={handleQueryChange}
|
||||
onRunQuery={handleRunQuery}
|
||||
data={data}
|
||||
range={timeRange}
|
||||
app={CoreApp.PanelEditor}
|
||||
/>
|
||||
</ErrorBoundaryAlert>
|
||||
</DataSourcePluginContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const error = data?.error || data?.errors?.find((e) => e.refId === query.refId);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.datasourceSection}>
|
||||
<DataSourcePicker
|
||||
current={dsSettings?.uid || query.datasource}
|
||||
onChange={handleDataSourceChange}
|
||||
placeholder={t('dashboard-scene.scene-query-detail-view.select-datasource', 'Select data source')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<QueryOperationRow
|
||||
id={`query-${query.refId}`}
|
||||
index={queryIndex}
|
||||
draggable={false}
|
||||
collapsable={false}
|
||||
isOpen={true}
|
||||
hideHeader={true}
|
||||
>
|
||||
<div className={styles.queryContent}>
|
||||
{renderQueryEditor()}
|
||||
{error && <QueryErrorAlert error={error} />}
|
||||
</div>
|
||||
</QueryOperationRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
padding: theme.spacing(2),
|
||||
}),
|
||||
datasourceSection: css({
|
||||
paddingBottom: theme.spacing(2),
|
||||
}),
|
||||
queryContent: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
noEditor: css({
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
color: theme.colors.text.secondary,
|
||||
}),
|
||||
};
|
||||
};
|
||||
+1
@@ -227,6 +227,7 @@ export const TransformationOperationRow = ({
|
||||
draggable
|
||||
actions={renderActions}
|
||||
disabled={disabled}
|
||||
hideHeader={true}
|
||||
expanderMessages={{
|
||||
close: 'Collapse transformation row',
|
||||
open: 'Expand transformation row',
|
||||
|
||||
@@ -6180,6 +6180,9 @@
|
||||
"text-loading-rules": "Loading rules...",
|
||||
"title-dashboard-not-saved": "Dashboard not saved"
|
||||
},
|
||||
"panel-data-pane": {
|
||||
"empty-state": "Select a query or transformation to edit"
|
||||
},
|
||||
"panel-data-queries-tab": {
|
||||
"tab-label": "Queries"
|
||||
},
|
||||
@@ -6190,13 +6193,6 @@
|
||||
"panel-data-transformations-tab": {
|
||||
"tab-label": "Transformations"
|
||||
},
|
||||
"panel-data-transformations-tab-rendered": {
|
||||
"add-another-transformation": "Add another transformation",
|
||||
"body-delete-all-transformations": "By deleting all transformations, you will go back to the main selection screen.",
|
||||
"confirmText-delete-all": "Delete all",
|
||||
"delete-all-transformations": "Delete all transformations",
|
||||
"title-delete-all-transformations": "Delete all transformations?"
|
||||
},
|
||||
"panel-edit-controls": {
|
||||
"table-view-aria-label-toggletableview": "Toggle table view",
|
||||
"table-view-label-table-view": "Table view"
|
||||
@@ -6242,6 +6238,17 @@
|
||||
"query-editor": {
|
||||
"query": "Query"
|
||||
},
|
||||
"query-transform-card": {
|
||||
"duplicate": "Duplicate query",
|
||||
"hide-response": "Hide response",
|
||||
"remove-query": "Remove query",
|
||||
"remove-transform": "Remove transformation",
|
||||
"show-response": "Show response"
|
||||
},
|
||||
"query-transform-list": {
|
||||
"add-query": "Add query",
|
||||
"add-transformation": "Add transformation"
|
||||
},
|
||||
"query-variable-editor-form": {
|
||||
"description-examples": "Named capture groups can be used to separate the display text and value (<1>see examples</1> ).",
|
||||
"description-optional": "Optional, if you want to extract part of a series name or metric node segment.",
|
||||
@@ -6352,6 +6359,11 @@
|
||||
"title-same-as-folder": "Dashboard name cannot be the same as the folder name",
|
||||
"title-validation-failed": "Dashboard title validation failed."
|
||||
},
|
||||
"scene-query-detail-view": {
|
||||
"loading": "Loading data source...",
|
||||
"no-editor": "This data source does not have a query editor",
|
||||
"select-datasource": "Select data source"
|
||||
},
|
||||
"scenes-new-rule-from-panel-button": {
|
||||
"body-no-alerting-capable-query-found": "Cannot create alerts from this panel because no query to an alerting capable datasource is found.",
|
||||
"new-alert-rule": "New alert rule",
|
||||
|
||||
Reference in New Issue
Block a user