chore: alex - transformation ui improvements
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { DataFrame, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { Container, ScrollContainer, useStyles2 } from '@grafana/ui';
|
||||
@@ -11,6 +11,7 @@ import { DetailViewHeader } from './DetailViewHeader';
|
||||
import { ExpressionDetailView } from './ExpressionDetailView';
|
||||
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
|
||||
import { QueryDetailView } from './QueryDetailView';
|
||||
import { TransformationPickerView } from './TransformationPickerView';
|
||||
import { TabId, QueryTransformItem } from './types';
|
||||
|
||||
interface DetailViewProps {
|
||||
@@ -19,13 +20,41 @@ interface DetailViewProps {
|
||||
tabs: Array<{ tabId: TabId }>;
|
||||
onRemoveTransform?: (index: number) => void;
|
||||
onToggleTransformVisibility?: (index: number) => void;
|
||||
isAddingTransform?: boolean;
|
||||
onAddTransformation?: (selectedItem: SelectableValue<string>, customOptions?: Record<string, unknown>) => void;
|
||||
onCancelAddTransform?: () => void;
|
||||
transformationData?: DataFrame[];
|
||||
onGoToQueries?: () => void;
|
||||
}
|
||||
|
||||
export const DetailView = memo(
|
||||
({ selectedItem, panel, tabs, onRemoveTransform, onToggleTransformVisibility }: DetailViewProps) => {
|
||||
({
|
||||
selectedItem,
|
||||
panel,
|
||||
tabs,
|
||||
onRemoveTransform,
|
||||
onToggleTransformVisibility,
|
||||
isAddingTransform,
|
||||
onAddTransformation,
|
||||
onCancelAddTransform,
|
||||
transformationData,
|
||||
onGoToQueries,
|
||||
}: DetailViewProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
// Show transformation picker when in add mode
|
||||
if (isAddingTransform && onAddTransformation && onCancelAddTransform) {
|
||||
return (
|
||||
<TransformationPickerView
|
||||
data={transformationData || []}
|
||||
onAddTransformation={onAddTransformation}
|
||||
onCancel={onCancelAddTransform}
|
||||
onGoToQueries={onGoToQueries}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedItem) {
|
||||
return (
|
||||
<div className={styles.emptyState}>
|
||||
@@ -82,7 +111,19 @@ export const DetailView = memo(
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [selectedItem, panel, tabs, styles.emptyState, onRemoveTransform, onToggleTransformVisibility]);
|
||||
}, [
|
||||
selectedItem,
|
||||
panel,
|
||||
tabs,
|
||||
styles.emptyState,
|
||||
onRemoveTransform,
|
||||
onToggleTransformVisibility,
|
||||
isAddingTransform,
|
||||
onAddTransformation,
|
||||
onCancelAddTransform,
|
||||
transformationData,
|
||||
onGoToQueries,
|
||||
]);
|
||||
|
||||
return <div className={styles.container}>{renderContent()}</div>;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
CoreApp,
|
||||
DataFrame,
|
||||
DataQuery,
|
||||
DataSourceInstanceSettings,
|
||||
DataTransformerConfig,
|
||||
DataTransformContext,
|
||||
GrafanaTheme2,
|
||||
standardTransformersRegistry,
|
||||
transformDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { getDataSourceSrv, getTemplateSrv } from '@grafana/runtime';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
Dropdown,
|
||||
FieldValidationMessage,
|
||||
Icon,
|
||||
IconButton,
|
||||
Input,
|
||||
JSONFormatter,
|
||||
Menu,
|
||||
Stack,
|
||||
useStyles2,
|
||||
useTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
import { FALLBACK_DOCS_LINK } from 'app/features/transformers/docs/constants';
|
||||
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
|
||||
@@ -65,6 +74,10 @@ export const DetailViewHeader = ({
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
const [isSavedQueriesDrawerOpen, setIsSavedQueriesDrawerOpen] = useState(false);
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const [showDebug, setShowDebug] = useState(false);
|
||||
const [transformInput, setTransformInput] = useState<DataFrame[]>([]);
|
||||
const [transformOutput, setTransformOutput] = useState<DataFrame[]>([]);
|
||||
|
||||
// Helper to update queries with consistent pattern
|
||||
const updateQueries = useCallback(
|
||||
@@ -293,16 +306,78 @@ export const DetailViewHeader = ({
|
||||
const isTransformDisabled =
|
||||
selectedItem.type === 'transform' && 'disabled' in selectedItem.data && selectedItem.data.disabled;
|
||||
|
||||
// Get transformation display name
|
||||
const transformationName = useMemo(() => {
|
||||
// Get transformation display name and transformer info
|
||||
const transformerInfo = useMemo(() => {
|
||||
if (selectedItem.type === 'transform' && 'id' in selectedItem.data) {
|
||||
const transformId = selectedItem.data.id;
|
||||
const transformer = standardTransformersRegistry.get(transformId);
|
||||
return transformer?.name || transformId;
|
||||
return transformer;
|
||||
}
|
||||
return '';
|
||||
return undefined;
|
||||
}, [selectedItem]);
|
||||
|
||||
const transformationName = transformerInfo?.name || '';
|
||||
|
||||
// Calculate transformation input/output for debug mode
|
||||
useEffect(() => {
|
||||
if (selectedItem.type !== 'transform' || !showDebug || !('disabled' in selectedItem.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the query runner for source data
|
||||
const queryRunner = getQueryRunnerFor(panel);
|
||||
if (!queryRunner) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the source data (before any transformations)
|
||||
const sourceData = queryRunner.state.data;
|
||||
if (!sourceData?.series || sourceData.series.length === 0) {
|
||||
setTransformInput([]);
|
||||
setTransformOutput([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all transformations from the panel's data transformer
|
||||
const $data = panel.state.$data;
|
||||
if (!$data || !('state' in $data) || !('transformations' in $data.state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transformations = $data.state.transformations;
|
||||
if (!Array.isArray(transformations)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allTransformations: DataTransformerConfig[] = transformations;
|
||||
const currentIndex = selectedItem.index;
|
||||
|
||||
// Get transformations before and including current one
|
||||
const inputTransforms = allTransformations.slice(0, currentIndex);
|
||||
const outputTransforms = allTransformations.slice(currentIndex, currentIndex + 1);
|
||||
|
||||
const ctx: DataTransformContext = {
|
||||
interpolate: (v: string) => getTemplateSrv().replace(v),
|
||||
};
|
||||
|
||||
// Input: Apply all transformations before this one to the source data
|
||||
const inputSubscription = transformDataFrame(inputTransforms, sourceData.series, ctx).subscribe((frames) => {
|
||||
setTransformInput(frames);
|
||||
});
|
||||
|
||||
// Output: Apply input transforms, then apply the current transform to get the output
|
||||
const outputSubscription = transformDataFrame(inputTransforms, sourceData.series, ctx)
|
||||
.pipe(mergeMap((before) => transformDataFrame(outputTransforms, before, ctx)))
|
||||
.subscribe((frames) => {
|
||||
setTransformOutput(frames);
|
||||
});
|
||||
|
||||
return () => {
|
||||
inputSubscription.unsubscribe();
|
||||
outputSubscription.unsubscribe();
|
||||
};
|
||||
}, [selectedItem, showDebug, panel]);
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerContent}>
|
||||
@@ -422,6 +497,18 @@ export const DetailViewHeader = ({
|
||||
{/* Right side: Actions Menu for transformations */}
|
||||
{selectedItem.type === 'transform' && (
|
||||
<Stack gap={0.5} alignItems="center">
|
||||
<IconButton
|
||||
name="question-circle"
|
||||
variant="secondary"
|
||||
tooltip={t('dashboard-scene.detail-view-header.show-documentation', 'Show documentation')}
|
||||
onClick={() => setShowHelp(true)}
|
||||
/>
|
||||
<IconButton
|
||||
name="bug"
|
||||
variant="secondary"
|
||||
tooltip={t('dashboard-scene.detail-view-header.debug', 'Debug transformation')}
|
||||
onClick={() => setShowDebug(!showDebug)}
|
||||
/>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
@@ -471,6 +558,51 @@ export const DetailViewHeader = ({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Transformation Help Drawer */}
|
||||
{selectedItem.type === 'transform' && transformerInfo && showHelp && (
|
||||
<Drawer
|
||||
title={transformerInfo.name}
|
||||
subtitle={t('dashboard-scene.detail-view-header.transformation-help', 'Transformation help')}
|
||||
onClose={() => setShowHelp(false)}
|
||||
>
|
||||
<OperationRowHelp
|
||||
markdown={transformerInfo.help || FALLBACK_DOCS_LINK}
|
||||
styleOverrides={{ borderTop: '2px solid' }}
|
||||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
{/* Transformation Debug Drawer */}
|
||||
{selectedItem.type === 'transform' && showDebug && (
|
||||
<Drawer
|
||||
title={t('dashboard-scene.detail-view-header.debug-transformation', 'Debug transformation')}
|
||||
subtitle={transformationName}
|
||||
onClose={() => setShowDebug(false)}
|
||||
>
|
||||
<div className={styles.debugWrapper}>
|
||||
<div className={styles.debug}>
|
||||
<div className={styles.debugTitle}>
|
||||
<Trans i18nKey="dashboard-scene.detail-view-header.input-data">Input data</Trans>
|
||||
</div>
|
||||
<div className={styles.debugJson}>
|
||||
<JSONFormatter json={transformInput} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.debugSeparator}>
|
||||
<Icon name="arrow-right" />
|
||||
</div>
|
||||
<div className={styles.debug}>
|
||||
<div className={styles.debugTitle}>
|
||||
<Trans i18nKey="dashboard-scene.detail-view-header.output-data">Output data</Trans>
|
||||
</div>
|
||||
<div className={styles.debugJson}>
|
||||
<JSONFormatter json={transformOutput} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -493,6 +625,7 @@ const getStyles = (theme: GrafanaTheme2, config: { color: string }) => {
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(2),
|
||||
height: '100%',
|
||||
paddingLeft: theme.spacing(1),
|
||||
}),
|
||||
icon: css({
|
||||
color: theme.colors.text.secondary,
|
||||
@@ -548,5 +681,46 @@ const getStyles = (theme: GrafanaTheme2, config: { color: string }) => {
|
||||
color: theme.colors.text.primary,
|
||||
fontSize: theme.typography.body.fontSize,
|
||||
}),
|
||||
debugWrapper: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}),
|
||||
debugSeparator: css({
|
||||
width: '48px',
|
||||
minHeight: '300px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
justifyContent: 'center',
|
||||
margin: `0 ${theme.spacing(0.5)}`,
|
||||
color: theme.colors.primary.text,
|
||||
}),
|
||||
debugTitle: css({
|
||||
padding: `${theme.spacing(1)} ${theme.spacing(0.25)}`,
|
||||
fontFamily: theme.typography.fontFamilyMonospace,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
color: theme.colors.text.primary,
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
}),
|
||||
debug: css({
|
||||
marginTop: theme.spacing(1),
|
||||
padding: `0 ${theme.spacing(1, 1, 1)}`,
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
background: `${theme.isLight ? theme.v1.palette.white : theme.v1.palette.gray05}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
width: '100%',
|
||||
minHeight: '300px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignSelf: 'stretch',
|
||||
}),
|
||||
debugJson: css({
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
padding: theme.spacing(0.5),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
+3
-1
@@ -16,6 +16,7 @@ interface EmptyTransformationsProps {
|
||||
onShowPicker: () => void;
|
||||
onGoToQueries?: () => void;
|
||||
onAddTransformation?: (transformationId: string) => void;
|
||||
showIllustrations?: boolean;
|
||||
}
|
||||
|
||||
const TRANSFORMATION_IDS = [
|
||||
@@ -60,6 +61,7 @@ export function LegacyEmptyTransformationsMessage({ onShowPicker }: { onShowPick
|
||||
export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps) {
|
||||
const hasGoToQueries = props.onGoToQueries != null;
|
||||
const hasAddTransformation = props.onAddTransformation != null;
|
||||
const showIllustrations = props.showIllustrations ?? true;
|
||||
|
||||
// Get transformations from registry
|
||||
const transformations = useMemo(() => {
|
||||
@@ -118,7 +120,7 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps)
|
||||
key={transform.id}
|
||||
transform={transform}
|
||||
onClick={handleTransformationClick}
|
||||
showIllustrations={true}
|
||||
showIllustrations={showIllustrations}
|
||||
showPluginState={false}
|
||||
showTags={false}
|
||||
/>
|
||||
|
||||
@@ -100,6 +100,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
open: false,
|
||||
index: null,
|
||||
});
|
||||
const [isAddingTransform, setIsAddingTransform] = useState(false);
|
||||
|
||||
const panel = panelRef.resolve();
|
||||
|
||||
@@ -197,6 +198,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
|
||||
const handleSelect = useCallback((id: string) => {
|
||||
setSelectedId(id);
|
||||
setIsAddingTransform(false);
|
||||
}, []);
|
||||
|
||||
const updateQuerySelectionOnStateChange = useCallback(
|
||||
@@ -236,6 +238,16 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
[queriesTab, updateQuerySelectionOnStateChange, queries]
|
||||
);
|
||||
|
||||
const handleGoToQueries = useCallback(() => {
|
||||
// Close the transformation picker
|
||||
setIsAddingTransform(false);
|
||||
// Add a SQL expression
|
||||
if (queriesTab) {
|
||||
updateQuerySelectionOnStateChange(queries?.length ?? 0);
|
||||
queriesTab.onAddExpressionOfType(ExpressionQueryType.sql);
|
||||
}
|
||||
}, [queriesTab, updateQuerySelectionOnStateChange, queries]);
|
||||
|
||||
const handleDuplicateQuery = useCallback(
|
||||
(index: number) => {
|
||||
if (queryRunner && queriesTab) {
|
||||
@@ -343,6 +355,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
|
||||
setSelectedId(!!newTransform ? transformItemId(selectedIndex) : null);
|
||||
setTransformDrawerState({ open: false, index: null });
|
||||
setIsAddingTransform(false);
|
||||
unsub.unsubscribe();
|
||||
});
|
||||
|
||||
@@ -429,7 +442,10 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
onSelect={handleSelect}
|
||||
onAddQuery={handleAddQuery}
|
||||
onAddFromSavedQueries={(index) => setSavedQueriesDrawerState({ open: true, index: index ?? null })}
|
||||
onAddTransform={(index) => setTransformDrawerState({ open: true, index: index ?? null })}
|
||||
onAddTransform={(index) => {
|
||||
setIsAddingTransform(true);
|
||||
setSelectedId(null);
|
||||
}}
|
||||
onAddExpression={handleAddExpression}
|
||||
onDuplicateQuery={handleDuplicateQuery}
|
||||
onRemoveQuery={handleRemoveQuery}
|
||||
@@ -470,6 +486,11 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
tabs={tabs}
|
||||
onRemoveTransform={handleRemoveTransform}
|
||||
onToggleTransformVisibility={handleToggleTransformVisibility}
|
||||
isAddingTransform={isAddingTransform}
|
||||
onAddTransformation={handleAddTransform}
|
||||
onCancelAddTransform={() => setIsAddingTransform(false)}
|
||||
transformationData={series}
|
||||
onGoToQueries={handleGoToQueries}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { FormEvent, useMemo, useState } from 'react';
|
||||
|
||||
import { DataFrame, GrafanaTheme2, SelectableValue, standardTransformersRegistry } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Box, FilterPill, Grid, IconButton, Input, ScrollContainer, Stack, Switch, useStyles2 } from '@grafana/ui';
|
||||
import { getCategoriesLabels } from 'app/features/transformers/utils';
|
||||
|
||||
import { TransformationCard } from '../../../dashboard/components/TransformationsEditor/TransformationCard';
|
||||
import { FilterCategory } from '../../../dashboard/components/TransformationsEditor/TransformationsEditor';
|
||||
|
||||
import { NewEmptyTransformationsMessage } from './EmptyTransformationsMessage';
|
||||
|
||||
const VIEW_ALL_VALUE = 'viewAll';
|
||||
|
||||
interface TransformationPickerViewProps {
|
||||
data: DataFrame[];
|
||||
onAddTransformation: (selectedItem: SelectableValue<string>, customOptions?: Record<string, unknown>) => void;
|
||||
onCancel: () => void;
|
||||
onGoToQueries?: () => void;
|
||||
}
|
||||
|
||||
export function TransformationPickerView({
|
||||
data,
|
||||
onAddTransformation,
|
||||
onCancel,
|
||||
onGoToQueries,
|
||||
}: TransformationPickerViewProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [search, setSearch] = useState('');
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const [showIllustrations, setShowIllustrations] = useState(false);
|
||||
const [selectedFilter, setSelectedFilter] = useState<FilterCategory>(VIEW_ALL_VALUE);
|
||||
|
||||
const allTransformations = useMemo(
|
||||
() => standardTransformersRegistry.list().sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0)),
|
||||
[]
|
||||
);
|
||||
|
||||
const filterCategoriesLabels: Array<[FilterCategory, string]> = useMemo(
|
||||
() => [
|
||||
[VIEW_ALL_VALUE, t('dashboard.transformation-picker-ng.view-all', 'View all')],
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
...(Object.entries(getCategoriesLabels()) as Array<[FilterCategory, string]>),
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const transformations = allTransformations.filter((t) => {
|
||||
// Filter by category
|
||||
if (selectedFilter && selectedFilter !== VIEW_ALL_VALUE && !t.categories?.has(selectedFilter)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by search
|
||||
const searchLower = search.toLocaleLowerCase();
|
||||
const textMatch =
|
||||
t.name.toLocaleLowerCase().includes(searchLower) || t.description?.toLocaleLowerCase().includes(searchLower);
|
||||
const tagMatch = t.tags?.size
|
||||
? Array.from(t.tags).some((tag) => tag.toLocaleLowerCase().includes(searchLower))
|
||||
: false;
|
||||
return textMatch || tagMatch;
|
||||
});
|
||||
|
||||
const onSearchChange = (e: FormEvent<HTMLInputElement>) => setSearch(e.currentTarget.value);
|
||||
|
||||
const handleAddTransformation = (transformationId: string) => {
|
||||
reportInteraction('grafana_panel_transformations_clicked', {
|
||||
type: transformationId,
|
||||
context: 'transformation_picker_view',
|
||||
});
|
||||
onAddTransformation({ value: transformationId });
|
||||
};
|
||||
|
||||
const searchBoxSuffix = search ? (
|
||||
<>
|
||||
{transformations.length} / {allTransformations.length}
|
||||
<IconButton
|
||||
name="times"
|
||||
onClick={() => setSearch('')}
|
||||
tooltip={t('dashboard-scene.transformation-picker-view.clear-search', 'Clear search')}
|
||||
/>
|
||||
</>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Stack direction="row" gap={1} alignItems="center" justifyContent="space-between">
|
||||
<h3 className={styles.title}>
|
||||
<Trans i18nKey="dashboard-scene.transformation-picker-view.title">Add transformation</Trans>
|
||||
</h3>
|
||||
<IconButton
|
||||
name="times"
|
||||
onClick={onCancel}
|
||||
tooltip={t('dashboard-scene.transformation-picker-view.close', 'Close')}
|
||||
size="lg"
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
{showAll && (
|
||||
<>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchWrapper}>
|
||||
<Input
|
||||
value={search}
|
||||
onChange={onSearchChange}
|
||||
placeholder={t(
|
||||
'dashboard-scene.transformation-picker-view.search-placeholder',
|
||||
'Search for transformation'
|
||||
)}
|
||||
suffix={search ? searchBoxSuffix : undefined}
|
||||
autoFocus
|
||||
data-testid={selectors.components.Transforms.searchInput}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
<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={() => setShowIllustrations(!showIllustrations)} />
|
||||
</Stack>
|
||||
</div>
|
||||
<Stack direction="row" wrap="wrap" rowGap={1} columnGap={0.5}>
|
||||
{filterCategoriesLabels.map(([slug, label]) => (
|
||||
<FilterPill
|
||||
key={slug}
|
||||
onClick={() => setSelectedFilter(slug)}
|
||||
label={label}
|
||||
selected={selectedFilter === slug}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<ScrollContainer>
|
||||
<Box padding={2}>
|
||||
{showAll ? (
|
||||
// Show all transformations when "show more" clicked
|
||||
transformations.length === 0 ? (
|
||||
<div className={styles.noResults}>
|
||||
<p>
|
||||
<Trans i18nKey="dashboard-scene.transformation-picker-view.no-results">
|
||||
No transformations found
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Grid columns={3} gap={1}>
|
||||
{transformations.map((transform) => (
|
||||
<TransformationCard
|
||||
key={transform.id}
|
||||
transform={transform}
|
||||
showIllustrations={showIllustrations}
|
||||
showPluginState={false}
|
||||
showTags={true}
|
||||
onClick={handleAddTransformation}
|
||||
data={data}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
) : (
|
||||
// Show empty state with featured transformations when not searching
|
||||
<NewEmptyTransformationsMessage
|
||||
onShowPicker={() => setShowAll(true)}
|
||||
onAddTransformation={handleAddTransformation}
|
||||
onGoToQueries={onGoToQueries}
|
||||
showIllustrations={true}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
background: theme.colors.background.primary,
|
||||
}),
|
||||
header: css({
|
||||
padding: theme.spacing(2, 2, 0, 2),
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
paddingBottom: theme.spacing(2),
|
||||
}),
|
||||
title: css({
|
||||
margin: 0,
|
||||
fontSize: theme.typography.h4.fontSize,
|
||||
fontWeight: theme.typography.h4.fontWeight,
|
||||
}),
|
||||
searchContainer: css({
|
||||
padding: theme.spacing(2),
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
}),
|
||||
searchWrapper: css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
columnGap: theme.spacing(2),
|
||||
rowGap: theme.spacing(1),
|
||||
width: '100%',
|
||||
paddingBottom: theme.spacing(1),
|
||||
}),
|
||||
searchInput: css({
|
||||
flexGrow: 1,
|
||||
width: 'initial',
|
||||
}),
|
||||
switchLabel: css({
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
noResults: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '200px',
|
||||
color: theme.colors.text.secondary,
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
}),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user