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:
Marcus Andersson
2020-04-07 06:54:09 +02:00
committed by GitHub
parent 139753358d
commit 1f717f514a
20 changed files with 948 additions and 92 deletions

View File

@@ -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;

View File

@@ -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)),
});

View File

@@ -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;
`,
});

View File

@@ -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>
);
}
}

View File

@@ -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,