SQL Expressions: Add SQL banner (#106865)

* prototype: sql expressions discovery work

* chore: remove compact view work

* chore: cleanup

* chore: moar clean

* chore: use grid instead

* chore: remove no margin + i18n fix

* chore: betterer

* cleanupz

* chore: oh it needs noMargin

* feat: convert to alert

* chore: i18n extract

* i18n

* chore: polish

* fix: change localstorage provider import

* chore: update local storage key name

---------

Co-authored-by: Develer <yeryomin.igor@gmail.com>
This commit is contained in:
Alex Spencer
2025-08-12 12:34:13 -07:00
committed by GitHub
co-authored by Develer
parent 56c91c2222
commit 08501677e3
4 changed files with 108 additions and 68 deletions
-3
View File
@@ -2079,9 +2079,6 @@ exports[`better eslint`] = {
[0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"],
[0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"]
],
"public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
],
"public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
@@ -0,0 +1,35 @@
import { LocalStorageValueProvider } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { Alert, Stack, TextLink } from '@grafana/ui';
const LOCAL_STORAGE_KEY = 'grafana.dashboard.sql-expressions-banner.dismissed';
export const SqlExpressionsBanner = () => (
<LocalStorageValueProvider<boolean> storageKey={LOCAL_STORAGE_KEY} defaultValue={false}>
{(isDismissed, onDismiss) => {
if (isDismissed) {
return null;
}
return (
<Alert
bottomSpacing={1}
title={t('dashboard.transformation-picker-ng.sql-expressions-title', 'SQL Expressions')}
severity="info"
onRemove={() => onDismiss(true)}
>
<Stack direction="column" gap={1}>
<Trans i18nKey="dashboard.transformation-picker-ng.sql-expressions-message-description">
A new way to manipulate and transform the results of data source queries using MySQL-like syntax.
</Trans>
<TextLink
href="https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/sql-expressions/"
external
>
<Trans i18nKey="dashboard.transformation-picker-ng.sql-expressions-message-link">Learn more</Trans>
</TextLink>
</Stack>
</Alert>
);
}}
</LocalStorageValueProvider>
);
@@ -11,10 +11,24 @@ import {
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Badge, Card, Drawer, FilterPill, IconButton, Input, Switch, useStyles2, useTheme2 } from '@grafana/ui';
import {
Badge,
Card,
Drawer,
FilterPill,
Grid,
IconButton,
Input,
Stack,
Switch,
useStyles2,
useTheme2,
} from '@grafana/ui';
import config from 'app/core/config';
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
import { getCategoriesLabels } from 'app/features/transformers/utils';
import { SqlExpressionsBanner } from './SqlExpressions/SqlExpressionsBanner';
import { FilterCategory } from './TransformationsEditor';
const VIEW_ALL_VALUE = 'viewAll';
@@ -72,68 +86,65 @@ export function TransformationPickerNg(props: TransformationPickerNgProps) {
}}
title={t('dashboard.transformation-picker-ng.title-add-another-transformation', 'Add another transformation')}
>
<div className={styles.searchWrapper}>
<Input
data-testid={selectors.components.Transforms.searchInput}
className={styles.searchInput}
value={search ?? ''}
placeholder={t(
'dashboard.transformation-picker-ng.placeholder-search-for-transformation',
'Search for transformation'
)}
onChange={onSearchChange}
onKeyDown={onSearchKeyDown}
suffix={suffix}
ref={searchInputRef}
autoFocus={true}
/>
<div className={styles.showImages}>
<span className={styles.illustationSwitchLabel}>
<Trans i18nKey="dashboard.transformation-picker-ng.show-images">Show images</Trans>
</span>{' '}
<Switch
value={showIllustrations}
onChange={() => onShowIllustrationsChange && onShowIllustrationsChange(!showIllustrations)}
<Stack direction="column" gap={2}>
{config?.featureToggles?.sqlExpressions && <SqlExpressionsBanner />}
<div className={styles.searchWrapper}>
<Input
data-testid={selectors.components.Transforms.searchInput}
className={styles.searchInput}
value={search ?? ''}
placeholder={t(
'dashboard.transformation-picker-ng.placeholder-search-for-transformation',
'Search for transformation'
)}
onChange={onSearchChange}
onKeyDown={onSearchKeyDown}
suffix={suffix}
ref={searchInputRef}
autoFocus={true}
/>
</div>
</div>
<div className={styles.filterWrapper}>
{filterCategoriesLabels.map(([slug, label]) => {
return (
<FilterPill
key={slug}
onClick={() => onSelectedFilterChange && onSelectedFilterChange(slug)}
label={label}
selected={selectedFilter === slug}
<Stack direction="row" alignItems="center" gap={0.5}>
<span className={styles.switchLabel}>
<Trans i18nKey="dashboard.transformation-picker-ng.show-images">Show images</Trans>
</span>
<Switch
value={showIllustrations}
onChange={() => onShowIllustrationsChange && onShowIllustrationsChange(!showIllustrations)}
/>
);
})}
</div>
</Stack>
</div>
<TransformationsGrid
showIllustrations={showIllustrations}
transformations={xforms}
data={data}
onClick={(id) => {
onTransformationAdd({ value: id });
}}
/>
<Stack direction="row" wrap="wrap" rowGap={1} columnGap={0.5}>
{filterCategoriesLabels.map(([slug, label]) => {
return (
<FilterPill
key={slug}
onClick={() => onSelectedFilterChange && onSelectedFilterChange(slug)}
label={label}
selected={selectedFilter === slug}
/>
);
})}
</Stack>
<TransformationsGrid
showIllustrations={showIllustrations}
transformations={xforms}
data={data}
onClick={(id) => {
onTransformationAdd({ value: id });
}}
/>
</Stack>
</Drawer>
);
}
function getTransformationPickerStyles(theme: GrafanaTheme2) {
return {
showImages: css({
flexBasis: '0',
display: 'flex',
gap: '8px',
alignItems: 'center',
}),
pickerInformationLine: css({
fontSize: '16px',
marginBottom: `${theme.spacing(2)}`,
marginBottom: theme.spacing(2),
}),
pickerInformationLineHighlight: css({
verticalAlign: 'middle',
@@ -141,8 +152,8 @@ function getTransformationPickerStyles(theme: GrafanaTheme2) {
searchWrapper: css({
display: 'flex',
flexWrap: 'wrap',
columnGap: '27px',
rowGap: '16px',
columnGap: theme.spacing(2),
rowGap: theme.spacing(1),
width: '100%',
paddingBottom: theme.spacing(1),
}),
@@ -150,16 +161,9 @@ function getTransformationPickerStyles(theme: GrafanaTheme2) {
flexGrow: '1',
width: 'initial',
}),
illustationSwitchLabel: css({
switchLabel: css({
whiteSpace: 'nowrap',
}),
filterWrapper: css({
padding: `${theme.spacing(1)} 0`,
display: 'flex',
flexWrap: 'wrap',
rowGap: `${theme.spacing(1)}`,
columnGap: `${theme.spacing(0.5)}`,
}),
};
}
@@ -175,7 +179,7 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data
const styles = useStyles2(getTransformationGridStyles);
return (
<div className={styles.grid}>
<Grid columns={3} gap={1}>
{transformations.map((transform) => {
// Check to see if the transform
// is applicable to the given data
@@ -208,6 +212,7 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data
data-testid={selectors.components.TransformTab.newTransform(transform.name)}
onClick={() => onClick(transform.id)}
key={transform.id}
noMargin
>
<Card.Heading className={styles.heading}>
<div className={styles.titleRow}>
@@ -242,7 +247,7 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data
</Card>
);
})}
</div>
</Grid>
);
}
@@ -266,7 +271,7 @@ function getTransformationGridStyles(theme: GrafanaTheme2) {
width: '100%',
}),
description: css({
fontSize: '12px',
fontSize: theme.typography.bodySmall.fontSize,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
+3
View File
@@ -5395,6 +5395,9 @@
"transformation-picker-ng": {
"placeholder-search-for-transformation": "Search for transformation",
"show-images": "Show images",
"sql-expressions-message-description": "A new way to manipulate and transform the results of data source queries using MySQL-like syntax.",
"sql-expressions-message-link": "Learn more",
"sql-expressions-title": "SQL Expressions",
"title-add-another-transformation": "Add another transformation",
"view-all": "View all"
},