mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TabelPanel: add support for organizing fields/columns. (#23135)
* Added draft on transformers to sort and hide fields. * added structure for the UI. * draft on sorting/filtering UI. * simplified the datastructure a bit. * added draft on drag and drop support. * added some super simple styling. Nothing final still waiting for a proper design on this. * updated lockfile after merge. * changed so we use the new path for button. * added one more test. * Ignore feature toggle * Moved editor to app * Added top description * Minor update * Did some renaming and simplified the code a bit. * fixed so we dont use capital naming on the transformer. * changed to an vertical drag and drop design. * added support to rename fields. Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { config } from 'app/core/config';
|
||||
import { css } from 'emotion';
|
||||
import { TabsBar, Tab, stylesFactory, TabContent, TransformationsEditor } from '@grafana/ui';
|
||||
import { TabsBar, Tab, stylesFactory, TabContent } from '@grafana/ui';
|
||||
import { DataTransformerConfig, LoadingState, PanelData } from '@grafana/data';
|
||||
import { PanelEditorTab, PanelEditorTabId } from './types';
|
||||
import { DashboardModel } from '../../state';
|
||||
@@ -9,6 +9,7 @@ import { QueriesTab } from '../../panel_editor/QueriesTab';
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
import { AlertTab } from 'app/features/alerting/AlertTab';
|
||||
import { VisualizationTab } from './VisualizationTab';
|
||||
import { TransformationsEditor } from '../TransformationsEditor/TransformationsEditor';
|
||||
|
||||
interface PanelEditorTabsProps {
|
||||
panel: PanelModel;
|
||||
|
||||
@@ -17,7 +17,7 @@ export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardMod
|
||||
const panel = dashboard.initPanelEditor(sourcePanel);
|
||||
|
||||
const queryRunner = panel.getQueryRunner();
|
||||
const querySubscription = queryRunner.getData().subscribe({
|
||||
const querySubscription = queryRunner.getData(false).subscribe({
|
||||
next: (data: PanelData) => dispatch(setEditorPanelData(data)),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { JSONFormatter, ThemeContext } from '@grafana/ui';
|
||||
import { GrafanaTheme, DataFrame } from '@grafana/data';
|
||||
|
||||
interface TransformationRowProps {
|
||||
name: string;
|
||||
description: string;
|
||||
editor?: JSX.Element;
|
||||
onRemove: () => void;
|
||||
input: DataFrame[];
|
||||
}
|
||||
|
||||
export const TransformationRow = ({ onRemove, editor, name, input }: TransformationRowProps) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const [viewDebug, setViewDebug] = useState(false);
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
margin-bottom: 10px;
|
||||
`}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.iconRow}>
|
||||
<div onClick={() => setViewDebug(!viewDebug)} className={styles.icon}>
|
||||
<i className="fa fa-fw fa-bug" />
|
||||
</div>
|
||||
<div onClick={onRemove} className={styles.icon}>
|
||||
<i className="fa fa-fw fa-trash" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.editor}>
|
||||
{editor}
|
||||
{viewDebug && (
|
||||
<div>
|
||||
<JSONFormatter json={input} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
title: css`
|
||||
display: flex;
|
||||
padding: 4px 8px 4px 8px;
|
||||
position: relative;
|
||||
height: 35px;
|
||||
background: ${theme.colors.textFaint};
|
||||
border-radius: 4px 4px 0 0;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`,
|
||||
name: css`
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
color: ${theme.colors.blue};
|
||||
`,
|
||||
iconRow: css`
|
||||
display: flex;
|
||||
`,
|
||||
icon: css`
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
color: ${theme.colors.textWeak};
|
||||
margin-left: ${theme.spacing.sm};
|
||||
&:hover {
|
||||
color: ${theme.colors.text};
|
||||
}
|
||||
`,
|
||||
editor: css`
|
||||
border: 2px dashed ${theme.colors.textFaint};
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding: 8px;
|
||||
`,
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { css } from 'emotion';
|
||||
import React from 'react';
|
||||
import { transformersUIRegistry } from '@grafana/ui/src/components/TransformersUI/transformers';
|
||||
import { DataTransformerID, DataTransformerConfig, DataFrame, transformDataFrame } from '@grafana/data';
|
||||
import { Button, Select } from '@grafana/ui';
|
||||
import { TransformationRow } from './TransformationRow';
|
||||
|
||||
interface Props {
|
||||
onChange: (transformations: DataTransformerConfig[]) => void;
|
||||
transformations: DataTransformerConfig[];
|
||||
dataFrames: DataFrame[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
updateCounter: number;
|
||||
}
|
||||
|
||||
export class TransformationsEditor extends React.PureComponent<Props, State> {
|
||||
state = { updateCounter: 0 };
|
||||
|
||||
onTransformationAdd = () => {
|
||||
const { transformations, onChange } = this.props;
|
||||
onChange([
|
||||
...transformations,
|
||||
{
|
||||
id: DataTransformerID.noop,
|
||||
options: {},
|
||||
},
|
||||
]);
|
||||
this.setState({ updateCounter: this.state.updateCounter + 1 });
|
||||
};
|
||||
|
||||
onTransformationChange = (idx: number, config: DataTransformerConfig) => {
|
||||
const { transformations, onChange } = this.props;
|
||||
transformations[idx] = config;
|
||||
onChange(transformations);
|
||||
this.setState({ updateCounter: this.state.updateCounter + 1 });
|
||||
};
|
||||
|
||||
onTransformationRemove = (idx: number) => {
|
||||
const { transformations, onChange } = this.props;
|
||||
transformations.splice(idx, 1);
|
||||
onChange(transformations);
|
||||
this.setState({ updateCounter: this.state.updateCounter + 1 });
|
||||
};
|
||||
|
||||
renderTransformationEditors = () => {
|
||||
const { transformations, dataFrames } = this.props;
|
||||
const hasTransformations = transformations.length > 0;
|
||||
const preTransformData = dataFrames;
|
||||
|
||||
if (!hasTransformations) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const availableTransformers = transformersUIRegistry.list().map(t => {
|
||||
return {
|
||||
value: t.transformer.id,
|
||||
label: t.transformer.name,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{transformations.map((t, i) => {
|
||||
let editor, input;
|
||||
if (t.id === DataTransformerID.noop) {
|
||||
return (
|
||||
<Select
|
||||
className={css`
|
||||
margin-bottom: 10px;
|
||||
`}
|
||||
key={`${t.id}-${i}`}
|
||||
options={availableTransformers}
|
||||
placeholder="Select transformation"
|
||||
onChange={v => {
|
||||
this.onTransformationChange(i, {
|
||||
id: v.value as string,
|
||||
options: {},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const transformationUI = transformersUIRegistry.getIfExists(t.id);
|
||||
input = transformDataFrame(transformations.slice(0, i), preTransformData);
|
||||
|
||||
if (transformationUI) {
|
||||
editor = React.createElement(transformationUI.component, {
|
||||
options: { ...transformationUI.transformer.defaultOptions, ...t.options },
|
||||
input,
|
||||
onChange: (options: any) => {
|
||||
this.onTransformationChange(i, {
|
||||
id: t.id,
|
||||
options,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<TransformationRow
|
||||
key={`${t.id}-${i}`}
|
||||
input={input || []}
|
||||
onRemove={() => this.onTransformationRemove(i)}
|
||||
editor={editor}
|
||||
name={transformationUI ? transformationUI.name : ''}
|
||||
description={transformationUI ? transformationUI.description : ''}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="panel-editor__content">
|
||||
<p className="muted text-center" style={{ padding: '8px' }}>
|
||||
Transformations allow you to combine, re-order, hide and rename specific parts the the data set before being
|
||||
visualized.
|
||||
</p>
|
||||
{this.renderTransformationEditors()}
|
||||
<Button variant="secondary" icon="fa fa-plus" onClick={this.onTransformationAdd}>
|
||||
Add transformation
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { ReplaySubject, Unsubscribable, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
// Services & Utils
|
||||
import { config } from 'app/core/config';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
@@ -70,37 +69,38 @@ export class PanelQueryRunner {
|
||||
return this.subject.pipe(
|
||||
map((data: PanelData) => {
|
||||
let processedData = data;
|
||||
// apply transformations
|
||||
if (transform && this.hasTransformations()) {
|
||||
processedData = {
|
||||
...processedData,
|
||||
series: transformDataFrame(this.dataConfigSource.getTransformations(), data.series),
|
||||
};
|
||||
|
||||
// Apply transformations
|
||||
|
||||
if (transform) {
|
||||
const transformations = this.dataConfigSource.getTransformations();
|
||||
|
||||
if (transformations && transformations.length > 0) {
|
||||
processedData = {
|
||||
...processedData,
|
||||
series: transformDataFrame(this.dataConfigSource.getTransformations(), data.series),
|
||||
};
|
||||
}
|
||||
}
|
||||
// apply overrides
|
||||
if (this.hasFieldOverrideOptions()) {
|
||||
|
||||
// Apply field defaults & overrides
|
||||
const fieldConfig = this.dataConfigSource.getFieldOverrideOptions();
|
||||
|
||||
if (fieldConfig) {
|
||||
processedData = {
|
||||
...processedData,
|
||||
series: applyFieldOverrides({
|
||||
data: processedData.series,
|
||||
...this.dataConfigSource.getFieldOverrideOptions(),
|
||||
...fieldConfig,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return processedData;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
hasTransformations = () => {
|
||||
const transformations = this.dataConfigSource.getTransformations();
|
||||
return config.featureToggles.transformations && transformations && transformations.length > 0;
|
||||
};
|
||||
|
||||
hasFieldOverrideOptions = () => {
|
||||
return this.dataConfigSource.getFieldOverrideOptions();
|
||||
};
|
||||
|
||||
async run(options: QueryRunnerOptions) {
|
||||
const {
|
||||
queries,
|
||||
|
||||
Reference in New Issue
Block a user