mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
@@ -2178,16 +2178,14 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard/components/TransformationsEditor/TransformationEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"],
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "2"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"],
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
|
||||
@@ -124,6 +124,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `prometheusIncrementalQueryInstrumentation` | Adds RudderStack events to incremental queries |
|
||||
| `logsExploreTableVisualisation` | A table visualisation for logs in Explore |
|
||||
| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers |
|
||||
| `transformationsRedesign` | Enables the transformations redesign |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
||||
@@ -111,4 +111,5 @@ export interface FeatureToggles {
|
||||
prometheusIncrementalQueryInstrumentation?: boolean;
|
||||
logsExploreTableVisualisation?: boolean;
|
||||
awsDatasourcesTempCredentials?: boolean;
|
||||
transformationsRedesign?: boolean;
|
||||
}
|
||||
|
||||
@@ -208,13 +208,13 @@ export const Components = {
|
||||
alertV2: (severity: string) => `data-testid Alert ${severity}`,
|
||||
},
|
||||
TransformTab: {
|
||||
content: 'Transform editor tab content',
|
||||
newTransform: (name: string) => `New transform ${name}`,
|
||||
transformationEditor: (name: string) => `Transformation editor ${name}`,
|
||||
transformationEditorDebugger: (name: string) => `Transformation editor debugger ${name}`,
|
||||
content: 'data-testid Transform editor tab content',
|
||||
newTransform: (name: string) => `data-testid New transform ${name}`,
|
||||
transformationEditor: (name: string) => `data-testid Transformation editor ${name}`,
|
||||
transformationEditorDebugger: (name: string) => `data-testid Transformation editor debugger ${name}`,
|
||||
},
|
||||
Transforms: {
|
||||
card: (name: string) => `New transform ${name}`,
|
||||
card: (name: string) => `data-testid New transform ${name}`,
|
||||
Reduce: {
|
||||
modeLabel: 'Transform mode label',
|
||||
calculationsLabel: 'Transform calculations label',
|
||||
@@ -241,6 +241,7 @@ export const Components = {
|
||||
},
|
||||
},
|
||||
searchInput: 'search transformations',
|
||||
addTransformationButton: 'data-testid add transformation button',
|
||||
},
|
||||
NavBar: {
|
||||
Configuration: {
|
||||
|
||||
@@ -41,6 +41,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
height: 32px;
|
||||
position: relative;
|
||||
border: 1px solid ${theme.colors.background.secondary};
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.action.hover};
|
||||
|
||||
@@ -634,5 +634,12 @@ var (
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: awsDatasourcesSquad,
|
||||
},
|
||||
{
|
||||
Name: "transformationsRedesign",
|
||||
Description: "Enables the transformations redesign",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaObservabilityMetricsSquad,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -92,3 +92,4 @@ vizAndWidgetSplit,experimental,@grafana/dashboards-squad,false,false,false,true
|
||||
prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-metrics,false,false,false,true
|
||||
logsExploreTableVisualisation,experimental,@grafana/observability-logs,false,false,false,true
|
||||
awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false,false
|
||||
transformationsRedesign,experimental,@grafana/observability-metrics,false,false,false,true
|
||||
|
||||
|
@@ -378,4 +378,8 @@ const (
|
||||
// FlagAwsDatasourcesTempCredentials
|
||||
// Support temporary security credentials in AWS plugins for Grafana Cloud customers
|
||||
FlagAwsDatasourcesTempCredentials = "awsDatasourcesTempCredentials"
|
||||
|
||||
// FlagTransformationsRedesign
|
||||
// Enables the transformations redesign
|
||||
FlagTransformationsRedesign = "transformationsRedesign"
|
||||
)
|
||||
|
||||
@@ -28,8 +28,13 @@ export const PanelEditorTabs = React.memo(({ panel, dashboard, tabs, onChangeTab
|
||||
|
||||
const instrumentedOnChangeTab = useCallback(
|
||||
(tab: PanelEditorTab) => {
|
||||
let eventName = 'panel_editor_tabs_changed';
|
||||
if (config.featureToggles.transformationsRedesign) {
|
||||
eventName = 'transformations_redesign_' + eventName;
|
||||
}
|
||||
|
||||
if (!tab.active) {
|
||||
reportInteraction('panel_editor_tabs_changed', { tab_id: tab.id });
|
||||
reportInteraction(eventName, { tab_id: tab.id });
|
||||
}
|
||||
|
||||
onChangeTab(tab);
|
||||
|
||||
@@ -74,12 +74,12 @@ export const TransformationEditor = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.editor} aria-label={selectors.components.TransformTab.transformationEditor(uiConfig.name)}>
|
||||
<div className={styles.editor} data-testid={selectors.components.TransformTab.transformationEditor(uiConfig.name)}>
|
||||
{editor}
|
||||
{debugMode && (
|
||||
<div
|
||||
className={styles.debugWrapper}
|
||||
aria-label={selectors.components.TransformTab.transformationEditorDebugger(uiConfig.name)}
|
||||
data-testid={selectors.components.TransformTab.transformationEditorDebugger(uiConfig.name)}
|
||||
>
|
||||
<div className={styles.debug}>
|
||||
<div className={styles.debugTitle}>Transformation input data</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useToggle } from 'react-use';
|
||||
|
||||
import { DataFrame, DataTransformerConfig, TransformerRegistryItem, FrameMatcherID } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { HorizontalGroup } from '@grafana/ui';
|
||||
import { ConfirmModal, HorizontalGroup } from '@grafana/ui';
|
||||
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
||||
import {
|
||||
QueryOperationAction,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
QueryOperationRow,
|
||||
QueryOperationRowRenderProps,
|
||||
} from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||
import config from 'app/core/config';
|
||||
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
|
||||
|
||||
import { TransformationEditor } from './TransformationEditor';
|
||||
@@ -38,6 +39,7 @@ export const TransformationOperationRow = ({
|
||||
uiConfig,
|
||||
onChange,
|
||||
}: TransformationOperationRowProps) => {
|
||||
const [showDeleteModal, setShowDeleteModal] = useToggle(false);
|
||||
const [showDebug, toggleDebug] = useToggle(false);
|
||||
const [showHelp, toggleHelp] = useToggle(false);
|
||||
const disabled = !!configs[index].transformation.disabled;
|
||||
@@ -73,7 +75,12 @@ export const TransformationOperationRow = ({
|
||||
const instrumentToggleCallback = useCallback(
|
||||
(callback: (e: React.MouseEvent) => void, toggleId: string, active: boolean | undefined) =>
|
||||
(e: React.MouseEvent) => {
|
||||
reportInteraction('panel_editor_tabs_transformations_toggle', {
|
||||
let eventName = 'panel_editor_tabs_transformations_toggle';
|
||||
if (config.featureToggles.transformationsRedesign) {
|
||||
eventName = 'transformations_redesign_' + eventName;
|
||||
}
|
||||
|
||||
reportInteraction(eventName, {
|
||||
action: active ? 'off' : 'on',
|
||||
toggleId,
|
||||
transformationId: configs[index].transformation.id,
|
||||
@@ -115,7 +122,25 @@ export const TransformationOperationRow = ({
|
||||
onClick={instrumentToggleCallback(() => onDisableToggle(index), 'disabled', disabled)}
|
||||
active={disabled}
|
||||
/>
|
||||
<QueryOperationAction title="Remove" icon="trash-alt" onClick={() => onRemove(index)} />
|
||||
<QueryOperationAction
|
||||
title="Remove"
|
||||
icon="trash-alt"
|
||||
onClick={() => (config.featureToggles.transformationsRedesign ? setShowDeleteModal(true) : onRemove(index))}
|
||||
/>
|
||||
|
||||
{config.featureToggles.transformationsRedesign && (
|
||||
<ConfirmModal
|
||||
isOpen={showDeleteModal}
|
||||
title={`Delete ${uiConfig.name}?`}
|
||||
body="Note that removing one transformation may break others. If there is only a single transformation, you will go back to the main selection screen."
|
||||
confirmText="Delete"
|
||||
onConfirm={() => {
|
||||
setShowDeleteModal(false);
|
||||
onRemove(index);
|
||||
}}
|
||||
onDismiss={() => setShowDeleteModal(false)}
|
||||
/>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
|
||||
import { DataTransformerConfig, standardTransformersRegistry } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import config from 'app/core/config';
|
||||
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
|
||||
|
||||
import { PanelModel } from '../../state';
|
||||
@@ -20,30 +21,43 @@ describe('TransformationsEditor', () => {
|
||||
standardTransformersRegistry.setInit(getStandardTransformers);
|
||||
|
||||
describe('when no transformations configured', () => {
|
||||
it('renders transformations selection list', () => {
|
||||
function renderList() {
|
||||
setup();
|
||||
|
||||
const cards = screen.getAllByLabelText(/^New transform/i);
|
||||
const cards = screen.getAllByTestId(/New transform/i);
|
||||
expect(cards.length).toEqual(standardTransformersRegistry.list().length);
|
||||
}
|
||||
|
||||
it('renders transformations selection list', renderList);
|
||||
it('renders transformations selection list with transformationsRedesign feature toggled on', () => {
|
||||
config.featureToggles.transformationsRedesign = true;
|
||||
renderList();
|
||||
config.featureToggles.transformationsRedesign = false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when transformations configured', () => {
|
||||
it('renders transformation editors', () => {
|
||||
function renderEditors() {
|
||||
setup([
|
||||
{
|
||||
id: 'reduce',
|
||||
options: {},
|
||||
},
|
||||
]);
|
||||
const editors = screen.getAllByLabelText(/^Transformation editor/);
|
||||
const editors = screen.getAllByTestId(/Transformation editor/);
|
||||
expect(editors).toHaveLength(1);
|
||||
}
|
||||
|
||||
it('renders transformation editors', renderEditors);
|
||||
it('renders transformation editors with transformationsRedesign feature toggled on', () => {
|
||||
config.featureToggles.transformationsRedesign = true;
|
||||
renderEditors();
|
||||
config.featureToggles.transformationsRedesign = false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when Add transformation clicked', () => {
|
||||
it('renders transformations picker', async () => {
|
||||
const buttonLabel = 'Add transformation';
|
||||
async function renderPicker() {
|
||||
setup([
|
||||
{
|
||||
id: 'reduce',
|
||||
@@ -51,17 +65,24 @@ describe('TransformationsEditor', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const addTransformationButton = screen.getByText(buttonLabel);
|
||||
const addTransformationButton = screen.getByTestId(selectors.components.Transforms.addTransformationButton);
|
||||
await userEvent.click(addTransformationButton);
|
||||
|
||||
const search = screen.getByLabelText(selectors.components.Transforms.searchInput);
|
||||
const search = screen.getByTestId(selectors.components.Transforms.searchInput);
|
||||
expect(search).toBeDefined();
|
||||
}
|
||||
|
||||
it('renders transformations picker', renderPicker);
|
||||
it('renders transformation picker with transformationsRedesign feature toggled on', async () => {
|
||||
config.featureToggles.transformationsRedesign = true;
|
||||
await renderPicker();
|
||||
config.featureToggles.transformationsRedesign = false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('debug', () => {
|
||||
it('should show/hide debugger', async () => {
|
||||
async function showHideDebugger() {
|
||||
setup([
|
||||
{
|
||||
id: 'reduce',
|
||||
@@ -70,12 +91,19 @@ describe('TransformationsEditor', () => {
|
||||
]);
|
||||
const debuggerSelector = selectors.components.TransformTab.transformationEditorDebugger('Reduce');
|
||||
|
||||
expect(screen.queryByLabelText(debuggerSelector)).toBeNull();
|
||||
expect(screen.queryByTestId(debuggerSelector)).toBeNull();
|
||||
|
||||
const debugButton = screen.getByLabelText(selectors.components.QueryEditorRow.actionButton('Debug'));
|
||||
await userEvent.click(debugButton);
|
||||
|
||||
expect(screen.getByLabelText(debuggerSelector)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(debuggerSelector)).toBeInTheDocument();
|
||||
}
|
||||
|
||||
it('should show/hide debugger', showHideDebugger);
|
||||
it('renders transformation editors with transformationsRedesign feature toggled on', async () => {
|
||||
config.featureToggles.transformationsRedesign = true;
|
||||
await showHideDebugger();
|
||||
config.featureToggles.transformationsRedesign = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,25 +12,33 @@ import {
|
||||
SelectableValue,
|
||||
standardTransformersRegistry,
|
||||
TransformerRegistryItem,
|
||||
TransformerCategory,
|
||||
DataTransformerID,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
ConfirmModal,
|
||||
Container,
|
||||
CustomScrollbar,
|
||||
FilterPill,
|
||||
Themeable,
|
||||
VerticalGroup,
|
||||
withTheme,
|
||||
Input,
|
||||
Icon,
|
||||
IconButton,
|
||||
useStyles2,
|
||||
Card,
|
||||
Switch,
|
||||
} from '@grafana/ui';
|
||||
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
|
||||
import config from 'app/core/config';
|
||||
import { getDocsLink } from 'app/core/utils/docsLinks';
|
||||
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
|
||||
import { categoriesLabels } from 'app/features/transformers/utils';
|
||||
|
||||
import { AppNotificationSeverity } from '../../../../types';
|
||||
import { PanelModel } from '../../state';
|
||||
@@ -45,11 +53,26 @@ interface TransformationsEditorProps extends Themeable {
|
||||
panel: PanelModel;
|
||||
}
|
||||
|
||||
type viewAllType = 'viewAll';
|
||||
const viewAllValue = 'viewAll';
|
||||
const viewAllLabel = 'View all';
|
||||
|
||||
type FilterCategory = TransformerCategory | viewAllType;
|
||||
|
||||
const filterCategoriesLabels: Array<[FilterCategory, string]> = [
|
||||
[viewAllValue, viewAllLabel],
|
||||
...(Object.entries(categoriesLabels) as Array<[FilterCategory, string]>),
|
||||
];
|
||||
|
||||
interface State {
|
||||
data: DataFrame[];
|
||||
transformations: TransformationsEditorTransformation[];
|
||||
search: string;
|
||||
showPicker?: boolean;
|
||||
scrollTop?: number;
|
||||
showRemoveAllModal?: boolean;
|
||||
selectedFilter?: FilterCategory;
|
||||
showIllustrations?: boolean;
|
||||
}
|
||||
|
||||
class UnThemedTransformationsEditor extends React.PureComponent<TransformationsEditorProps, State> {
|
||||
@@ -67,6 +90,8 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
})),
|
||||
data: [],
|
||||
search: '',
|
||||
selectedFilter: viewAllValue,
|
||||
showIllustrations: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,6 +150,25 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<TransformationsEditorProps>, prevState: Readonly<State>): void {
|
||||
if (config.featureToggles.transformationsRedesign) {
|
||||
const prevHasTransforms = prevState.transformations.length > 0;
|
||||
const prevShowPicker = !prevHasTransforms || prevState.showPicker;
|
||||
|
||||
const currentHasTransforms = this.state.transformations.length > 0;
|
||||
const currentShowPicker = !currentHasTransforms || this.state.showPicker;
|
||||
|
||||
if (prevShowPicker !== currentShowPicker) {
|
||||
// kindOfZero will be a random number between 0 and 0.5. It will be rounded to 0 by the scrollable component.
|
||||
// We cannot always use 0 as it will not trigger a rerender of the scrollable component consistently
|
||||
// due to React changes detection algo.
|
||||
const kindOfZero = Math.random() / 2;
|
||||
|
||||
this.setState({ scrollTop: currentShowPicker ? kindOfZero : Number.MAX_SAFE_INTEGER });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onChange(transformations: TransformationsEditorTransformation[]) {
|
||||
this.setState({ transformations });
|
||||
this.props.panel.setTransformations(transformations.map((t) => t.transformation));
|
||||
@@ -145,7 +189,12 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
};
|
||||
|
||||
onTransformationAdd = (selectable: SelectableValue<string>) => {
|
||||
reportInteraction('panel_editor_tabs_transformations_management', {
|
||||
let eventName = 'panel_editor_tabs_transformations_management';
|
||||
if (config.featureToggles.transformationsRedesign) {
|
||||
eventName = 'transformations_redesign_' + eventName;
|
||||
}
|
||||
|
||||
reportInteraction(eventName, {
|
||||
action: 'add',
|
||||
transformationId: selectable.value,
|
||||
});
|
||||
@@ -165,21 +214,31 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
]);
|
||||
};
|
||||
|
||||
onTransformationChange = (idx: number, config: DataTransformerConfig) => {
|
||||
onTransformationChange = (idx: number, dataConfig: DataTransformerConfig) => {
|
||||
const { transformations } = this.state;
|
||||
const next = Array.from(transformations);
|
||||
reportInteraction('panel_editor_tabs_transformations_management', {
|
||||
let eventName = 'panel_editor_tabs_transformations_management';
|
||||
if (config.featureToggles.transformationsRedesign) {
|
||||
eventName = 'transformations_redesign_' + eventName;
|
||||
}
|
||||
|
||||
reportInteraction(eventName, {
|
||||
action: 'change',
|
||||
transformationId: next[idx].transformation.id,
|
||||
});
|
||||
next[idx].transformation = config;
|
||||
next[idx].transformation = dataConfig;
|
||||
this.onChange(next);
|
||||
};
|
||||
|
||||
onTransformationRemove = (idx: number) => {
|
||||
const { transformations } = this.state;
|
||||
const next = Array.from(transformations);
|
||||
reportInteraction('panel_editor_tabs_transformations_management', {
|
||||
let eventName = 'panel_editor_tabs_transformations_management';
|
||||
if (config.featureToggles.transformationsRedesign) {
|
||||
eventName = 'transformations_redesign_' + eventName;
|
||||
}
|
||||
|
||||
reportInteraction(eventName, {
|
||||
action: 'remove',
|
||||
transformationId: next[idx].transformation.id,
|
||||
});
|
||||
@@ -187,6 +246,11 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
this.onChange(next);
|
||||
};
|
||||
|
||||
onTransformationRemoveAll = () => {
|
||||
this.onChange([]);
|
||||
this.setState({ showRemoveAllModal: false });
|
||||
};
|
||||
|
||||
onDragEnd = (result: DropResult) => {
|
||||
const { transformations } = this.state;
|
||||
|
||||
@@ -230,10 +294,20 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
};
|
||||
|
||||
renderTransformsPicker() {
|
||||
const styles = getStyles(config.theme2);
|
||||
const { transformations, search } = this.state;
|
||||
let suffix: React.ReactNode = null;
|
||||
let xforms = standardTransformersRegistry.list().sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0));
|
||||
|
||||
if (this.state.selectedFilter !== viewAllValue) {
|
||||
xforms = xforms.filter(
|
||||
(t) =>
|
||||
t.categories &&
|
||||
this.state.selectedFilter &&
|
||||
t.categories.has(this.state.selectedFilter as TransformerCategory)
|
||||
);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
const lower = search.toLowerCase();
|
||||
const filtered = xforms.filter((t) => {
|
||||
@@ -272,6 +346,107 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
);
|
||||
}
|
||||
|
||||
const Picker = () => (
|
||||
<>
|
||||
{config.featureToggles.transformationsRedesign && (
|
||||
<>
|
||||
{!noTransforms && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
icon="angle-left"
|
||||
onClick={() => {
|
||||
this.setState({ showPicker: false });
|
||||
}}
|
||||
>
|
||||
Go back to <i>Transformations in use</i>
|
||||
</Button>
|
||||
)}
|
||||
<div className={styles.pickerInformationLine}>
|
||||
<a href={getDocsLink(DocsId.Transformations)} className="external-link" target="_blank" rel="noreferrer">
|
||||
<span className={styles.pickerInformationLineHighlight}>Transformations</span>{' '}
|
||||
<Icon name="external-link-alt" />
|
||||
</a>
|
||||
allow you to manipulate your data before a visualization is applied.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<VerticalGroup>
|
||||
{!config.featureToggles.transformationsRedesign && (
|
||||
<Input
|
||||
data-testid={selectors.components.Transforms.searchInput}
|
||||
value={search ?? ''}
|
||||
autoFocus={!noTransforms}
|
||||
placeholder="Search for transformation"
|
||||
onChange={this.onSearchChange}
|
||||
onKeyDown={this.onSearchKeyDown}
|
||||
suffix={suffix}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!config.featureToggles.transformationsRedesign &&
|
||||
xforms.map((t) => {
|
||||
return (
|
||||
<TransformationCard
|
||||
key={t.name}
|
||||
transform={t}
|
||||
onClick={() => {
|
||||
this.onTransformationAdd({ value: t.id });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{config.featureToggles.transformationsRedesign && (
|
||||
<div className={styles.searchWrapper}>
|
||||
<Input
|
||||
data-testid={selectors.components.Transforms.searchInput}
|
||||
className={styles.searchInput}
|
||||
value={search ?? ''}
|
||||
autoFocus={!noTransforms}
|
||||
placeholder="Search for transformation"
|
||||
onChange={this.onSearchChange}
|
||||
onKeyDown={this.onSearchKeyDown}
|
||||
suffix={suffix}
|
||||
/>
|
||||
<div className={styles.showImages}>
|
||||
<span className={styles.illustationSwitchLabel}>Show images</span>{' '}
|
||||
<Switch
|
||||
value={this.state.showIllustrations}
|
||||
onChange={() => this.setState({ showIllustrations: !this.state.showIllustrations })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.featureToggles.transformationsRedesign && (
|
||||
<div className={styles.filterWrapper}>
|
||||
{filterCategoriesLabels.map(([slug, label]) => {
|
||||
return (
|
||||
<FilterPill
|
||||
key={slug}
|
||||
onClick={() => this.setState({ selectedFilter: slug })}
|
||||
label={label}
|
||||
selected={this.state.selectedFilter === slug}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.featureToggles.transformationsRedesign && (
|
||||
<TransformationsGrid
|
||||
showIllustrations={this.state.showIllustrations}
|
||||
transformations={xforms}
|
||||
onClick={(id) => {
|
||||
this.onTransformationAdd({ value: id });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{noTransforms && (
|
||||
@@ -312,29 +487,7 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
</Container>
|
||||
)}
|
||||
{showPicker ? (
|
||||
<VerticalGroup>
|
||||
<Input
|
||||
aria-label={selectors.components.Transforms.searchInput}
|
||||
value={search ?? ''}
|
||||
autoFocus={!noTransforms}
|
||||
placeholder="Add transformation"
|
||||
onChange={this.onSearchChange}
|
||||
onKeyDown={this.onSearchKeyDown}
|
||||
suffix={suffix}
|
||||
/>
|
||||
|
||||
{xforms.map((t) => {
|
||||
return (
|
||||
<TransformationCard
|
||||
key={t.name}
|
||||
transform={t}
|
||||
onClick={() => {
|
||||
this.onTransformationAdd({ value: t.id });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</VerticalGroup>
|
||||
<Picker />
|
||||
) : (
|
||||
<Button
|
||||
icon="plus"
|
||||
@@ -342,8 +495,9 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
onClick={() => {
|
||||
this.setState({ showPicker: true });
|
||||
}}
|
||||
data-testid={selectors.components.Transforms.addTransformationButton}
|
||||
>
|
||||
Add transformation
|
||||
Add{config.featureToggles.transformationsRedesign ? ' another ' : ' '}transformation
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -351,6 +505,7 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
}
|
||||
|
||||
render() {
|
||||
const styles = getStyles(config.theme2);
|
||||
const {
|
||||
panel: { alert },
|
||||
} = this.props;
|
||||
@@ -363,16 +518,40 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomScrollbar autoHeightMin="100%">
|
||||
<Container padding="md">
|
||||
<div aria-label={selectors.components.TransformTab.content}>
|
||||
<CustomScrollbar scrollTop={this.state.scrollTop} autoHeightMin="100%">
|
||||
<Container padding="lg">
|
||||
<div data-testid={selectors.components.TransformTab.content}>
|
||||
{hasTransforms && alert ? (
|
||||
<Alert
|
||||
severity={AppNotificationSeverity.Error}
|
||||
title="Transformations can't be used on a panel with alerts"
|
||||
/>
|
||||
) : null}
|
||||
{hasTransforms && this.renderTransformationEditors()}
|
||||
{hasTransforms && config.featureToggles.transformationsRedesign && !this.state.showPicker && (
|
||||
<div className={styles.listInformationLineWrapper}>
|
||||
<span className={styles.listInformationLineText}>Transformations in use</span>{' '}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
this.setState({ showRemoveAllModal: true });
|
||||
}}
|
||||
>
|
||||
Delete all transformations
|
||||
</Button>
|
||||
<ConfirmModal
|
||||
isOpen={Boolean(this.state.showRemoveAllModal)}
|
||||
title="Delete all transformations?"
|
||||
body="By deleting all transformations, you will go back to the main selection screen."
|
||||
confirmText="Delete all"
|
||||
onConfirm={() => this.onTransformationRemoveAll()}
|
||||
onDismiss={() => this.setState({ showRemoveAllModal: false })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{hasTransforms &&
|
||||
(!config.featureToggles.transformationsRedesign || !this.state.showPicker) &&
|
||||
this.renderTransformationEditors()}
|
||||
{this.renderTransformsPicker()}
|
||||
</div>
|
||||
</Container>
|
||||
@@ -391,7 +570,7 @@ function TransformationCard({ transform, onClick }: TransformationCardProps) {
|
||||
return (
|
||||
<Card
|
||||
className={styles.card}
|
||||
aria-label={selectors.components.TransformTab.newTransform(transform.name)}
|
||||
data-testid={selectors.components.TransformTab.newTransform(transform.name)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Card.Heading>{transform.name}</Card.Heading>
|
||||
@@ -411,7 +590,159 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
margin: 0;
|
||||
padding: ${theme.spacing(1)};
|
||||
`,
|
||||
grid: css`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
grid-auto-rows: 1fr;
|
||||
gap: ${theme.spacing(2)} ${theme.spacing(1)};
|
||||
width: 100%;
|
||||
`,
|
||||
newCard: css`
|
||||
grid-template-rows: min-content 0 1fr 0;
|
||||
`,
|
||||
badge: css`
|
||||
padding: 4px 3px;
|
||||
`,
|
||||
heading: css`
|
||||
font-weight: 400;
|
||||
|
||||
> button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: no-wrap;
|
||||
}
|
||||
`,
|
||||
description: css`
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
`,
|
||||
image: css`
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin-top: ${theme.spacing(2)};
|
||||
`,
|
||||
searchWrapper: css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 27px;
|
||||
row-gap: 16px;
|
||||
width: 100%;
|
||||
`,
|
||||
searchInput: css`
|
||||
flex-grow: 1;
|
||||
width: initial;
|
||||
`,
|
||||
showImages: css`
|
||||
flex-basis: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
`,
|
||||
pickerInformationLine: css`
|
||||
font-size: 16px;
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
pickerInformationLineHighlight: css`
|
||||
vertical-align: middle;
|
||||
`,
|
||||
illustationSwitchLabel: css`
|
||||
white-space: nowrap;
|
||||
`,
|
||||
filterWrapper: css`
|
||||
padding: ${theme.spacing(1)} 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
row-gap: ${theme.spacing(1)};
|
||||
column-gap: ${theme.spacing(0.5)};
|
||||
`,
|
||||
listInformationLineWrapper: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
`,
|
||||
listInformationLineText: css`
|
||||
font-size: 16px;
|
||||
`,
|
||||
pluginStateInfoWrapper: css`
|
||||
margin-left: 5px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
interface TransformationsGridProps {
|
||||
transformations: Array<TransformerRegistryItem<any>>;
|
||||
showIllustrations?: boolean;
|
||||
onClick: (id: string) => void;
|
||||
}
|
||||
|
||||
function TransformationsGrid({ showIllustrations, transformations, onClick }: TransformationsGridProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.grid}>
|
||||
{transformations.map((transform) => (
|
||||
<Card
|
||||
key={transform.id}
|
||||
className={styles.newCard}
|
||||
data-testid={selectors.components.TransformTab.newTransform(transform.name)}
|
||||
onClick={() => onClick(transform.id)}
|
||||
>
|
||||
<Card.Heading className={styles.heading}>
|
||||
<>
|
||||
<span>{transform.name}</span>
|
||||
<span className={styles.pluginStateInfoWrapper}>
|
||||
<PluginStateInfo className={styles.badge} state={transform.state} />
|
||||
</span>
|
||||
</>
|
||||
</Card.Heading>
|
||||
<Card.Description className={styles.description}>
|
||||
<>
|
||||
<span>{getTransformationsRedesignDescriptions(transform.id)}</span>
|
||||
{showIllustrations && (
|
||||
<span>
|
||||
<img className={styles.image} src={getImagePath(transform.id)} alt={transform.name} />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
</Card.Description>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getImagePath = (id: string) => {
|
||||
const folder = config.theme2.isDark ? 'dark' : 'light';
|
||||
|
||||
return `public/img/transformations/${folder}/${id}.svg`;
|
||||
};
|
||||
|
||||
const getTransformationsRedesignDescriptions = (id: string): string => {
|
||||
const overrides: { [key: string]: string } = {
|
||||
[DataTransformerID.concatenate]: 'Combine all fields into a single frame.',
|
||||
[DataTransformerID.configFromData]: 'Set unit, min, max and more.',
|
||||
[DataTransformerID.fieldLookup]: 'Use a field value to lookup countries, states, or airports.',
|
||||
[DataTransformerID.filterFieldsByName]: 'Removes part of the query results using a regex pattern.',
|
||||
[DataTransformerID.filterByRefId]: 'Filter out queries in panels that have multiple queries.',
|
||||
[DataTransformerID.filterByValue]: 'Removes rows of the query results using user-defined filters.',
|
||||
[DataTransformerID.groupBy]: 'Group the data by a field value then process calculations.',
|
||||
[DataTransformerID.groupingToMatrix]: 'Summarizes and reorganizes data based on three fields.',
|
||||
[DataTransformerID.joinByField]: 'Combine rows from 2+ tables, based on a related field.',
|
||||
[DataTransformerID.labelsToFields]: 'Groups series by time and return labels or tags as fields.',
|
||||
[DataTransformerID.merge]: 'Merge multiple series. Values will be combined into one row.',
|
||||
[DataTransformerID.organize]: 'Allows the user to re-order, hide, or rename fields / columns.',
|
||||
[DataTransformerID.partitionByValues]: 'Splits a one-frame dataset into multiple series.',
|
||||
[DataTransformerID.prepareTimeSeries]: 'Will stretch data frames from the wide format into the long format.',
|
||||
[DataTransformerID.reduce]: 'Reduce all rows or data points to a single value (ex. max, mean).',
|
||||
[DataTransformerID.renameByRegex]: 'Reduce all rows or data points to a single value (ex. max, mean).',
|
||||
[DataTransformerID.seriesToRows]: 'Merge multiple series. Return time, metric and values as a row.',
|
||||
};
|
||||
|
||||
return overrides[id] || standardTransformersRegistry.getIfExists(id)?.description || '';
|
||||
};
|
||||
|
||||
export const TransformationsEditor = withTheme(UnThemedTransformationsEditor);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Badge, BadgeProps } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
state?: PluginState;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const PluginStateInfo = (props: Props) => {
|
||||
@@ -14,7 +15,15 @@ export const PluginStateInfo = (props: Props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Badge color={display.color} title={display.tooltip} text={display.text} icon={display.icon} />;
|
||||
return (
|
||||
<Badge
|
||||
className={props.className}
|
||||
color={display.color}
|
||||
title={display.tooltip}
|
||||
text={display.text}
|
||||
icon={display.icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function getFeatureStateInfo(state?: PluginState): BadgeProps | null {
|
||||
|
||||
Reference in New Issue
Block a user