From e7625186af89454eb60f03e4702fb9aa85df4a67 Mon Sep 17 00:00:00 2001 From: Ayush Kaithwas Date: Tue, 30 Dec 2025 20:05:43 +0530 Subject: [PATCH 1/5] Dashboards: Clear edit pane selection when entering panel edit (#115658) * Clear selection on entering edit mode. Added test to verify selection is cleared when editing a panel. * Update comment --------- Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> --- .../panel-edit/PanelEditor.test.ts | 31 +++++++++++++++++++ .../panel-edit/PanelEditor.tsx | 5 +++ 2 files changed, 36 insertions(+) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index ee2bda935fd..89634322347 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -112,6 +112,37 @@ describe('PanelEditor', () => { }); }); + describe('Entering panel edit', () => { + it('should clear edit pane selection', () => { + pluginPromise = Promise.resolve(getPanelPlugin({ id: 'text', skipDataQuery: true })); + + const panel = new VizPanel({ + key: 'panel-1', + pluginId: 'text', + title: 'original title', + }); + const gridItem = new DashboardGridItem({ body: panel }); + const panelEditor = buildPanelEditScene(panel); + const dashboard = new DashboardScene({ + editPanel: panelEditor, + isEditing: true, + $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), + body: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [gridItem], + }), + }), + }); + + dashboard.state.editPane.selectObject(panel, panel.state.key!, { force: true }); + expect(dashboard.state.editPane.getSelection()).toBe(panel); + + deactivate = activateFullSceneTree(dashboard); + + expect(dashboard.state.editPane.getSelection()).toBeUndefined(); + }); + }); + describe('When discarding', () => { it('should discard changes revert all changes', async () => { const { panelEditor, panel, dashboard } = await setup(); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index e656a39e6a1..497d58e505a 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -84,6 +84,11 @@ export class PanelEditor extends SceneObjectBase { private _activationHandler() { const panel = this.state.panelRef.resolve(); + const dashboard = getDashboardSceneFor(this); + + // Clear any panel selection when entering panel edit mode. + // Need to clear selection here since selection is activated when panel edit mode is entered through the panel actions menu. This causes sidebar panel editor to be open when exiting panel edit mode + dashboard.state.editPane.clearSelection(); if (panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) { if (config.featureToggles.newVizSuggestions) { From 9c6feb8de5fb5adf0304b79b88fc03917ff5b177 Mon Sep 17 00:00:00 2001 From: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:37:19 -0600 Subject: [PATCH 2/5] Elasticsearch: Builder queries no longer execute in code mode (#115456) * The builder query no longer runs if code mode query is empty. Remove checks for query being empty to run raw query. * missed save * prettier? * Update public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts Co-authored-by: Andreas Christou --------- Co-authored-by: Andreas Christou --- .../elasticsearch/data_query_processor.go | 2 +- .../elasticsearch/data_query_validator.go | 2 +- .../state/reducer.test.ts | 25 ++++++++++- .../BucketAggregationsEditor/state/reducer.ts | 7 ++- .../state/reducer.test.ts | 24 ++++++++++- .../MetricAggregationsEditor/state/reducer.ts | 7 ++- .../components/QueryEditor/state.test.ts | 43 ++++++++++++++++++- .../components/QueryEditor/state.ts | 4 ++ 8 files changed, 107 insertions(+), 7 deletions(-) diff --git a/pkg/tsdb/elasticsearch/data_query_processor.go b/pkg/tsdb/elasticsearch/data_query_processor.go index 1c4ec7b3cdd..288d6ce30de 100644 --- a/pkg/tsdb/elasticsearch/data_query_processor.go +++ b/pkg/tsdb/elasticsearch/data_query_processor.go @@ -24,7 +24,7 @@ func (e *elasticsearchDataQuery) processQuery(q *Query, ms *es.MultiSearchReques filters.AddDateRangeFilter(defaultTimeField, to, from, es.DateFormatEpochMS) filters.AddQueryStringFilter(q.RawQuery, true) - if q.EditorType != nil && *q.EditorType == "code" && q.RawDSLQuery != "" { + if q.EditorType != nil && *q.EditorType == "code" { cfg := backend.GrafanaConfigFromContext(e.ctx) if !cfg.FeatureToggles().IsEnabled("elasticsearchRawDSLQuery") { return backend.DownstreamError(fmt.Errorf("raw DSL query feature is disabled. Enable the elasticsearchRawDSLQuery feature toggle to use this query type")) diff --git a/pkg/tsdb/elasticsearch/data_query_validator.go b/pkg/tsdb/elasticsearch/data_query_validator.go index 648dbb53109..72bcde016b6 100644 --- a/pkg/tsdb/elasticsearch/data_query_validator.go +++ b/pkg/tsdb/elasticsearch/data_query_validator.go @@ -7,7 +7,7 @@ import ( // isQueryWithError validates the query and returns an error if invalid func isQueryWithError(query *Query) error { // Skip validation for raw DSL queries because no easy way to see it is valid without just running it - if query.EditorType != nil && *query.EditorType == "code" && query.RawDSLQuery != "" { + if query.EditorType != nil && *query.EditorType == "code" { return nil } if len(query.BucketAggs) == 0 { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts index 6a34d5d7d91..f4a5cc02dde 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts @@ -7,7 +7,7 @@ import { import { defaultBucketAgg } from '../../../../queryDef'; import { reducerTester } from '../../../reducerTester'; import { changeMetricType } from '../../MetricAggregationsEditor/state/actions'; -import { initQuery } from '../../state'; +import { changeEditorTypeAndResetQuery, initQuery } from '../../state'; import { bucketAggregationConfig } from '../utils'; import { @@ -180,4 +180,27 @@ describe('Bucket Aggregations Reducer', () => { .thenStateShouldEqual([bucketAgg]); }); }); + + describe('When switching editor type', () => { + it('Should reset bucket aggregations to default when switching editor types', () => { + const defaultTimeField = '@timestamp'; + const initialState: BucketAggregation[] = [ + { + id: '1', + type: 'date_histogram', + field: '@timestamp', + }, + { + id: '2', + type: 'terms', + field: 'status', + }, + ]; + + reducerTester() + .givenReducer(createReducer(defaultTimeField), initialState) + .whenActionIsDispatched(changeEditorTypeAndResetQuery('code')) + .thenStateShouldEqual([{ ...defaultBucketAgg('2'), field: defaultTimeField }]); + }); + }); }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts index b3638e1f1d1..5ba29e656d8 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts @@ -6,7 +6,7 @@ import { defaultBucketAgg } from '../../../../queryDef'; import { removeEmpty } from '../../../../utils'; import { changeMetricType } from '../../MetricAggregationsEditor/state/actions'; import { metricAggregationConfig } from '../../MetricAggregationsEditor/utils'; -import { initQuery } from '../../state'; +import { changeEditorTypeAndResetQuery, initQuery } from '../../state'; import { bucketAggregationConfig } from '../utils'; import { @@ -87,6 +87,11 @@ export const createReducer = return state; } + if (changeEditorTypeAndResetQuery.match(action)) { + // Returns the default bucket agg. We will always want to set the default when switching types + return [{ ...defaultBucketAgg('2'), field: defaultTimeField }]; + } + if (changeBucketAggregationSetting.match(action)) { return state!.map((bucketAgg) => { if (bucketAgg.id !== action.payload.bucketAgg.id) { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts index 5662ad399ea..9dcbaa9f974 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts @@ -7,7 +7,7 @@ import { import { defaultMetricAgg } from '../../../../queryDef'; import { reducerTester } from '../../../reducerTester'; -import { initQuery } from '../../state'; +import { changeEditorTypeAndResetQuery, initQuery } from '../../state'; import { metricAggregationConfig } from '../utils'; import { @@ -248,4 +248,26 @@ describe('Metric Aggregations Reducer', () => { .whenActionIsDispatched(initQuery()) .thenStateShouldEqual([defaultMetricAgg('1')]); }); + + describe('When switching editor type', () => { + it('Should reset to single default metric when switching to code editor', () => { + const initialState: MetricAggregation[] = [ + { + id: '1', + type: 'avg', + field: 'value', + }, + { + id: '2', + type: 'max', + field: 'value', + }, + ]; + + reducerTester() + .givenReducer(reducer, initialState) + .whenActionIsDispatched(changeEditorTypeAndResetQuery('code')) + .thenStateShouldEqual([defaultMetricAgg('1')]); + }); + }); }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts index 966bd71d6c8..c0dab7bd4b1 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.ts @@ -4,7 +4,7 @@ import { ElasticsearchDataQuery, MetricAggregation } from 'app/plugins/datasourc import { defaultMetricAgg, queryTypeToMetricType } from '../../../../queryDef'; import { removeEmpty } from '../../../../utils'; -import { initQuery } from '../../state'; +import { changeEditorTypeAndResetQuery, initQuery } from '../../state'; import { isMetricAggregationWithMeta, isMetricAggregationWithSettings, isPipelineAggregation } from '../aggregations'; import { getChildren, metricAggregationConfig } from '../utils'; @@ -65,6 +65,11 @@ export const reducer = ( }); } + if (changeEditorTypeAndResetQuery.match(action)) { + // Reset to default metric when switching to editor types + return [defaultMetricAgg('1')]; + } + if (changeMetricField.match(action)) { return state!.map((metric) => { if (metric.id !== action.payload.id) { diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts index cad89cd32a7..111b284eb79 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts @@ -1,7 +1,15 @@ import { ElasticsearchDataQuery } from '../../dataquery.gen'; import { reducerTester } from '../reducerTester'; -import { aliasPatternReducer, changeAliasPattern, changeQuery, initQuery, queryReducer } from './state'; +import { + aliasPatternReducer, + changeAliasPattern, + changeEditorTypeAndResetQuery, + changeQuery, + initQuery, + queryReducer, + rawDSLQueryReducer, +} from './state'; describe('Query Reducer', () => { describe('On Init', () => { @@ -42,6 +50,17 @@ describe('Query Reducer', () => { .whenActionIsDispatched({ type: 'THIS ACTION SHOULD NOT HAVE ANY EFFECT IN THIS REDUCER' }) .thenStateShouldEqual(initialState); }); + + describe('When switching editor type', () => { + it('Should clear query when switching editor types', () => { + const initialQuery: ElasticsearchDataQuery['query'] = 'Some lucene query'; + + reducerTester() + .givenReducer(queryReducer, initialQuery) + .whenActionIsDispatched(changeEditorTypeAndResetQuery('code')) + .thenStateShouldEqual(''); + }); + }); }); describe('Alias Pattern Reducer', () => { @@ -62,4 +81,26 @@ describe('Alias Pattern Reducer', () => { .whenActionIsDispatched({ type: 'THIS ACTION SHOULD NOT HAVE ANY EFFECT IN THIS REDUCER' }) .thenStateShouldEqual(initialState); }); + + describe('When switching editor type', () => { + it('Should clear alias when switching editor types', () => { + const initialAlias: ElasticsearchDataQuery['alias'] = 'Some alias pattern'; + + reducerTester() + .givenReducer(aliasPatternReducer, initialAlias) + .whenActionIsDispatched(changeEditorTypeAndResetQuery('code')) + .thenStateShouldEqual(''); + }); + }); +}); + +describe('Raw DSL Query Reducer', () => { + it('Should clear raw DSL query when switching editor types', () => { + const initialRawQuery: ElasticsearchDataQuery['rawDSLQuery'] = '{"query": {"match_all": {}}}'; + + reducerTester() + .givenReducer(rawDSLQueryReducer, initialRawQuery) + .whenActionIsDispatched(changeEditorTypeAndResetQuery('builder')) + .thenStateShouldEqual(''); + }); }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.ts index a9ed51b39ff..5a1be7be31c 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.ts @@ -58,6 +58,10 @@ export const aliasPatternReducer = (prevAliasPattern: ElasticsearchDataQuery['al return action.payload; } + if (changeEditorTypeAndResetQuery.match(action)) { + return ''; + } + if (initQuery.match(action)) { return prevAliasPattern || ''; } From d291dfb35b324f12f55ebf34dc97be36d8e27f1c Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Tue, 30 Dec 2025 08:51:46 -0700 Subject: [PATCH 3/5] Dashboard Conversion: Fix type assertion mismatch in data loss detection (#115749) --- .../conversion_data_loss_detection.go | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go index db3353b66a1..269fb51bd70 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_data_loss_detection.go @@ -180,12 +180,15 @@ func countAnnotationsV0V1(spec map[string]interface{}) int { return 0 } - annotationList, ok := annotations["list"].([]interface{}) - if !ok { - return 0 + // Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation) + if annotationList, ok := annotations["list"].([]interface{}); ok { + return len(annotationList) + } + if annotationList, ok := annotations["list"].([]map[string]interface{}); ok { + return len(annotationList) } - return len(annotationList) + return 0 } // countLinksV0V1 counts dashboard links in v0alpha1 or v1beta1 dashboard spec @@ -194,12 +197,15 @@ func countLinksV0V1(spec map[string]interface{}) int { return 0 } - links, ok := spec["links"].([]interface{}) - if !ok { - return 0 + // Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation) + if links, ok := spec["links"].([]interface{}); ok { + return len(links) + } + if links, ok := spec["links"].([]map[string]interface{}); ok { + return len(links) } - return len(links) + return 0 } // countVariablesV0V1 counts template variables in v0alpha1 or v1beta1 dashboard spec @@ -213,12 +219,15 @@ func countVariablesV0V1(spec map[string]interface{}) int { return 0 } - variableList, ok := templating["list"].([]interface{}) - if !ok { - return 0 + // Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation) + if variableList, ok := templating["list"].([]interface{}); ok { + return len(variableList) + } + if variableList, ok := templating["list"].([]map[string]interface{}); ok { + return len(variableList) } - return len(variableList) + return 0 } // collectStatsV0V1 collects statistics from v0alpha1 or v1beta1 dashboard From 52698cf0da5d07eeef04398d9c5cbd2c57a4c3ad Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Tue, 30 Dec 2025 10:55:40 -0500 Subject: [PATCH 4/5] Sparkline: Restore to a function component (#115447) * Sparkline: Restore to a function component * fix whitespace lint issue --- .../src/components/Sparkline/Sparkline.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx index d1fb4f3b0e0..a9d3f039c42 100644 --- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx +++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx @@ -17,8 +17,9 @@ export interface SparklineProps extends Themeable2 { showHighlights?: boolean; } -export const SparklineFn: React.FC = memo((props) => { +export const Sparkline: React.FC = memo((props) => { const { sparkline, config: fieldConfig, theme, width, height, showHighlights } = props; + const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, theme, fieldConfig, showHighlights); if (warning) { return null; @@ -30,14 +31,4 @@ export const SparklineFn: React.FC = memo((props) => { return ; }); -SparklineFn.displayName = 'Sparkline'; - -// we converted to function component above, but some apps extend Sparkline, so we need -// to keep exporting a class component until those apps are all rolled out. -// see https://github.com/grafana/app-observability-plugin/pull/2079 -// eslint-disable-next-line react-prefer-function-component/react-prefer-function-component -export class Sparkline extends React.PureComponent { - render() { - return ; - } -} +Sparkline.displayName = 'Sparkline'; From 82b4ce0ece684c46ba1d749a939fbbaee8627bf7 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Tue, 30 Dec 2025 11:46:29 -0500 Subject: [PATCH 5/5] Redesign Empty Transformation Panel (#115648) Co-authored-by: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> --- .../EmptyTransformationsMessage.tsx | 47 ++++---- .../SqlExpressionCard.tsx | 62 ++-------- .../TransformationCard.tsx | 106 ++++-------------- .../TransformationPickerNg.tsx | 9 +- .../TransformationsEditor/getCardStyles.ts | 34 ++++++ 5 files changed, 96 insertions(+), 162 deletions(-) create mode 100644 public/app/features/dashboard/components/TransformationsEditor/getCardStyles.ts diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx index eab5f3e9c58..1e8ff639785 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx @@ -4,7 +4,7 @@ import { DataFrame, DataTransformerID, standardTransformersRegistry, Transformer import { selectors } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; -import { Box, Button, Grid, Stack, Text } from '@grafana/ui'; +import { Box, Button, Stack, Text } from '@grafana/ui'; import config from 'app/core/config'; import { SqlExpressionCard } from '../../../dashboard/components/TransformationsEditor/SqlExpressionCard'; @@ -26,9 +26,6 @@ const TRANSFORMATION_IDS = [ DataTransformerID.filterByValue, ]; -const GRID_COLUMNS_WITH_SQL = 5; -const GRID_COLUMNS_WITHOUT_SQL = 4; - export function LegacyEmptyTransformationsMessage({ onShowPicker }: { onShowPicker: () => void }) { return ( @@ -94,13 +91,25 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps) }; const showSqlCard = hasGoToQueries && config.featureToggles.sqlExpressions; - const gridColumns = showSqlCard ? GRID_COLUMNS_WITH_SQL : GRID_COLUMNS_WITHOUT_SQL; return ( - - + + + + + Add a Transformation + + + + Transformations allow data to be changed in various ways before your visualization is shown. +
+ This includes joining data together, renaming fields, making calculations, formatting data for display, + and more. +
+
+
{(hasAddTransformation || hasGoToQueries) && ( - + {showSqlCard && ( ))} - +
)} - - - +
); diff --git a/public/app/features/dashboard/components/TransformationsEditor/SqlExpressionCard.tsx b/public/app/features/dashboard/components/TransformationsEditor/SqlExpressionCard.tsx index 0cb9302df2e..5f9712897b8 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/SqlExpressionCard.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/SqlExpressionCard.tsx @@ -1,7 +1,6 @@ -import { css } from '@emotion/css'; +import { Card, Text, useStyles2 } from '@grafana/ui'; -import { GrafanaTheme2 } from '@grafana/data'; -import { Card, useStyles2 } from '@grafana/ui'; +import { getCardStyles } from './getCardStyles'; export interface SqlExpressionCardProps { name: string; @@ -12,60 +11,15 @@ export interface SqlExpressionCardProps { } export function SqlExpressionCard({ name, description, imageUrl, onClick, testId }: SqlExpressionCardProps) { - const styles = useStyles2(getSqlExpressionCardStyles); + const styles = useStyles2(getCardStyles); return ( - - -
- {name} -
-
- - {description} - {imageUrl && ( - - {name} - - )} + + {name} + + {description} + {imageUrl && {name}} ); } - -function getSqlExpressionCardStyles(theme: GrafanaTheme2) { - return { - card: css({ - gridTemplateRows: 'min-content 0 1fr 0', - marginBottom: 0, - }), - heading: css({ - fontWeight: 400, - '> button': { - width: '100%', - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: theme.spacing(1), - }, - }), - titleRow: css({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - flexWrap: 'nowrap', - width: '100%', - }), - description: css({ - fontSize: theme.typography.bodySmall.fontSize, - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - }), - image: css({ - display: 'block', - maxWidth: '100%', - marginTop: theme.spacing(2), - }), - }; -} diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationCard.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationCard.tsx index ad113f1b227..8e909480f74 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationCard.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationCard.tsx @@ -1,35 +1,38 @@ -import { cx, css } from '@emotion/css'; +import { cx } from '@emotion/css'; import { DataFrame, - GrafanaTheme2, TransformerRegistryItem, TransformationApplicabilityLevels, standardTransformersRegistry, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Badge, Card, IconButton, useStyles2, useTheme2 } from '@grafana/ui'; +import { Badge, Card, IconButton, Stack, Text, useStyles2, useTheme2 } from '@grafana/ui'; import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; +import { getCardStyles } from './getCardStyles'; + export interface TransformationCardProps { - transform: TransformerRegistryItem; + data?: DataFrame[]; + fullWidth?: boolean; onClick: (id: string) => void; showIllustrations?: boolean; - data?: DataFrame[]; showPluginState?: boolean; showTags?: boolean; + transform: TransformerRegistryItem; } export function TransformationCard({ - transform, - showIllustrations, - onClick, data = [], + fullWidth = false, + onClick, + showIllustrations, showPluginState = true, showTags = true, + transform, }: TransformationCardProps) { const theme = useTheme2(); - const styles = useStyles2(getTransformationCardStyles); + const styles = useStyles2(getCardStyles, fullWidth); // Check to see if the transform is applicable to the given data let applicabilityScore = TransformationApplicabilityLevels.Applicable; @@ -47,7 +50,7 @@ export function TransformationCard({ } } - const cardClasses = !isApplicable && data.length > 0 ? cx(styles.newCard, styles.cardDisabled) : styles.newCard; + const cardClasses = cx(styles.baseCard, { [styles.cardDisabled]: !isApplicable }); const imageUrl = theme.isDark ? transform.imageDark : transform.imageLight; const description = standardTransformersRegistry.getIfExists(transform.id)?.description; @@ -58,15 +61,11 @@ export function TransformationCard({ onClick={() => onClick(transform.id)} noMargin > - -
- {transform.name} - {showPluginState && ( - - - - )} -
+ + + {transform.name} + {showPluginState && } + {showTags && transform.tags && transform.tags.size > 0 && (
{Array.from(transform.tags).map((tag) => ( @@ -75,74 +74,13 @@ export function TransformationCard({
)}
- - {description} - {showIllustrations && imageUrl && ( - - {transform.name} - - )} + + {description || ''} + {showIllustrations && imageUrl && {transform.name}} {!isApplicable && applicabilityDescription !== null && ( - + )}
); } - -function getTransformationCardStyles(theme: GrafanaTheme2) { - return { - heading: css({ - fontWeight: 400, - '> button': { - width: '100%', - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: theme.spacing(1), - }, - }), - titleRow: css({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - flexWrap: 'nowrap', - width: '100%', - }), - description: css({ - fontSize: theme.typography.bodySmall.fontSize, - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - }), - image: css({ - display: 'block', - maxWidth: '100%', - marginTop: theme.spacing(2), - }), - cardDisabled: css({ - backgroundColor: theme.colors.action.disabledBackground, - img: { - filter: 'grayscale(100%)', - opacity: 0.33, - }, - }), - cardApplicableInfo: css({ - position: 'absolute', - bottom: theme.spacing(1), - right: theme.spacing(1), - }), - newCard: css({ - gridTemplateRows: 'min-content 0 1fr 0', - marginBottom: 0, - }), - pluginStateInfoWrapper: css({ - marginLeft: theme.spacing(0.5), - }), - tagsWrapper: css({ - display: 'flex', - flexWrap: 'wrap', - gap: theme.spacing(0.5), - }), - }; -} diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx index fb0be6864f5..e27e554fada 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx @@ -165,11 +165,12 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data {transformations.map((transform) => ( ))} diff --git a/public/app/features/dashboard/components/TransformationsEditor/getCardStyles.ts b/public/app/features/dashboard/components/TransformationsEditor/getCardStyles.ts new file mode 100644 index 00000000000..b3989282ee2 --- /dev/null +++ b/public/app/features/dashboard/components/TransformationsEditor/getCardStyles.ts @@ -0,0 +1,34 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; + +export const getCardStyles = (theme: GrafanaTheme2, fullWidth?: boolean) => ({ + baseCard: css({ + maxWidth: fullWidth ? 'none' : '200px', + width: fullWidth ? '100%' : 'auto', + marginBottom: 0, + }), + image: css({ + display: 'block', + maxWidth: '100%', + marginTop: theme.spacing(2), + }), + cardDisabled: css({ + backgroundColor: theme.colors.action.disabledBackground, + img: { + filter: 'grayscale(100%)', + opacity: 0.33, + }, + }), + applicableInfoButton: css({ + position: 'absolute', + bottom: theme.spacing(1), + right: theme.spacing(1), + }), + tagsWrapper: css({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(0.5), + marginTop: theme.spacing(0.5), + }), +});