Accessibility: Ensure dashboard edit panel inputs have accessible labels (again) (#110163)
* Revert "Revert: "Accessibility: Ensure dashboard edit panel inputs have accessible labels" (#109984)" This reverts commit7331a2e8c3. * revert test change that accidentally catches the issue * use useId in some places * make ID required * fix some ids * fix a couple more * add ids to variables Co-authored-by: Ashley Harrison <ashharrison90@gmail.com> * add ids to get viz options * add ids in getPanelFrameOptions * add getFieldOverrideElements * change name * Replace other uuids with hard coded ids * hoist useId out * use some new selectors for table e2es * commit betterer crimes * use useId where we can * Revert "use useId where we can" This reverts commit34090ac75d. * fix some dashboard layouts tests * rm AutoCellOptionsEditor * idk try and fix tests * restore fixed its for url state --------- Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> Co-authored-by: Ashley Harrison <ashharrison90@gmail.com> Co-authored-by: Paul Marbach <paul.marbach@grafana.com>
This commit is contained in:
co-authored by
Ashley Harrison
Ashley Harrison
Paul Marbach
parent
85c567609d
commit
67b22177a8
+24
-10
@@ -1702,15 +1702,21 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "React Hook \\"useSnappingSplitter\\" is called conditionally. React Hooks must be called in the exact same order in every component render.", "3"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx:5381": [
|
||||
[0, 0, 0, "React Hook \\"useLayoutCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"]
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useLayoutCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx:5381": [
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"]
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "5"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
|
||||
@@ -1884,18 +1890,26 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "9"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/settings/variables/LocalVariableEditableElement.tsx:5381": [
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"]
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
|
||||
[0, 0, 0, "React Hook \\"useLocalVariableOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"],
|
||||
[0, 0, 0, "React Hook \\"useVariableSelectionOptionsCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"],
|
||||
[0, 0, 0, "React Hook \\"useVariableTypeCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"]
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "3"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "4"],
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "5"],
|
||||
[0, 0, 0, "React Hook \\"useLocalVariableOptions\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "6"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "7"],
|
||||
[0, 0, 0, "React Hook \\"useVariableSelectionOptionsCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "8"],
|
||||
[0, 0, 0, "React Hook \\"useVariableTypeCategory\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "9"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/settings/variables/VariableSetEditableElement.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"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"]
|
||||
[0, 0, 0, "React Hook \\"useId\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "1"],
|
||||
[0, 0, 0, "React Hook \\"useMemo\\" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function.", "2"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx:5381": [
|
||||
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
|
||||
|
||||
@@ -406,9 +406,7 @@ test.describe(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
|
||||
|
||||
// Expand layouts section
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Group layout category').click();
|
||||
|
||||
// Select tabs layout
|
||||
await page.getByLabel('Tabs').click();
|
||||
@@ -695,9 +693,7 @@ test.describe(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true });
|
||||
|
||||
// Expand layouts section
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Group layout category').click();
|
||||
|
||||
// Select rows layout
|
||||
await page.getByLabel('Rows').click();
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe(
|
||||
const initialBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background);
|
||||
expect(initialBackground).not.toMatch(/rgba\(0, 0, 0, 0\)/);
|
||||
|
||||
await page.locator('#transparent-background').click({ force: true });
|
||||
await page.getByRole('switch', { name: 'Transparent background' }).click({ force: true });
|
||||
|
||||
const transparentBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background);
|
||||
expect(transparentBackground).toMatch(/rgba\(0, 0, 0, 0\)/);
|
||||
|
||||
@@ -31,9 +31,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -64,9 +62,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -126,9 +122,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -185,9 +179,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -222,9 +214,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -279,9 +269,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
@@ -338,9 +326,7 @@ test.describe(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
|
||||
).toHaveCount(3);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category'))
|
||||
.click();
|
||||
await page.getByLabel('Expand Panel layout category').click();
|
||||
|
||||
await page.getByLabel('Auto grid').click();
|
||||
|
||||
|
||||
@@ -75,11 +75,9 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||
// text wrapping is enabled by default on this panel.
|
||||
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeGreaterThan(100);
|
||||
|
||||
// FIXME very bad selector to get the correct "wrap text" toggle here.
|
||||
// toggle the lorem ipsum column's wrap text toggle and confirm that the height shrinks.
|
||||
await page
|
||||
.locator('[id="Override 13"]')
|
||||
.locator(`[aria-label="${selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text')}"]`)
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.OptionsGroup.group('panel-options-override-12'))
|
||||
.getByLabel(selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text'))
|
||||
.click();
|
||||
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
|
||||
|
||||
@@ -95,8 +93,8 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Cell options Cell value inspect'))
|
||||
.first()
|
||||
.locator('label[for="custom.inspect"]')
|
||||
.click();
|
||||
.getByRole('switch', { name: 'Cell value inspect' })
|
||||
.click({ force: true });
|
||||
await loremIpsumCell.hover();
|
||||
await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100);
|
||||
|
||||
@@ -117,19 +115,19 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table']
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink'))
|
||||
).toBeVisible();
|
||||
|
||||
const stateOverride = dashboardPage.getByGrafanaSelector(
|
||||
selectors.components.OptionsGroup.group('panel-options-override-11')
|
||||
);
|
||||
|
||||
// confirm that "State" column is hidden by default.
|
||||
expect(page.getByRole('row').nth(0)).not.toContainText('State');
|
||||
|
||||
// toggle the "State" column visibility and test that it appears before re-hiding it.
|
||||
// FIXME this selector is utterly godawful, but there's no way to give testIds or aria-labels or anything to
|
||||
// the panel editor builder. we should fix that to make e2e's easier to write for our team.
|
||||
const hideStateColumnSwitch = page.locator('[id="Override 12"]').locator('label').last();
|
||||
// toggle the "State" column visibility via the override we set up in the kitchen sink panel.
|
||||
const hideStateColumnSwitch = stateOverride.locator('label').last();
|
||||
await hideStateColumnSwitch.click();
|
||||
expect(page.getByRole('row').nth(0)).toContainText('State');
|
||||
|
||||
// now change the display name of the "State" column.
|
||||
// FIXME it would be good to have a better selector here too.
|
||||
const displayNameInput = page.locator('[id="Override 12"]').locator('input[value="State"]').last();
|
||||
const displayNameInput = stateOverride.locator('input[value="State"]').last();
|
||||
await displayNameInput.fill('State (renamed)');
|
||||
await displayNameInput.press('Enter');
|
||||
expect(page.getByRole('row').nth(0)).toContainText('State (renamed)');
|
||||
|
||||
@@ -37,7 +37,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
pickerTriggerRef = createRef<any>();
|
||||
|
||||
render() {
|
||||
const { theme, children, onChange, color } = this.props;
|
||||
const { theme, children, onChange, color, id } = this.props;
|
||||
const styles = getStyles(theme);
|
||||
const popoverElement = React.createElement(popover, {
|
||||
...{ ...this.props, children: null },
|
||||
@@ -67,6 +67,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
})
|
||||
) : (
|
||||
<ColorSwatch
|
||||
id={id}
|
||||
ref={this.pickerTriggerRef}
|
||||
onClick={showPopper}
|
||||
onMouseLeave={hidePopper}
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface ColorPickerProps extends Themeable2 {
|
||||
color: string;
|
||||
onChange: ColorPickerChangeHandler;
|
||||
enableNamedColors?: boolean;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface Props<T> extends ColorPickerProps, PopoverContentProps {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useFieldDisplayNames, useSelectOptions, frameHasName } from './utils';
|
||||
type Props = StandardEditorProps<string, FieldNamePickerConfigSettings>;
|
||||
|
||||
// Pick a field name out of the fields
|
||||
export const FieldNamePicker = ({ value, onChange, context, item }: Props) => {
|
||||
export const FieldNamePicker = ({ value, onChange, context, item, id }: Props) => {
|
||||
const settings: FieldNamePickerConfigSettings = item.settings ?? {};
|
||||
const names = useFieldDisplayNames(context.data, settings?.filter);
|
||||
const selectOptions = useSelectOptions(names, value, undefined, undefined, settings.baseNameMode);
|
||||
@@ -29,6 +29,7 @@ export const FieldNamePicker = ({ value, onChange, context, item }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
inputId={id}
|
||||
value={selectedOption}
|
||||
placeholder={
|
||||
settings.placeholderText ?? t('grafana-ui.matchers-ui.field-name-picker.placeholder', 'Select field')
|
||||
|
||||
@@ -41,10 +41,11 @@ export interface Props {
|
||||
data: DataFrame[];
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
// Not exported globally... but used in grafana core
|
||||
export function RefIDPicker({ value, data, onChange, placeholder }: Props) {
|
||||
export function RefIDPicker({ value, data, onChange, placeholder, id }: Props) {
|
||||
const listOfRefIds = useMemo(() => getListOfQueryRefIds(data), [data]);
|
||||
|
||||
const [priorSelectionState, updatePriorSelectionState] = useState<{
|
||||
@@ -77,6 +78,7 @@ export function RefIDPicker({ value, data, onChange, placeholder }: Props) {
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
inputId={id}
|
||||
options={listOfRefIds}
|
||||
onChange={onFilterChange}
|
||||
isClearable={true}
|
||||
@@ -114,9 +116,10 @@ export interface MultiProps {
|
||||
data: DataFrame[];
|
||||
onChange: (value: string[]) => void;
|
||||
placeholder?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function RefIDMultiPicker({ value, data, onChange, placeholder }: MultiProps) {
|
||||
export function RefIDMultiPicker({ value, data, onChange, placeholder, id }: MultiProps) {
|
||||
const listOfRefIds = useMemo(() => getListOfQueryRefIds(data), [data]);
|
||||
|
||||
const [priorSelectionState, updatePriorSelectionState] = useState<{
|
||||
@@ -172,6 +175,7 @@ export function RefIDMultiPicker({ value, data, onChange, placeholder }: MultiPr
|
||||
}
|
||||
return (
|
||||
<MultiSelect
|
||||
inputId={id}
|
||||
options={listOfRefIds}
|
||||
onChange={onFilterChange}
|
||||
isClearable={true}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface UnitPickerProps {
|
||||
onChange: (item?: string) => void;
|
||||
value?: string;
|
||||
width?: number;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
function formatCreateLabel(input: string) {
|
||||
@@ -21,7 +22,7 @@ export class UnitPicker extends PureComponent<UnitPickerProps> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { value, width } = this.props;
|
||||
const { value, width, id } = this.props;
|
||||
|
||||
// Set the current selection
|
||||
let current: SelectableValue<string> | undefined = undefined;
|
||||
@@ -56,6 +57,7 @@ export class UnitPicker extends PureComponent<UnitPickerProps> {
|
||||
|
||||
return (
|
||||
<Cascader
|
||||
id={id}
|
||||
width={width}
|
||||
initialValue={current && current.label}
|
||||
allowCustomValue
|
||||
|
||||
@@ -47,6 +47,9 @@ export interface NestedFolderPickerProps {
|
||||
|
||||
/* Whether the picker should be clearable */
|
||||
clearable?: boolean;
|
||||
|
||||
/* HTML ID for the button element for form labels */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const debouncedSearch = debounce(getSearchResults, 300);
|
||||
@@ -72,6 +75,7 @@ export function NestedFolderPicker({
|
||||
rootFolderUID,
|
||||
permission = 'edit',
|
||||
onChange,
|
||||
id,
|
||||
}: NestedFolderPickerProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const selectedFolder = useGetFolderQueryFacade(value);
|
||||
@@ -287,6 +291,7 @@ export function NestedFolderPicker({
|
||||
if (!overlayOpen) {
|
||||
return (
|
||||
<Trigger
|
||||
id={id}
|
||||
label={labelComponent}
|
||||
handleClearSelection={clearable && value !== undefined ? handleClearSelection : undefined}
|
||||
invalid={invalid}
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as React from 'react';
|
||||
import { Field, Input } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
value?: number;
|
||||
placeholder?: string;
|
||||
autoFocus?: boolean;
|
||||
@@ -101,6 +102,7 @@ export class NumberInput extends PureComponent<Props, State> {
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
id={this.props.id}
|
||||
ref={this.inputRef}
|
||||
min={this.props.min}
|
||||
max={this.props.max}
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface ColorValueEditorSettings {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
value?: string;
|
||||
onChange: (value: string | undefined) => void;
|
||||
settings?: ColorValueEditorSettings;
|
||||
@@ -25,7 +26,7 @@ interface Props {
|
||||
/**
|
||||
* @alpha
|
||||
* */
|
||||
export const ColorValueEditor = ({ value, settings, onChange, details }: Props) => {
|
||||
export const ColorValueEditor = ({ value, settings, onChange, details, id }: Props) => {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@@ -37,6 +38,7 @@ export const ColorValueEditor = ({ value, settings, onChange, details }: Props)
|
||||
<div className={styles.colorPicker}>
|
||||
<ColorSwatch
|
||||
ref={ref}
|
||||
id={id}
|
||||
onClick={showColorPicker}
|
||||
onMouseLeave={hideColorPicker}
|
||||
color={value ? theme.visualization.getColorByName(value) : theme.components.input.borderColor}
|
||||
|
||||
@@ -55,11 +55,12 @@ export class MultiSelectValueEditor<T> extends PureComponent<Props<T>, State<T>>
|
||||
|
||||
render() {
|
||||
const { options, isLoading } = this.state;
|
||||
const { value, onChange, item } = this.props;
|
||||
const { value, onChange, item, id } = this.props;
|
||||
|
||||
const { settings } = item;
|
||||
return (
|
||||
<MultiSelect<T>
|
||||
inputId={id}
|
||||
isLoading={isLoading}
|
||||
value={value}
|
||||
defaultValue={value}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NumberInput } from './NumberInput';
|
||||
|
||||
type Props = StandardEditorProps<number, NumberFieldConfigSettings>;
|
||||
|
||||
export const NumberValueEditor = ({ value, onChange, item }: Props) => {
|
||||
export const NumberValueEditor = ({ value, onChange, item, id }: Props) => {
|
||||
const { settings } = item;
|
||||
|
||||
const onValueChange = useCallback(
|
||||
@@ -18,6 +18,7 @@ export const NumberValueEditor = ({ value, onChange, item }: Props) => {
|
||||
|
||||
return (
|
||||
<NumberInput
|
||||
id={id}
|
||||
value={value}
|
||||
min={settings?.min}
|
||||
max={settings?.max}
|
||||
|
||||
@@ -51,7 +51,7 @@ export class SelectValueEditor<T> extends PureComponent<Props<T>, State<T>> {
|
||||
|
||||
render() {
|
||||
const { options, isLoading } = this.state;
|
||||
const { value, onChange, item } = this.props;
|
||||
const { value, onChange, item, id } = this.props;
|
||||
|
||||
const { settings } = item;
|
||||
let current = options.find((v) => v.value === value);
|
||||
@@ -63,6 +63,7 @@ export class SelectValueEditor<T> extends PureComponent<Props<T>, State<T>> {
|
||||
}
|
||||
return (
|
||||
<Select<T>
|
||||
inputId={id}
|
||||
isLoading={isLoading}
|
||||
value={current}
|
||||
defaultValue={value}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { NumberInput } from './NumberInput';
|
||||
|
||||
type Props = StandardEditorProps<number, SliderFieldConfigSettings>;
|
||||
|
||||
export const SliderValueEditor = ({ value, onChange, item }: Props) => {
|
||||
export const SliderValueEditor = ({ value, onChange, item, id }: Props) => {
|
||||
// Input reference
|
||||
const inputRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
@@ -109,7 +109,7 @@ export const SliderValueEditor = ({ value, onChange, item }: Props) => {
|
||||
included={included}
|
||||
/>
|
||||
<span className={stylesSlider.numberInputWrapper} ref={inputRef}>
|
||||
<NumberInput value={sliderValue} onChange={onSliderInputChange} max={max} min={min} step={step} />
|
||||
<NumberInput id={id} value={sliderValue} onChange={onSliderInputChange} max={max} min={min} step={step} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props extends StandardEditorProps<string, StringFieldConfigSettings> {
|
||||
suffix?: ReactNode;
|
||||
}
|
||||
|
||||
export const StringValueEditor = ({ value, onChange, item, suffix }: Props) => {
|
||||
export const StringValueEditor = ({ value, onChange, item, suffix, id }: Props) => {
|
||||
const Component = item.settings?.useTextarea ? TextArea : Input;
|
||||
const onValueChange = useCallback(
|
||||
(
|
||||
@@ -36,6 +36,7 @@ export const StringValueEditor = ({ value, onChange, item, suffix }: Props) => {
|
||||
|
||||
return (
|
||||
<Component
|
||||
id={id}
|
||||
placeholder={item.settings?.placeholder}
|
||||
defaultValue={value || ''}
|
||||
rows={(item.settings?.useTextarea && item.settings.rows) || 5}
|
||||
|
||||
@@ -6,14 +6,14 @@ import { IconButton, UnitPicker, useStyles2 } from '@grafana/ui';
|
||||
|
||||
type Props = StandardEditorProps<string, UnitFieldConfigSettings>;
|
||||
|
||||
export function UnitValueEditor({ value, onChange, item }: Props) {
|
||||
export function UnitValueEditor({ value, onChange, item, id }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (item?.settings?.isClearable && value != null) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<span className={styles.first}>
|
||||
<UnitPicker value={value} onChange={onChange} />
|
||||
<UnitPicker value={value} onChange={onChange} id={id} />
|
||||
</span>
|
||||
<IconButton
|
||||
name="times"
|
||||
@@ -23,7 +23,7 @@ export function UnitValueEditor({ value, onChange, item }: Props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <UnitPicker value={value} onChange={onChange} />;
|
||||
return <UnitPicker value={value} onChange={onChange} id={id} />;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ export function useConditionalRenderingEditor(
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title,
|
||||
id: 'conditional-rendering-options-item',
|
||||
render: () => <conditionalRenderingToRender.Component model={conditionalRenderingToRender} />,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useMemo, useRef } from 'react';
|
||||
import { ReactNode, useId, useMemo, useRef } from 'react';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
@@ -36,10 +36,10 @@ export class DashboardEditableElement implements EditableDashboardElement {
|
||||
|
||||
// When layout changes we need to update options list
|
||||
const { body } = dashboard.useState();
|
||||
const dashboardTitleInputId = useId();
|
||||
const dashboardDescriptionInputId = useId();
|
||||
|
||||
const dashboardOptions = useMemo(() => {
|
||||
const dashboardTitleInputId = 'dashboard-title-input';
|
||||
const dashboardDescriptionInputId = 'dashboard-description-input';
|
||||
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ title: '', id: 'dashboard-options' })
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
@@ -57,7 +57,7 @@ export class DashboardEditableElement implements EditableDashboardElement {
|
||||
);
|
||||
|
||||
return editPaneHeaderOptions;
|
||||
}, [dashboard]);
|
||||
}, [dashboard, dashboardDescriptionInputId, dashboardTitleInputId]);
|
||||
|
||||
const layoutCategory = useLayoutCategory(body);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useId, useMemo } from 'react';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
@@ -42,19 +42,24 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
|
||||
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
|
||||
const panel = this.panel;
|
||||
const layoutElement = panel.parent!;
|
||||
const rootId = useId();
|
||||
const titleId = useId();
|
||||
const descriptionId = useId();
|
||||
const backgroundId = useId();
|
||||
|
||||
const panelOptions = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({ title: '', id: 'panel-options' })
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: rootId,
|
||||
render: () => <OpenPanelEditViz panel={this.panel} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.viz-panel.options.title-option', 'Title'),
|
||||
id: 'PanelFrameTitle',
|
||||
id: titleId,
|
||||
value: panel.state.title,
|
||||
popularRank: 1,
|
||||
render: (descriptor) => (
|
||||
@@ -65,7 +70,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.viz-panel.options.description', 'Description'),
|
||||
id: 'description-text-area',
|
||||
id: descriptionId,
|
||||
value: panel.state.description,
|
||||
render: (descriptor) => <PanelDescriptionTextArea id={descriptor.props.id} panel={panel} />,
|
||||
})
|
||||
@@ -73,11 +78,11 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.viz-panel.options.transparent-background', 'Transparent background'),
|
||||
id: 'transparent-background',
|
||||
id: backgroundId,
|
||||
render: (descriptor) => <PanelBackgroundSwitch id={descriptor.props.id} panel={panel} />,
|
||||
})
|
||||
);
|
||||
}, [panel, isNewElement]);
|
||||
}, [rootId, titleId, panel, descriptionId, backgroundId, isNewElement]);
|
||||
|
||||
const layoutCategories = useMemo(
|
||||
() => (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions ? layoutElement.getOptions() : []),
|
||||
@@ -133,9 +138,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
|
||||
}
|
||||
}
|
||||
|
||||
type OpenPanelEditVizProps = {
|
||||
panel: VizPanel;
|
||||
};
|
||||
type OpenPanelEditVizProps = { panel: VizPanel };
|
||||
|
||||
const OpenPanelEditViz = ({ panel }: OpenPanelEditVizProps) => {
|
||||
return (
|
||||
|
||||
@@ -37,7 +37,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard-scene.get-panel-frame-options.title.title', 'Title'),
|
||||
id: 'PanelFrameTitle',
|
||||
id: 'panel-frame-options-title',
|
||||
value: panel.state.title,
|
||||
popularRank: 1,
|
||||
render: function renderTitle(descriptor) {
|
||||
@@ -55,7 +55,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard-scene.get-panel-frame-options.title.description', 'Description'),
|
||||
id: 'description-text-area',
|
||||
id: 'panel-frame-options-description',
|
||||
value: panel.state.description,
|
||||
render: function renderDescription(descriptor) {
|
||||
return <PanelDescriptionTextArea id={descriptor.props.id} panel={panel} />;
|
||||
@@ -71,7 +71,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard-scene.get-panel-frame-options.title.transparent-background', 'Transparent background'),
|
||||
id: 'transparent-background',
|
||||
id: 'panel-frame-options-transparent-bg',
|
||||
render: function renderTransparent(descriptor) {
|
||||
return <PanelBackgroundSwitch id={descriptor.props.id} panel={panel} />;
|
||||
},
|
||||
@@ -86,6 +86,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard-scene.get-panel-frame-options.title.panel-links', 'Panel links'),
|
||||
id: 'panel-frame-options-panel-links',
|
||||
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ export function getOptions(model: AutoGridItem): OptionsPaneCategoryDescriptor[]
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.auto-grid.item-options.repeat.variable.title', 'Repeat by variable'),
|
||||
id: 'repeat-by-variable-select',
|
||||
id: 'auto-grid-repeat-by-variable',
|
||||
description: t(
|
||||
'dashboard.auto-grid.item-options.repeat.variable.description',
|
||||
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.'
|
||||
|
||||
+2
@@ -15,6 +15,7 @@ export function getEditOptions(layoutManager: AutoGridLayoutManager): OptionsPan
|
||||
|
||||
options.push(
|
||||
new OptionsPaneItemDescriptor({
|
||||
id: 'layout-auto-grid-column-options',
|
||||
title: t('dashboard-scene.get-edit-options.title.column-options', 'Column options'),
|
||||
skipField: true,
|
||||
render: () => <GridLayoutColumns layoutManager={layoutManager} />,
|
||||
@@ -23,6 +24,7 @@ export function getEditOptions(layoutManager: AutoGridLayoutManager): OptionsPan
|
||||
|
||||
options.push(
|
||||
new OptionsPaneItemDescriptor({
|
||||
id: 'layout-auto-grid-row-height',
|
||||
title: t('dashboard-scene.get-edit-options.title.row-height-options', 'Row height options'),
|
||||
skipField: true,
|
||||
render: () => <GridLayoutRows layoutManager={layoutManager} />,
|
||||
|
||||
+8
-4
@@ -14,15 +14,16 @@ import { dashboardEditActions } from '../../edit-pane/shared';
|
||||
import { DashboardGridItem } from './DashboardGridItem';
|
||||
|
||||
export function getDashboardGridItemOptions(gridItem: DashboardGridItem): OptionsPaneCategoryDescriptor[] {
|
||||
const categoryId = 'repeat-options';
|
||||
const repeatCategory = new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.default-layout.item-options.repeat.title', 'Repeat options'),
|
||||
id: 'Repeat options',
|
||||
id: categoryId,
|
||||
isOpenDefault: false,
|
||||
})
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.default-layout.item-options.repeat.variable.title', 'Repeat by variable'),
|
||||
id: 'repeat-by-variable-select',
|
||||
id: `${categoryId}-repeat-by-variable`,
|
||||
description: t(
|
||||
'dashboard.default-layout.item-options.repeat.variable.description',
|
||||
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.'
|
||||
@@ -33,6 +34,7 @@ export function getDashboardGridItemOptions(gridItem: DashboardGridItem): Option
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.default-layout.item-options.repeat.direction.title', 'Repeat direction'),
|
||||
id: `${categoryId}-repeat-direction`,
|
||||
useShowIf: () => {
|
||||
const { variableName } = gridItem.useState();
|
||||
return Boolean(variableName);
|
||||
@@ -43,11 +45,12 @@ export function getDashboardGridItemOptions(gridItem: DashboardGridItem): Option
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.default-layout.item-options.repeat.max', 'Max per row'),
|
||||
id: `${categoryId}-max-per-row`,
|
||||
useShowIf: () => {
|
||||
const { variableName, repeatDirection } = gridItem.useState();
|
||||
return Boolean(variableName) && repeatDirection === 'h';
|
||||
},
|
||||
render: () => <MaxPerRowOption gridItem={gridItem} />,
|
||||
render: (descriptor) => <MaxPerRowOption id={descriptor.props.id} gridItem={gridItem} />,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -90,7 +93,7 @@ function RepeatDirectionOption({ gridItem }: OptionComponentProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function MaxPerRowOption({ gridItem }: OptionComponentProps) {
|
||||
function MaxPerRowOption({ gridItem, id }: OptionComponentProps & { id?: string }) {
|
||||
const { maxPerRow } = gridItem.useState();
|
||||
const maxPerRowOptions: Array<SelectableValue<number>> = [2, 3, 4, 6, 8, 12].map((value) => ({
|
||||
label: value.toString(),
|
||||
@@ -99,6 +102,7 @@ function MaxPerRowOption({ gridItem }: OptionComponentProps) {
|
||||
|
||||
return (
|
||||
<Select
|
||||
id={id}
|
||||
options={maxPerRowOptions}
|
||||
value={maxPerRow ?? 4}
|
||||
onChange={(value) => {
|
||||
|
||||
+8
-5
@@ -46,7 +46,8 @@ export class SceneGridRowEditableElement implements EditableDashboardElement, Bu
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.default-layout.row-options.form.title', 'Title'),
|
||||
render: () => <RowTitleInput row={row} />,
|
||||
id: 'row-options-title',
|
||||
render: (descriptor) => <RowTitleInput id={descriptor.props.id} row={row} />,
|
||||
})
|
||||
);
|
||||
}, [row]);
|
||||
@@ -61,7 +62,8 @@ export class SceneGridRowEditableElement implements EditableDashboardElement, Bu
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.default-layout.row-options.repeat.variable.title', 'Variable'),
|
||||
render: () => <RowRepeatSelect row={row} dashboard={dashboard} />,
|
||||
id: 'row-options-repeat-variable',
|
||||
render: (descriptor) => <RowRepeatSelect id={descriptor.props.id} row={row} dashboard={dashboard} />,
|
||||
})
|
||||
);
|
||||
}, [row]);
|
||||
@@ -78,13 +80,13 @@ export class SceneGridRowEditableElement implements EditableDashboardElement, Bu
|
||||
}
|
||||
}
|
||||
|
||||
function RowTitleInput({ row }: { row: SceneGridRow }) {
|
||||
function RowTitleInput({ row, id }: { row: SceneGridRow; id?: string }) {
|
||||
const { title } = row.useState();
|
||||
|
||||
return <Input value={title} onChange={(e) => row.setState({ title: e.currentTarget.value })} />;
|
||||
return <Input id={id} value={title} onChange={(e) => row.setState({ title: e.currentTarget.value })} />;
|
||||
}
|
||||
|
||||
function RowRepeatSelect({ row, dashboard }: { row: SceneGridRow; dashboard: DashboardScene }) {
|
||||
function RowRepeatSelect({ row, dashboard, id }: { row: SceneGridRow; dashboard: DashboardScene; id?: string }) {
|
||||
const { $behaviors, children } = row.useState();
|
||||
let repeatBehavior = $behaviors?.find((b) => b instanceof RowRepeaterBehavior);
|
||||
const vizPanels = useMemo(
|
||||
@@ -104,6 +106,7 @@ function RowRepeatSelect({ row, dashboard }: { row: SceneGridRow; dashboard: Das
|
||||
return (
|
||||
<>
|
||||
<RepeatRowSelect2
|
||||
id={id}
|
||||
sceneContext={dashboard}
|
||||
repeat={repeatBehavior?.state.variableName}
|
||||
onChange={(repeat) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useId, useMemo } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
@@ -21,10 +21,11 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa
|
||||
|
||||
const rowCategory = useMemo(
|
||||
() =>
|
||||
new OptionsPaneCategoryDescriptor({ title: '', id: 'row-options' })
|
||||
new OptionsPaneCategoryDescriptor({ title: '', id: 'dash-row-edit' })
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: 'dash-row-title',
|
||||
skipField: true,
|
||||
render: () => <RowTitleInput row={model} isNewElement={isNewElement} />,
|
||||
})
|
||||
@@ -32,13 +33,15 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.row.fill-screen', 'Fill screen'),
|
||||
render: () => <FillScreenSwitch row={model} />,
|
||||
id: 'dash-row-fill-screen',
|
||||
render: (descriptor) => <FillScreenSwitch id={descriptor.props.id} row={model} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.row.hide-header', 'Hide row header'),
|
||||
render: () => <RowHeaderSwitch row={model} />,
|
||||
id: 'dash-row-hide-header',
|
||||
render: (descriptor) => <RowHeaderSwitch id={descriptor.props.id} row={model} />,
|
||||
})
|
||||
),
|
||||
[model, isNewElement]
|
||||
@@ -48,16 +51,17 @@ export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPa
|
||||
() =>
|
||||
new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.repeat.title', 'Repeat options'),
|
||||
id: 'repeat-options',
|
||||
id: 'dash-row-repeat',
|
||||
isOpenDefault: false,
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Repeat by variable'),
|
||||
id: `dash-row-repeat-by-variable`,
|
||||
description: t(
|
||||
'dashboard.rows-layout.row-options.repeat.variable.description',
|
||||
'Repeat this row for each value in the selected variable.'
|
||||
),
|
||||
render: () => <RowRepeatSelect row={model} />,
|
||||
render: (descriptor) => <RowRepeatSelect id={descriptor.props.id} row={model} />,
|
||||
})
|
||||
),
|
||||
[model]
|
||||
@@ -94,6 +98,7 @@ function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: bool
|
||||
}
|
||||
>
|
||||
<Input
|
||||
id={useId()}
|
||||
ref={ref}
|
||||
title={t('dashboard.rows-layout.row-options.title-option', 'Title')}
|
||||
value={title}
|
||||
@@ -103,19 +108,19 @@ function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: bool
|
||||
);
|
||||
}
|
||||
|
||||
function RowHeaderSwitch({ row }: { row: RowItem }) {
|
||||
function RowHeaderSwitch({ row, id }: { row: RowItem; id?: string }) {
|
||||
const { hideHeader: isHeaderHidden = false } = row.useState();
|
||||
|
||||
return <Switch value={isHeaderHidden} onChange={() => row.onHeaderHiddenToggle()} />;
|
||||
return <Switch id={id} value={isHeaderHidden} onChange={() => row.onHeaderHiddenToggle()} />;
|
||||
}
|
||||
|
||||
function FillScreenSwitch({ row }: { row: RowItem }) {
|
||||
function FillScreenSwitch({ row, id }: { row: RowItem; id?: string }) {
|
||||
const { fillScreen } = row.useState();
|
||||
|
||||
return <Switch value={fillScreen} onChange={() => row.onChangeFillScreen(!fillScreen)} />;
|
||||
return <Switch id={id} value={fillScreen} onChange={() => row.onChangeFillScreen(!fillScreen)} />;
|
||||
}
|
||||
|
||||
function RowRepeatSelect({ row }: { row: RowItem }) {
|
||||
function RowRepeatSelect({ row, id }: { row: RowItem; id?: string }) {
|
||||
const { layout } = row.useState();
|
||||
const dashboard = useDashboard(row);
|
||||
|
||||
@@ -131,6 +136,7 @@ function RowRepeatSelect({ row }: { row: RowItem }) {
|
||||
return (
|
||||
<>
|
||||
<RepeatRowSelect2
|
||||
id={id}
|
||||
sceneContext={dashboard}
|
||||
repeat={row.state.repeatByVariable}
|
||||
onChange={(repeat) => row.onChangeRepeat(repeat)}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
|
||||
import { RowItems } from './RowItems';
|
||||
|
||||
export function getEditOptions(model: RowItems): OptionsPaneCategoryDescriptor[] {
|
||||
const options = new OptionsPaneCategoryDescriptor({ title: '', id: `rows-options` }).addItem(
|
||||
const categoryId = 'rows-options';
|
||||
const options = new OptionsPaneCategoryDescriptor({ title: '', id: categoryId }).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.row.header.title', 'Row header'),
|
||||
id: `${categoryId}-row-header`,
|
||||
render: () => <RowHeaderCheckboxMulti model={model} />,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -24,7 +24,8 @@ export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPa
|
||||
new OptionsPaneCategoryDescriptor({ title: '', id: 'tab-item-options' }).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'),
|
||||
render: () => <TabTitleInput tab={model} isNewElement={isNewElement} />,
|
||||
id: 'tab-options-title',
|
||||
render: (descriptor) => <TabTitleInput id={descriptor.props.id} tab={model} isNewElement={isNewElement} />,
|
||||
})
|
||||
),
|
||||
[model, isNewElement]
|
||||
@@ -39,11 +40,12 @@ export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPa
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.tabs-layout.tab-options.repeat.variable.title', 'Repeat by variable'),
|
||||
id: 'tab-options-repeat-variable',
|
||||
description: t(
|
||||
'dashboard.tabs-layout.tab-options.repeat.variable.description',
|
||||
'Repeat this tab for each value in the selected variable.'
|
||||
),
|
||||
render: () => <TabRepeatSelect tab={model} />,
|
||||
render: (descriptor) => <TabRepeatSelect id={descriptor.props.id} tab={model} />,
|
||||
})
|
||||
),
|
||||
[model]
|
||||
@@ -65,7 +67,7 @@ export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPa
|
||||
return editOptions;
|
||||
}
|
||||
|
||||
function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: boolean }) {
|
||||
function TabTitleInput({ tab, isNewElement, id }: { tab: TabItem; isNewElement: boolean; id?: string }) {
|
||||
const { title } = tab.useState();
|
||||
|
||||
const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement });
|
||||
@@ -79,6 +81,7 @@ function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: bool
|
||||
}
|
||||
>
|
||||
<Input
|
||||
id={id}
|
||||
ref={ref}
|
||||
title={t('dashboard.tabs-layout.tab-options.title-option', 'Title')}
|
||||
value={title}
|
||||
@@ -88,7 +91,7 @@ function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: bool
|
||||
);
|
||||
}
|
||||
|
||||
function TabRepeatSelect({ tab }: { tab: TabItem }) {
|
||||
function TabRepeatSelect({ tab, id }: { tab: TabItem; id?: string }) {
|
||||
const { layout } = tab.useState();
|
||||
const dashboard = useDashboard(tab);
|
||||
|
||||
@@ -104,6 +107,7 @@ function TabRepeatSelect({ tab }: { tab: TabItem }) {
|
||||
return (
|
||||
<>
|
||||
<RepeatRowSelect2
|
||||
id={id}
|
||||
sceneContext={dashboard}
|
||||
repeat={tab.state.repeatByVariable}
|
||||
onChange={(repeat) => tab.onChangeRepeat(repeat)}
|
||||
|
||||
+4
-2
@@ -50,19 +50,20 @@ export function useLayoutCategory(layoutManager: DashboardLayoutManager) {
|
||||
|
||||
const groupLayout = new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.layout.common.group-layout', 'Group layout'),
|
||||
id: 'group-layout-category',
|
||||
id: 'dash-group-layout',
|
||||
isOpenDefault: false,
|
||||
});
|
||||
|
||||
const gridLayout = new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.layout.common.panel-layout', 'Panel layout'),
|
||||
id: 'grid-layout-category',
|
||||
id: 'dash-grid-layout',
|
||||
isOpenDefault: false,
|
||||
});
|
||||
|
||||
gridLayout.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: 'dash-grid-layout-option',
|
||||
skipField: true,
|
||||
render: () => <DashboardLayoutSelector layoutManager={layoutManager} />,
|
||||
})
|
||||
@@ -77,6 +78,7 @@ export function useLayoutCategory(layoutManager: DashboardLayoutManager) {
|
||||
groupLayout.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: 'dash-group-layout-option',
|
||||
skipField: true,
|
||||
render: () => <DashboardLayoutSelector layoutManager={layoutManager} />,
|
||||
})
|
||||
|
||||
+6
-3
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useId, useMemo } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { LocalValueVariable } from '@grafana/scenes';
|
||||
@@ -24,16 +24,19 @@ export class LocalVariableEditableElement implements EditableDashboardElement {
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
const variable = this.variable;
|
||||
const localVariableCategoryId = useId();
|
||||
const localVariableId = useId();
|
||||
|
||||
return useMemo(() => {
|
||||
const category = new OptionsPaneCategoryDescriptor({
|
||||
title: '',
|
||||
id: 'local-variable-options',
|
||||
id: localVariableCategoryId,
|
||||
});
|
||||
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: localVariableId,
|
||||
skipField: true,
|
||||
render: () => {
|
||||
return (
|
||||
@@ -52,6 +55,6 @@ export class LocalVariableEditableElement implements EditableDashboardElement {
|
||||
);
|
||||
|
||||
return [category];
|
||||
}, [variable]);
|
||||
}, [localVariableCategoryId, localVariableId, variable]);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-11
@@ -1,4 +1,4 @@
|
||||
import { FormEvent, useMemo, useRef, useState } from 'react';
|
||||
import { FormEvent, useId, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { VariableHide } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
@@ -46,16 +46,22 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
|
||||
|
||||
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
|
||||
const variable = this.variable;
|
||||
const variableOptionsCategoryId = useId();
|
||||
const variableNameId = useId();
|
||||
const labelId = useId();
|
||||
const descriptionId = useId();
|
||||
const variableHideId = useId();
|
||||
|
||||
if (variable instanceof LocalValueVariable) {
|
||||
return useLocalVariableOptions(variable);
|
||||
}
|
||||
|
||||
const basicOptions = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({ title: '', id: 'variable-options' })
|
||||
return new OptionsPaneCategoryDescriptor({ title: '', id: variableOptionsCategoryId, isOpenDefault: true })
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: variableNameId,
|
||||
skipField: true,
|
||||
render: () => <VariableNameInput variable={variable} isNewElement={isNewElement} />,
|
||||
})
|
||||
@@ -63,24 +69,27 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.label', 'Label'),
|
||||
id: labelId,
|
||||
description: t('dashboard.edit-pane.variable.label-description', 'Optional display name'),
|
||||
render: () => <VariableLabelInput variable={variable} />,
|
||||
render: (descriptor) => <VariableLabelInput id={descriptor.props.id} variable={variable} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.description', 'Description'),
|
||||
render: () => <VariableDescriptionTextArea variable={variable} />,
|
||||
id: descriptionId,
|
||||
render: (descriptor) => <VariableDescriptionTextArea id={descriptor.props.id} variable={variable} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: variableHideId,
|
||||
skipField: true,
|
||||
render: () => <VariableHideInput variable={variable} />,
|
||||
})
|
||||
);
|
||||
}, [variable, isNewElement]);
|
||||
}, [variableOptionsCategoryId, variableNameId, labelId, descriptionId, variableHideId, variable, isNewElement]);
|
||||
|
||||
const categories = [basicOptions];
|
||||
const typeCategory = useVariableTypeCategory(variable);
|
||||
@@ -117,12 +126,14 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
|
||||
|
||||
interface VariableInputProps {
|
||||
variable: SceneVariable;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable; isNewElement: boolean }) {
|
||||
const { name } = variable.useState();
|
||||
const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement });
|
||||
const [nameError, setNameError] = useState<string>();
|
||||
const id = useId();
|
||||
|
||||
const onChange = (e: FormEvent<HTMLInputElement>) => {
|
||||
const result = validateVariableName(variable, e.currentTarget.value);
|
||||
@@ -138,6 +149,7 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable
|
||||
return (
|
||||
<Field label={t('dashboard.edit-pane.variable.name', 'Name')} invalid={!!nameError} error={nameError}>
|
||||
<Input
|
||||
id={id}
|
||||
ref={ref}
|
||||
value={name}
|
||||
onFocus={() => {
|
||||
@@ -171,12 +183,13 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable
|
||||
);
|
||||
}
|
||||
|
||||
function VariableLabelInput({ variable }: VariableInputProps) {
|
||||
function VariableLabelInput({ variable, id }: VariableInputProps) {
|
||||
const { label } = variable.useState();
|
||||
const oldLabel = useRef(label ?? '');
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
value={label}
|
||||
onFocus={() => {
|
||||
oldLabel.current = label ?? '';
|
||||
@@ -201,13 +214,13 @@ function VariableLabelInput({ variable }: VariableInputProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function VariableDescriptionTextArea({ variable }: VariableInputProps) {
|
||||
function VariableDescriptionTextArea({ variable, id }: VariableInputProps) {
|
||||
const { description } = variable.useState();
|
||||
const oldDescription = useRef(description ?? '');
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
id="description-text-area"
|
||||
id={id}
|
||||
value={description ?? ''}
|
||||
placeholder={t('dashboard.edit-pane.variable.description-placeholder', 'Descriptive text')}
|
||||
onFocus={() => {
|
||||
@@ -247,6 +260,7 @@ function VariableHideInput({ variable }: VariableInputProps) {
|
||||
}
|
||||
|
||||
function useVariableTypeCategory(variable: SceneVariable) {
|
||||
const oldVariableId = useId();
|
||||
return useMemo(() => {
|
||||
const variableEditorDef = getEditableVariableDefinition(variable.state.type);
|
||||
const categoryName = t('dashboard.edit-pane.variable.type-category', '{{type}} options', {
|
||||
@@ -266,6 +280,7 @@ function useVariableTypeCategory(variable: SceneVariable) {
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: oldVariableId,
|
||||
skipField: true,
|
||||
render: () => <OpenOldVariableEditButton variable={variable} />,
|
||||
})
|
||||
@@ -273,7 +288,7 @@ function useVariableTypeCategory(variable: SceneVariable) {
|
||||
}
|
||||
|
||||
return category;
|
||||
}, [variable]);
|
||||
}, [oldVariableId, variable]);
|
||||
}
|
||||
|
||||
function OpenOldVariableEditButton({ variable }: VariableInputProps) {
|
||||
@@ -305,15 +320,18 @@ function OpenOldVariableEditButton({ variable }: VariableInputProps) {
|
||||
}
|
||||
|
||||
function useLocalVariableOptions(variable: LocalValueVariable): OptionsPaneCategoryDescriptor[] {
|
||||
const localVariableOptionsCategoryId = useId();
|
||||
const localVariableId = useId();
|
||||
return useMemo(() => {
|
||||
const category = new OptionsPaneCategoryDescriptor({
|
||||
title: '',
|
||||
id: 'local-variable-options',
|
||||
id: localVariableOptionsCategoryId,
|
||||
});
|
||||
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: localVariableId,
|
||||
skipField: true,
|
||||
render: () => {
|
||||
return (
|
||||
@@ -332,5 +350,5 @@ function useLocalVariableOptions(variable: LocalValueVariable): OptionsPaneCateg
|
||||
);
|
||||
|
||||
return [category];
|
||||
}, [variable]);
|
||||
}, [localVariableId, localVariableOptionsCategoryId, variable]);
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
import { useId, useMemo } from 'react';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
@@ -36,17 +36,19 @@ export class VariableSetEditableElement implements EditableDashboardElement {
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
const variableListId = useId();
|
||||
const set = this.set;
|
||||
|
||||
const options = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({ title: '', id: 'variables' }).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: variableListId,
|
||||
skipField: true,
|
||||
render: () => <VariableList set={set} />,
|
||||
})
|
||||
);
|
||||
}, [set]);
|
||||
}, [set, variableListId]);
|
||||
|
||||
return [options];
|
||||
}
|
||||
|
||||
+4
-2
@@ -80,12 +80,13 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
render: () => <ValuesTextField variable={variable} />,
|
||||
id: 'custom-variable-values',
|
||||
render: (descriptor) => <ValuesTextField id={descriptor.props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function ValuesTextField({ variable }: { variable: CustomVariable }) {
|
||||
function ValuesTextField({ variable, id }: { variable: CustomVariable; id?: string }) {
|
||||
const { query } = variable.useState();
|
||||
|
||||
const onBlur = async (event: FormEvent<HTMLTextAreaElement>) => {
|
||||
@@ -95,6 +96,7 @@ function ValuesTextField({ variable }: { variable: CustomVariable }) {
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
id={id}
|
||||
rows={2}
|
||||
defaultValue={query}
|
||||
onBlur={onBlur}
|
||||
|
||||
+1
@@ -69,6 +69,7 @@ export function getAdHocFilterOptions(variable: SceneVariable): OptionsPaneItemD
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
id: `variable-${variable.state.name}-value`,
|
||||
render: () => <AdHocFiltersVariableEditor variable={variable} onRunQuery={noop} inline={true} />,
|
||||
}),
|
||||
];
|
||||
|
||||
+4
-2
@@ -31,12 +31,13 @@ export function getConstantVariableOptions(variable: SceneVariable): OptionsPane
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard-scene.constant-variable-form.label-value', 'Value'),
|
||||
render: () => <ConstantValueInput variable={variable} />,
|
||||
id: 'constant-variable-value',
|
||||
render: (descriptor) => <ConstantValueInput id={descriptor.props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function ConstantValueInput({ variable }: { variable: ConstantVariable }) {
|
||||
function ConstantValueInput({ variable, id }: { variable: ConstantVariable; id?: string }) {
|
||||
const { value } = variable.useState();
|
||||
|
||||
const onBlur = async (event: FormEvent<HTMLInputElement>) => {
|
||||
@@ -46,6 +47,7 @@ function ConstantValueInput({ variable }: { variable: ConstantVariable }) {
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
defaultValue={value.toString()}
|
||||
onBlur={onBlur}
|
||||
placeholder={t('dashboard-scene.constant-variable-form.placeholder-your-metric-prefix', 'Your metric prefix')}
|
||||
|
||||
+4
-2
@@ -58,12 +58,13 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
render: () => <ValuesTextField variable={variable} />,
|
||||
id: 'custom-variable-values',
|
||||
render: ({ props }) => <ValuesTextField id={props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function ValuesTextField({ variable }: { variable: CustomVariable }) {
|
||||
function ValuesTextField({ variable, id }: { variable: CustomVariable; id?: string }) {
|
||||
const { query } = variable.useState();
|
||||
|
||||
const onBlur = async (event: FormEvent<HTMLTextAreaElement>) => {
|
||||
@@ -73,6 +74,7 @@ function ValuesTextField({ variable }: { variable: CustomVariable }) {
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
id={id}
|
||||
rows={2}
|
||||
defaultValue={query}
|
||||
onBlur={onBlur}
|
||||
|
||||
+13
-4
@@ -80,20 +80,27 @@ export function getDataSourceVariableOptions(variable: SceneVariable): OptionsPa
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.datasource-options.type', 'Type'),
|
||||
render: () => <DataSourceTypeSelect variable={variable} />,
|
||||
id: 'datasource-options-type',
|
||||
render: ({ props }) => <DataSourceTypeSelect id={props.id} variable={variable} />,
|
||||
}),
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.datasource-options.name-filter', 'Name filter'),
|
||||
id: 'datasource-options-name-filter',
|
||||
description: t(
|
||||
'dashboard.edit-pane.variable.datasource-options.name-filter-description',
|
||||
'Regex filter for which data source instances to include. Leave empty for all.'
|
||||
),
|
||||
render: () => <DataSourceNameFilter variable={variable} />,
|
||||
render: ({ props }) => <DataSourceNameFilter id={props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function DataSourceTypeSelect({ variable }: { variable: DataSourceVariable }) {
|
||||
interface InputProps {
|
||||
variable: DataSourceVariable;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
function DataSourceTypeSelect({ variable, id }: InputProps) {
|
||||
const { pluginId } = variable.useState();
|
||||
const options = getOptionDataSourceTypes();
|
||||
|
||||
@@ -104,6 +111,7 @@ function DataSourceTypeSelect({ variable }: { variable: DataSourceVariable }) {
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
id={id}
|
||||
options={options}
|
||||
value={pluginId}
|
||||
onChange={onChange}
|
||||
@@ -113,7 +121,7 @@ function DataSourceTypeSelect({ variable }: { variable: DataSourceVariable }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DataSourceNameFilter({ variable }: { variable: DataSourceVariable }) {
|
||||
function DataSourceNameFilter({ variable, id }: InputProps) {
|
||||
const { regex } = variable.useState();
|
||||
|
||||
const onBlur = async (evt: React.FormEvent<HTMLInputElement>) => {
|
||||
@@ -123,6 +131,7 @@ function DataSourceNameFilter({ variable }: { variable: DataSourceVariable }) {
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
defaultValue={regex}
|
||||
onBlur={onBlur}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.nameFilter}
|
||||
|
||||
+1
@@ -67,6 +67,7 @@ export function getGroupByVariableOptions(variable: SceneVariable): OptionsPaneI
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
id: `variable-${variable.state.name}-value`,
|
||||
render: () => <GroupByVariableEditor variable={variable} onRunQuery={noop} inline={true} />,
|
||||
}),
|
||||
];
|
||||
|
||||
+1
@@ -71,6 +71,7 @@ export function getIntervalVariableOptions(variable: SceneVariable): OptionsPane
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
id: `variable-${variable.state.name}-value`,
|
||||
render: () => <IntervalVariableEditor variable={variable} onRunQuery={noop} inline={true} />,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -127,6 +127,7 @@ export function getQueryVariableOptions(variable: SceneVariable): OptionsPaneIte
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
id: `variable-${variable.state.name}-value`,
|
||||
render: () => <ModalEditor variable={variable} />,
|
||||
}),
|
||||
];
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ export function getSystemVariableOptions(variable: SceneVariable): OptionsPaneIt
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: `variable-${variable.state.name}-value`,
|
||||
render: () => {
|
||||
return (
|
||||
<Stack direction="column">
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ export function getTextBoxVariableOptions(variable: SceneVariable): OptionsPaneI
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard-scene.textbox-variable-form.label-value', 'Value'),
|
||||
id: `variable-${variable.state.name}-value`,
|
||||
render: () => <TextBoxVariableEditor onChange={noop} variable={variable} inline={true} />,
|
||||
}),
|
||||
];
|
||||
|
||||
+36
-13
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useCallback, useId, useMemo, useRef } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { MultiValueVariable, SceneVariableValueChangedEvent } from '@grafana/scenes';
|
||||
@@ -7,6 +7,11 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
export function useVariableSelectionOptionsCategory(variable: MultiValueVariable): OptionsPaneCategoryDescriptor {
|
||||
const multiValueId = useId();
|
||||
const includeAllId = useId();
|
||||
const customAllValueId = useId();
|
||||
const allowCustomId = useId();
|
||||
|
||||
return useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.selection-options.category', 'Selection options'),
|
||||
@@ -16,22 +21,25 @@ export function useVariableSelectionOptionsCategory(variable: MultiValueVariable
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.selection-options.multi-value', 'Multi-value'),
|
||||
render: () => <MultiValueSwitch variable={variable} />,
|
||||
id: multiValueId,
|
||||
render: (descriptor) => <MultiValueSwitch id={descriptor.props.id} variable={variable} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.selection-options.include-all', 'Include All value'),
|
||||
id: includeAllId,
|
||||
description: t(
|
||||
'dashboard.edit-pane.variable.selection-options.include-all-description',
|
||||
'Enables a single option that represent all values'
|
||||
),
|
||||
render: () => <IncludeAllSwitch variable={variable} />,
|
||||
render: (descriptor) => <IncludeAllSwitch id={descriptor.props.id} variable={variable} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.selection-options.custom-all-value', 'Custom all value'),
|
||||
id: customAllValueId,
|
||||
description: t(
|
||||
'dashboard.edit-pane.variable.selection-options.custom-all-value-description',
|
||||
'A wildcard regex or other value to represent All'
|
||||
@@ -39,46 +47,61 @@ export function useVariableSelectionOptionsCategory(variable: MultiValueVariable
|
||||
useShowIf: () => {
|
||||
return variable.useState().includeAll ?? false;
|
||||
},
|
||||
render: () => <CustomAllValueInput variable={variable} />,
|
||||
render: (descriptor) => <CustomAllValueInput id={descriptor.props.id} variable={variable} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.selection-options.allow-custom-values', 'Allow custom values'),
|
||||
id: allowCustomId,
|
||||
description: t(
|
||||
'dashboard.edit-pane.variable.selection-options.allow-custom-values-description',
|
||||
'Enables users to enter values'
|
||||
),
|
||||
render: () => <AllowCustomSwitch variable={variable} />,
|
||||
render: (descriptor) => <AllowCustomSwitch id={descriptor.props.id} variable={variable} />,
|
||||
})
|
||||
);
|
||||
}, [variable]);
|
||||
}, [allowCustomId, customAllValueId, includeAllId, multiValueId, variable]);
|
||||
}
|
||||
|
||||
function MultiValueSwitch({ variable }: { variable: MultiValueVariable }) {
|
||||
interface InputProps {
|
||||
variable: MultiValueVariable;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
function MultiValueSwitch({ variable, id }: InputProps) {
|
||||
const { isMulti } = variable.useState();
|
||||
|
||||
return <Switch value={isMulti} onChange={(evt) => variable.setState({ isMulti: evt.currentTarget.checked })} />;
|
||||
return (
|
||||
<Switch id={id} value={isMulti} onChange={(evt) => variable.setState({ isMulti: evt.currentTarget.checked })} />
|
||||
);
|
||||
}
|
||||
|
||||
function IncludeAllSwitch({ variable }: { variable: MultiValueVariable }) {
|
||||
function IncludeAllSwitch({ variable, id }: InputProps) {
|
||||
const { includeAll } = variable.useState();
|
||||
|
||||
return <Switch value={includeAll} onChange={(evt) => variable.setState({ includeAll: evt.currentTarget.checked })} />;
|
||||
return (
|
||||
<Switch
|
||||
id={id}
|
||||
value={includeAll}
|
||||
onChange={(evt) => variable.setState({ includeAll: evt.currentTarget.checked })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AllowCustomSwitch({ variable }: { variable: MultiValueVariable }) {
|
||||
function AllowCustomSwitch({ variable, id }: InputProps) {
|
||||
const { allowCustomValue } = variable.useState();
|
||||
|
||||
return (
|
||||
<Switch
|
||||
id={id}
|
||||
value={allowCustomValue}
|
||||
onChange={(evt) => variable.setState({ allowCustomValue: evt.currentTarget.checked })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomAllValueInput({ variable }: { variable: MultiValueVariable }) {
|
||||
function CustomAllValueInput({ variable, id }: InputProps) {
|
||||
const { allValue } = variable.useState();
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -97,5 +120,5 @@ function CustomAllValueInput({ variable }: { variable: MultiValueVariable }) {
|
||||
[variable]
|
||||
);
|
||||
|
||||
return <Input ref={ref} defaultValue={allValue ?? ''} onBlur={onInputBlur} />;
|
||||
return <Input id={id} ref={ref} defaultValue={allValue ?? ''} onBlur={onInputBlur} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { ReactNode } from 'react';
|
||||
import * as React from 'react';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
@@ -24,7 +23,8 @@ export interface OptionsPaneItemInfo {
|
||||
useShowIf?: () => boolean;
|
||||
overrides?: OptionPaneItemOverrideInfo[];
|
||||
addon?: ReactNode;
|
||||
id?: string;
|
||||
/** Must be unique on the page! */
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,10 +35,7 @@ export class OptionsPaneItemDescriptor {
|
||||
props: OptionsPaneItemInfo;
|
||||
|
||||
constructor(props: OptionsPaneItemInfo) {
|
||||
this.props = { ...props, id: props.id ?? props.title };
|
||||
if (this.props.id === '') {
|
||||
this.props.id = uniqueId();
|
||||
}
|
||||
this.props = { ...props };
|
||||
}
|
||||
|
||||
render(searchQuery?: string) {
|
||||
|
||||
@@ -23,6 +23,8 @@ import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
|
||||
import { OverrideCategoryTitle } from './OverrideCategoryTitle';
|
||||
|
||||
// [FIXME] Is there something else we need to do in here?
|
||||
|
||||
export function getFieldOverrideCategories(
|
||||
fieldConfig: FieldConfigSource,
|
||||
registry: FieldConfigOptionsRegistry,
|
||||
@@ -59,13 +61,7 @@ export function getFieldOverrideCategories(
|
||||
...currentFieldConfig,
|
||||
overrides: [
|
||||
...currentFieldConfig.overrides,
|
||||
{
|
||||
matcher: {
|
||||
id: info.id,
|
||||
options: info.defaultOptions,
|
||||
},
|
||||
properties: [],
|
||||
},
|
||||
{ matcher: { id: info.id, options: info.defaultOptions }, properties: [] },
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -84,6 +80,7 @@ export function getFieldOverrideCategories(
|
||||
const overrideName = t('dashboard.get-field-override-categories.override-name', 'Override {{overrideNum}}', {
|
||||
overrideNum: idx + 1,
|
||||
});
|
||||
const overrideId = `panel-options-override-${idx}`;
|
||||
const matcherUi = fieldMatchersUI.get(override.matcher.id);
|
||||
const configPropertiesOptions = getOverrideProperties(registry);
|
||||
const isSystemOverride = isSystemOverrideGuard(override);
|
||||
@@ -92,7 +89,7 @@ export function getFieldOverrideCategories(
|
||||
|
||||
const category = new OptionsPaneCategoryDescriptor({
|
||||
title: overrideName,
|
||||
id: overrideName,
|
||||
id: overrideId,
|
||||
forceOpen,
|
||||
renderTitle: function renderOverrideTitle(isExpanded: boolean) {
|
||||
return (
|
||||
@@ -109,18 +106,12 @@ export function getFieldOverrideCategories(
|
||||
});
|
||||
|
||||
const onMatcherConfigChange = (options: unknown) => {
|
||||
onOverrideChange(idx, {
|
||||
...override,
|
||||
matcher: { ...override.matcher, options },
|
||||
});
|
||||
onOverrideChange(idx, { ...override, matcher: { ...override.matcher, options } });
|
||||
};
|
||||
|
||||
const onDynamicConfigValueAdd = (override: ConfigOverrideRule, value: SelectableValue<string>) => {
|
||||
const registryItem = registry.get(value.value!);
|
||||
const propertyConfig: DynamicConfigValue = {
|
||||
id: registryItem.id,
|
||||
value: registryItem.defaultValue,
|
||||
};
|
||||
const propertyConfig: DynamicConfigValue = { id: registryItem.id, value: registryItem.defaultValue };
|
||||
|
||||
const properties = override.properties ?? [];
|
||||
properties.push(propertyConfig);
|
||||
@@ -131,13 +122,15 @@ export function getFieldOverrideCategories(
|
||||
/**
|
||||
* Add override matcher UI element
|
||||
*/
|
||||
const htmlId = `${overrideId}-matcher`;
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
id: htmlId,
|
||||
title: matcherUi.name,
|
||||
render: function renderMatcherUI() {
|
||||
return (
|
||||
<matcherUi.component
|
||||
id={`${matcherUi.matcher.id}-${idx}`}
|
||||
id={htmlId}
|
||||
matcher={matcherUi.matcher}
|
||||
data={data ?? []}
|
||||
options={override.matcher.options}
|
||||
@@ -173,23 +166,22 @@ export function getFieldOverrideCategories(
|
||||
};
|
||||
|
||||
const onPropertyRemove = () => {
|
||||
onOverrideChange(idx, {
|
||||
...override,
|
||||
properties: override.properties.filter((_, i) => i !== propIdx),
|
||||
});
|
||||
onOverrideChange(idx, { ...override, properties: override.properties.filter((_, i) => i !== propIdx) });
|
||||
};
|
||||
|
||||
const htmlId = `${overrideId}-property-${property.id}`;
|
||||
|
||||
/**
|
||||
* Add override property item
|
||||
*/
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: registryItemForProperty.name,
|
||||
skipField: true,
|
||||
id: htmlId,
|
||||
render: function renderPropertyEditor() {
|
||||
return (
|
||||
<DynamicConfigValueEditor
|
||||
key={`${property.id}/${propIdx}`}
|
||||
key={htmlId}
|
||||
isSystemOverride={isSystemOverride}
|
||||
onChange={onPropertyChange}
|
||||
onRemove={onPropertyRemove}
|
||||
@@ -210,8 +202,8 @@ export function getFieldOverrideCategories(
|
||||
if (!isSystemOverride && override.matcher.options) {
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '----------',
|
||||
skipField: true,
|
||||
id: `${overrideId}-add-button`,
|
||||
render: function renderAddPropertyButton() {
|
||||
return (
|
||||
<ValuePicker
|
||||
@@ -274,11 +266,7 @@ function getOverrideProperties(registry: FieldConfigOptionsRegistry) {
|
||||
if (item.category) {
|
||||
label = [...item.category, item.name].join(' > ');
|
||||
}
|
||||
return {
|
||||
label,
|
||||
value: item.id,
|
||||
description: item.description,
|
||||
};
|
||||
return { label, value: item.id, description: item.description };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -288,9 +276,5 @@ function AddOverrideButtonContainer({ children }: { children: React.ReactNode })
|
||||
}
|
||||
|
||||
function getBorderTopStyles(theme: GrafanaTheme2) {
|
||||
return css({
|
||||
borderTop: `1px solid ${theme.colors.border.weak}`,
|
||||
padding: `${theme.spacing(2)}`,
|
||||
display: 'flex',
|
||||
});
|
||||
return css({ borderTop: `1px solid ${theme.colors.border.weak}`, padding: `${theme.spacing(2)}`, display: 'flex' });
|
||||
}
|
||||
|
||||
@@ -24,12 +24,13 @@ export function getLibraryPanelOptionsCategory(props: OptionPaneRenderProps): Op
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-library-panel-options-category.title.name', 'Name'),
|
||||
id: 'library-panel-name',
|
||||
value: panel.libraryPanel.name,
|
||||
popularRank: 1,
|
||||
render: function renderName() {
|
||||
render: function renderName(descriptor) {
|
||||
return (
|
||||
<Input
|
||||
id="LibraryPanelFrameName"
|
||||
id={descriptor.props.id}
|
||||
defaultValue={panel.libraryPanel.name}
|
||||
onBlur={(e) =>
|
||||
onPanelConfigChange('libraryPanel', { ...panel.libraryPanel, name: e.currentTarget.value })
|
||||
@@ -42,6 +43,7 @@ export function getLibraryPanelOptionsCategory(props: OptionPaneRenderProps): Op
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-library-panel-options-category.title.information', 'Information'),
|
||||
id: 'library-panel-information',
|
||||
render: function renderLibraryPanelInformation() {
|
||||
return <LibraryPanelInformation panel={panel} formatDate={dashboard.formatDate} />;
|
||||
},
|
||||
|
||||
@@ -20,8 +20,11 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
|
||||
isOpenDefault: true,
|
||||
});
|
||||
|
||||
const panelFrameTitleId = 'panel-frame-title';
|
||||
const descriptionId = 'panel-frame-description';
|
||||
|
||||
const setPanelTitle = (title: string) => {
|
||||
const input = document.getElementById('PanelFrameTitle');
|
||||
const input = document.getElementById(panelFrameTitleId);
|
||||
if (input instanceof HTMLInputElement) {
|
||||
input.value = title;
|
||||
onPanelConfigChange('title', title);
|
||||
@@ -29,7 +32,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
|
||||
};
|
||||
|
||||
const setPanelDescription = (description: string) => {
|
||||
const input = document.getElementById('description-text-area');
|
||||
const input = document.getElementById(descriptionId);
|
||||
if (input instanceof HTMLTextAreaElement) {
|
||||
input.value = description;
|
||||
onPanelConfigChange('description', description);
|
||||
@@ -40,7 +43,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-panel-frame-category.title.title', 'Title'),
|
||||
id: 'PanelFrameTitle',
|
||||
id: panelFrameTitleId,
|
||||
value: panel.title,
|
||||
popularRank: 1,
|
||||
render: function renderTitle(descriptor) {
|
||||
@@ -65,7 +68,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-panel-frame-category.title.description', 'Description'),
|
||||
id: 'description-text-area',
|
||||
id: descriptionId,
|
||||
description: panel.description,
|
||||
value: panel.description,
|
||||
render: function renderDescription(descriptor) {
|
||||
@@ -86,7 +89,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-panel-frame-category.title.transparent-background', 'Transparent background'),
|
||||
id: 'transparent-background',
|
||||
id: 'panel-frame-transparent-bg',
|
||||
render: function renderTransparent(descriptor) {
|
||||
return (
|
||||
<Switch
|
||||
@@ -102,12 +105,13 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
|
||||
.addCategory(
|
||||
new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.get-panel-frame-category.title.panel-links', 'Panel links'),
|
||||
id: 'Panel links',
|
||||
id: 'panel-frame-links-category',
|
||||
isOpenDefault: false,
|
||||
itemsCount: panel.links?.length,
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-panel-frame-category.title.panel-links', 'Panel links'),
|
||||
id: 'panel-frame-links-category',
|
||||
render: function renderLinks() {
|
||||
return (
|
||||
<DataLinksInlineEditor
|
||||
@@ -124,13 +128,13 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
|
||||
.addCategory(
|
||||
new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.get-panel-frame-category.title.repeat-options', 'Repeat options'),
|
||||
id: 'Repeat options',
|
||||
id: 'panel-frame-repeat',
|
||||
isOpenDefault: false,
|
||||
})
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-panel-frame-category.title.repeat-by-variable', 'Repeat by variable'),
|
||||
id: 'repeat-by-variable-select',
|
||||
id: 'panel-frame-repeat-by-variable',
|
||||
description:
|
||||
'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.',
|
||||
render: function renderRepeatOptions(descriptor) {
|
||||
@@ -149,6 +153,7 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-panel-frame-category.title.repeat-direction', 'Repeat direction'),
|
||||
id: 'panel-frame-repeat-direction',
|
||||
showIf: () => !!panel.repeat,
|
||||
render: function renderRepeatOptions() {
|
||||
const directionOptions = [
|
||||
@@ -175,11 +180,13 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-panel-frame-category.title.max-per-row', 'Max per row'),
|
||||
id: 'panel-frame-repeat-max-per-row',
|
||||
showIf: () => Boolean(panel.repeat && panel.repeatDirection === 'h'),
|
||||
render: function renderOption() {
|
||||
render: function renderOption(descriptor) {
|
||||
const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value }));
|
||||
return (
|
||||
<Select
|
||||
id={descriptor.props.id}
|
||||
options={maxPerRowOptions}
|
||||
value={panel.maxPerRow}
|
||||
onChange={(value) => onPanelConfigChange('maxPerRow', value.value)}
|
||||
|
||||
@@ -93,7 +93,7 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
||||
};
|
||||
|
||||
// Load the options into categories
|
||||
fillOptionsPaneItems(plugin.getPanelOptionsSupplier(), access, getOptionsPaneCategory, context);
|
||||
fillOptionsPaneItems(plugin.meta.id, plugin.getPanelOptionsSupplier(), access, getOptionsPaneCategory, context);
|
||||
|
||||
/**
|
||||
* Field options
|
||||
@@ -130,9 +130,11 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
||||
category.props.itemsCount = fieldOption.getItemsCount(value);
|
||||
}
|
||||
|
||||
const htmlId = `${plugin.meta.id}-${fieldOption.path}`;
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: fieldOption.name,
|
||||
id: htmlId,
|
||||
description: fieldOption.description,
|
||||
overrides: getOptionOverrides(fieldOption, currentFieldConfig, data?.series),
|
||||
render: function renderEditor() {
|
||||
@@ -142,7 +144,7 @@ export function getVisualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
||||
);
|
||||
};
|
||||
|
||||
return <Editor value={value} onChange={onChange} item={fieldOption} context={context} id={fieldOption.id} />;
|
||||
return <Editor value={value} onChange={onChange} item={fieldOption} context={context} id={htmlId} />;
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -165,12 +167,13 @@ export function getLibraryVizPanelOptionsCategory(libraryPanel: LibraryPanelBeha
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-library-viz-panel-options-category.title.name', 'Name'),
|
||||
id: 'library-panel-name',
|
||||
value: libraryPanel,
|
||||
popularRank: 1,
|
||||
render: function renderName() {
|
||||
render: function renderName(descriptor) {
|
||||
return (
|
||||
<Input
|
||||
id="LibraryPanelFrameName"
|
||||
id={descriptor.props.id}
|
||||
data-testid="library panel name input"
|
||||
defaultValue={libraryPanel.state.name}
|
||||
onBlur={(e) => libraryPanel.setState({ name: e.currentTarget.value })}
|
||||
@@ -182,6 +185,7 @@ export function getLibraryVizPanelOptionsCategory(libraryPanel: LibraryPanelBeha
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.get-library-viz-panel-options-category.title.information', 'Information'),
|
||||
id: 'library-panel-information',
|
||||
render: function renderLibraryPanelInformation() {
|
||||
return <LibraryVizPanelInfo libraryPanel={libraryPanel} />;
|
||||
},
|
||||
@@ -236,7 +240,7 @@ export function getVisualizationOptions2(props: OptionPaneRenderProps2): Options
|
||||
});
|
||||
|
||||
// Load the options into categories
|
||||
fillOptionsPaneItems(plugin.getPanelOptionsSupplier(), access, getOptionsPaneCategory, context);
|
||||
fillOptionsPaneItems(plugin.meta.id, plugin.getPanelOptionsSupplier(), access, getOptionsPaneCategory, context);
|
||||
|
||||
// Field options
|
||||
const currentFieldConfig = panel.state.fieldConfig;
|
||||
@@ -264,9 +268,11 @@ export function getVisualizationOptions2(props: OptionPaneRenderProps2): Options
|
||||
category.props.itemsCount = fieldOption.getItemsCount(value);
|
||||
}
|
||||
|
||||
const htmlId = `${plugin.meta.id}-${fieldOption.path}`;
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: fieldOption.name,
|
||||
id: htmlId,
|
||||
description: fieldOption.description,
|
||||
overrides: getOptionOverrides(fieldOption, currentFieldConfig, data?.series),
|
||||
render: function renderEditor() {
|
||||
@@ -277,7 +283,7 @@ export function getVisualizationOptions2(props: OptionPaneRenderProps2): Options
|
||||
);
|
||||
};
|
||||
|
||||
return <Editor value={value} onChange={onChange} item={fieldOption} context={context} id={fieldOption.id} />;
|
||||
return <Editor value={value} onChange={onChange} item={fieldOption} context={context} id={htmlId} />;
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -292,6 +298,7 @@ export function getVisualizationOptions2(props: OptionPaneRenderProps2): Options
|
||||
* @internal
|
||||
*/
|
||||
export function fillOptionsPaneItems(
|
||||
idPrefix: string,
|
||||
supplier: PanelOptionsSupplier<any>,
|
||||
access: NestedValueAccess,
|
||||
getOptionsPaneCategory: categoryGetter,
|
||||
@@ -306,6 +313,8 @@ export function fillOptionsPaneItems(
|
||||
continue;
|
||||
}
|
||||
|
||||
const htmlId = `${idPrefix}-${pluginOption.id}`;
|
||||
|
||||
let category = parentCategory;
|
||||
if (!category) {
|
||||
category = getOptionsPaneCategory(pluginOption.category);
|
||||
@@ -321,6 +330,7 @@ export function fillOptionsPaneItems(
|
||||
: { ...context, options: access.getValue(pluginOption.path) };
|
||||
|
||||
fillOptionsPaneItems(
|
||||
htmlId,
|
||||
pluginOption.getBuilder(),
|
||||
subAccess,
|
||||
getOptionsPaneCategory,
|
||||
@@ -334,6 +344,7 @@ export function fillOptionsPaneItems(
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: pluginOption.name,
|
||||
id: htmlId,
|
||||
description: pluginOption.description,
|
||||
render: function renderEditor() {
|
||||
return (
|
||||
@@ -344,7 +355,7 @@ export function fillOptionsPaneItems(
|
||||
}}
|
||||
item={pluginOption}
|
||||
context={context}
|
||||
id={pluginOption.id}
|
||||
id={htmlId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { v4 as uuiv4 } from 'uuid';
|
||||
|
||||
import { OptionsPaneCategoryDescriptor } from '../OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from '../OptionsPaneItemDescriptor';
|
||||
|
||||
@@ -51,18 +53,21 @@ function getOptionCategories(): OptionsPaneCategoryDescriptor[] {
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Title',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Min',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'ASDSADASDSADA',
|
||||
id: uuiv4(),
|
||||
description: 'DescriptionMatch',
|
||||
render: jest.fn(),
|
||||
})
|
||||
@@ -74,18 +79,21 @@ function getOptionCategories(): OptionsPaneCategoryDescriptor[] {
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Min',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'DescriptionMatch',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Frame',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
),
|
||||
@@ -101,18 +109,21 @@ function getOverrides(): OptionsPaneCategoryDescriptor[] {
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Match by name',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Min',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Max',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
),
|
||||
@@ -123,18 +134,21 @@ function getOverrides(): OptionsPaneCategoryDescriptor[] {
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Match by name',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Threshold',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: 'Max',
|
||||
id: uuiv4(),
|
||||
render: jest.fn(),
|
||||
})
|
||||
),
|
||||
|
||||
@@ -21,7 +21,7 @@ export const ColorDimensionEditor = (props: StandardEditorProps<ColorDimensionCo
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const { value, context, onChange, item } = props;
|
||||
const { value, context, onChange, item, id } = props;
|
||||
|
||||
const defaultColor = 'dark-green';
|
||||
|
||||
@@ -71,6 +71,7 @@ export const ColorDimensionEditor = (props: StandardEditorProps<ColorDimensionCo
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<Select
|
||||
inputId={id}
|
||||
value={selectedOption}
|
||||
options={selectOptions}
|
||||
onChange={onSelectChange}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useId, useMemo } from 'react';
|
||||
|
||||
import { FieldType, GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
@@ -92,6 +92,8 @@ export const ScalarDimensionEditor = ({ value, context, onChange, item }: Props)
|
||||
[onChange, value]
|
||||
);
|
||||
|
||||
const valueInputId = useId();
|
||||
|
||||
const val = value ?? {};
|
||||
const mode = value?.mode ?? ScalarDimensionMode.Mod;
|
||||
const selectedOption = isFixed ? fixedValueOption : selectOptions.find((v) => v.value === fieldName);
|
||||
@@ -119,6 +121,7 @@ export const ScalarDimensionEditor = ({ value, context, onChange, item }: Props)
|
||||
grow={true}
|
||||
>
|
||||
<NumberInput
|
||||
id={valueInputId}
|
||||
value={val?.fixed ?? DEFAULT_VALUE}
|
||||
onChange={onValueChange}
|
||||
max={settings?.max}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useId, useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
@@ -12,7 +12,7 @@ import { validateScaleOptions, validateScaleConfig } from '../scale';
|
||||
import { ScaleDimensionOptions } from '../types';
|
||||
|
||||
export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionConfig, ScaleDimensionOptions>) => {
|
||||
const { value, context, onChange, item } = props;
|
||||
const { value, context, onChange, item, id } = props;
|
||||
const { settings } = item;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@@ -95,12 +95,17 @@ export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionCo
|
||||
[validateAndDoChange, value]
|
||||
);
|
||||
|
||||
const valueInputId = useId();
|
||||
const minInputId = useId();
|
||||
const maxInputId = useId();
|
||||
|
||||
const val = value ?? {};
|
||||
const selectedOption = isFixed ? fixedValueOption : selectOptions.find((v) => v.value === fieldName);
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Select
|
||||
inputId={id}
|
||||
value={selectedOption}
|
||||
options={selectOptions}
|
||||
onChange={onSelectChange}
|
||||
@@ -111,7 +116,7 @@ export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionCo
|
||||
{isFixed && (
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('dimensions.scale-dimension-editor.label-value', 'Value')} labelWidth={8} grow={true}>
|
||||
<NumberInput value={val.fixed} {...minMaxStep} onChange={onValueChange} />
|
||||
<NumberInput id={valueInputId} value={val.fixed} {...minMaxStep} onChange={onValueChange} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
@@ -119,12 +124,12 @@ export const ScaleDimensionEditor = (props: StandardEditorProps<ScaleDimensionCo
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('dimensions.scale-dimension-editor.label-min', 'Min')} labelWidth={8} grow={true}>
|
||||
<NumberInput value={val.min} {...minMaxStep} onChange={onMinChange} />
|
||||
<NumberInput id={minInputId} value={val.min} {...minMaxStep} onChange={onMinChange} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('dimensions.scale-dimension-editor.label-max', 'Max')} labelWidth={8} grow={true}>
|
||||
<NumberInput value={val.max} {...minMaxStep} onChange={onMaxChange} />
|
||||
<NumberInput id={maxInputId} value={val.max} {...minMaxStep} onChange={onMaxChange} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useId } from 'react';
|
||||
|
||||
import {
|
||||
FieldNamePickerConfigSettings,
|
||||
@@ -74,6 +74,10 @@ export const TextDimensionEditor = ({ value, context, onChange }: Props) => {
|
||||
onFixedChange('');
|
||||
};
|
||||
|
||||
const fieldInputId = useId();
|
||||
const valueInputId = useId();
|
||||
const templateInputId = useId();
|
||||
|
||||
const mode = value?.mode ?? TextDimensionMode.Fixed;
|
||||
return (
|
||||
<>
|
||||
@@ -94,6 +98,7 @@ export const TextDimensionEditor = ({ value, context, onChange }: Props) => {
|
||||
grow={true}
|
||||
>
|
||||
<FieldNamePicker
|
||||
id={fieldInputId}
|
||||
context={context}
|
||||
value={value.field ?? ''}
|
||||
onChange={onFieldChange}
|
||||
@@ -110,6 +115,7 @@ export const TextDimensionEditor = ({ value, context, onChange }: Props) => {
|
||||
grow={true}
|
||||
>
|
||||
<StringValueEditor
|
||||
id={valueInputId}
|
||||
context={context}
|
||||
value={value?.fixed}
|
||||
onChange={onFixedChange}
|
||||
@@ -138,6 +144,7 @@ export const TextDimensionEditor = ({ value, context, onChange }: Props) => {
|
||||
grow={true}
|
||||
>
|
||||
<StringValueEditor // This could be a code editor
|
||||
id={templateInputId}
|
||||
context={context}
|
||||
value={value?.fixed}
|
||||
onChange={onFixedChange}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const LocationModeEditor = ({
|
||||
onChange,
|
||||
context,
|
||||
item,
|
||||
id,
|
||||
}: StandardEditorProps<string, ModeEditorSettings, unknown, unknown>) => {
|
||||
const [info, setInfo] = useState<FrameGeometryField>();
|
||||
|
||||
@@ -97,6 +98,7 @@ export const LocationModeEditor = ({
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
inputId={id}
|
||||
options={MODE_OPTIONS}
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export function getTransformerOptionPane<T = any>(
|
||||
};
|
||||
|
||||
// Use the panel options loader
|
||||
fillOptionsPaneItems(supplier, access, getOptionsPaneCategory, context);
|
||||
fillOptionsPaneItems('spatial-transformer', supplier, access, getOptionsPaneCategory, context);
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,11 +128,12 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
|
||||
description: t('alertlist.description-datasource', 'Filter from alert source'),
|
||||
id: 'datasource',
|
||||
defaultValue: null,
|
||||
editor: function RenderDatasourcePicker(props) {
|
||||
editor: function RenderDatasourcePicker({ id, ...props }) {
|
||||
return (
|
||||
<Stack gap={1}>
|
||||
<DataSourcePicker
|
||||
{...props}
|
||||
inputId={id}
|
||||
type={SUPPORTED_RULE_SOURCE_TYPES}
|
||||
noDefault
|
||||
current={props.value}
|
||||
|
||||
@@ -174,7 +174,7 @@ function getOptionsPaneCategoryDescriptor<T extends object>(
|
||||
};
|
||||
|
||||
// Use the panel options loader
|
||||
fillOptionsPaneItems(supplier, access, getOptionsPaneCategory, context);
|
||||
fillOptionsPaneItems('canvas-inline', supplier, access, getOptionsPaneCategory, context);
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { RefIDMultiPicker, RefIDPicker, stringsToRegexp } from '@grafana/ui/inte
|
||||
|
||||
type Props = StandardEditorProps<MatcherConfig>;
|
||||
|
||||
export const FrameSelectionEditor = ({ value, context, onChange }: Props) => {
|
||||
export const FrameSelectionEditor = ({ value, context, onChange, id }: Props) => {
|
||||
const onFilterChange = useCallback(
|
||||
(v: string) => {
|
||||
onChange(
|
||||
@@ -23,6 +23,7 @@ export const FrameSelectionEditor = ({ value, context, onChange }: Props) => {
|
||||
|
||||
return (
|
||||
<RefIDPicker
|
||||
id={id}
|
||||
value={value?.options}
|
||||
onChange={onFilterChange}
|
||||
data={context.data}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { toLonLat } from 'ol/proj';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { useMemo, useCallback, useId } from 'react';
|
||||
|
||||
import { StandardEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
@@ -63,11 +63,14 @@ export const MapViewEditor = ({
|
||||
[value, onChange]
|
||||
);
|
||||
|
||||
const viewInputId = useId();
|
||||
const zoomInputId = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('geomap.map-view-editor.label-view', 'View')} labelWidth={labelWidth} grow={true}>
|
||||
<Select options={views.options} value={views.current} onChange={onSelectView} />
|
||||
<Select inputId={viewInputId} options={views.options} value={views.current} onChange={onSelectView} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
{value.id === MapCenterID.Coordinates && (
|
||||
@@ -88,6 +91,7 @@ export const MapViewEditor = ({
|
||||
grow={true}
|
||||
>
|
||||
<NumberInput
|
||||
id={zoomInputId}
|
||||
value={value?.zoom ?? 1}
|
||||
min={1}
|
||||
max={18}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { capitalize } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useId, useMemo } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
@@ -113,6 +113,21 @@ export const StyleEditor = (props: Props) => {
|
||||
const hasTextLabel = styleUsesText(value);
|
||||
const maxFiles = 2000;
|
||||
|
||||
const symbolId = useId();
|
||||
const rotationAngleId = useId();
|
||||
const colorId = useId();
|
||||
const opacityId = useId();
|
||||
const sizeId = useId();
|
||||
const symbol1Id = useId();
|
||||
const symbolVertId = useId();
|
||||
const color1Id = useId();
|
||||
const fillOpacityId = useId();
|
||||
const rotationAngle1Id = useId();
|
||||
const textId = useId();
|
||||
const fontSizeId = useId();
|
||||
const xOffsetId = useId();
|
||||
const yOffsetId = useId();
|
||||
|
||||
// Simple fixed value display
|
||||
if (settings?.simpleFixedValues) {
|
||||
return (
|
||||
@@ -122,6 +137,7 @@ export const StyleEditor = (props: Props) => {
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('geomap.style-editor.label-symbol', 'Symbol')}>
|
||||
<ResourceDimensionEditor
|
||||
id={symbolId}
|
||||
value={value?.symbol ?? defaultStyleConfig.symbol}
|
||||
context={context}
|
||||
onChange={onSymbolChange}
|
||||
@@ -147,6 +163,7 @@ export const StyleEditor = (props: Props) => {
|
||||
</InlineFieldRow>
|
||||
<Field label={t('geomap.style-editor.label-rotation-angle', 'Rotation angle')}>
|
||||
<ScalarDimensionEditor
|
||||
id={rotationAngleId}
|
||||
value={value?.rotation ?? defaultStyleConfig.rotation}
|
||||
context={context}
|
||||
onChange={onRotationChange}
|
||||
@@ -166,6 +183,7 @@ export const StyleEditor = (props: Props) => {
|
||||
<InlineField label={t('geomap.style-editor.label-color', 'Color')} labelWidth={10}>
|
||||
<InlineLabel width={4}>
|
||||
<ColorPicker
|
||||
id={colorId}
|
||||
color={value?.color?.fixed ?? defaultStyleConfig.color.fixed}
|
||||
onChange={(v) => {
|
||||
onColorChange({ fixed: v });
|
||||
@@ -177,6 +195,7 @@ export const StyleEditor = (props: Props) => {
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('geomap.style-editor.label-opacity', 'Opacity')} labelWidth={10} grow>
|
||||
<SliderValueEditor
|
||||
id={opacityId}
|
||||
value={value?.opacity ?? defaultStyleConfig.opacity}
|
||||
context={context}
|
||||
onChange={onOpacityChange}
|
||||
@@ -200,6 +219,7 @@ export const StyleEditor = (props: Props) => {
|
||||
<>
|
||||
<Field label={t('geomap.style-editor.label-size', 'Size')}>
|
||||
<ScaleDimensionEditor
|
||||
id={sizeId}
|
||||
value={value?.size ?? defaultStyleConfig.size}
|
||||
context={context}
|
||||
onChange={onSizeChange}
|
||||
@@ -217,6 +237,7 @@ export const StyleEditor = (props: Props) => {
|
||||
<>
|
||||
<Field label={t('geomap.style-editor.label-symbol', 'Symbol')}>
|
||||
<ResourceDimensionEditor
|
||||
id={symbol1Id}
|
||||
value={value?.symbol ?? defaultStyleConfig.symbol}
|
||||
context={context}
|
||||
onChange={onSymbolChange}
|
||||
@@ -241,6 +262,7 @@ export const StyleEditor = (props: Props) => {
|
||||
</Field>
|
||||
<Field label={t('geomap.style-editor.label-symbol-vertical-align', 'Symbol vertical align')}>
|
||||
<RadioButtonGroup
|
||||
id={symbolVertId}
|
||||
value={value?.symbolAlign?.vertical ?? defaultStyleConfig.symbolAlign.vertical}
|
||||
onChange={onAlignVerticalChange}
|
||||
options={[
|
||||
@@ -280,6 +302,7 @@ export const StyleEditor = (props: Props) => {
|
||||
)}
|
||||
<Field label={t('geomap.style-editor.label-color', 'Color')}>
|
||||
<ColorDimensionEditor
|
||||
id={color1Id}
|
||||
value={value?.color ?? defaultStyleConfig.color}
|
||||
context={context}
|
||||
onChange={onColorChange}
|
||||
@@ -288,6 +311,7 @@ export const StyleEditor = (props: Props) => {
|
||||
</Field>
|
||||
<Field label={t('geomap.style-editor.label-fill-opacity', 'Fill opacity')}>
|
||||
<SliderValueEditor
|
||||
id={fillOpacityId}
|
||||
value={value?.opacity ?? defaultStyleConfig.opacity}
|
||||
context={context}
|
||||
onChange={onOpacityChange}
|
||||
@@ -305,6 +329,7 @@ export const StyleEditor = (props: Props) => {
|
||||
{settings?.displayRotation && (
|
||||
<Field label={t('geomap.style-editor.label-rotation-angle', 'Rotation angle')}>
|
||||
<ScalarDimensionEditor
|
||||
id={rotationAngle1Id}
|
||||
value={value?.rotation ?? defaultStyleConfig.rotation}
|
||||
context={context}
|
||||
onChange={onRotationChange}
|
||||
@@ -321,6 +346,7 @@ export const StyleEditor = (props: Props) => {
|
||||
)}
|
||||
<Field label={t('geomap.style-editor.label-text-label', 'Text label')}>
|
||||
<TextDimensionEditor
|
||||
id={textId}
|
||||
value={value?.text ?? defaultTextConfig}
|
||||
context={context}
|
||||
onChange={onTextChange}
|
||||
@@ -333,6 +359,7 @@ export const StyleEditor = (props: Props) => {
|
||||
<Stack>
|
||||
<Field label={t('geomap.style-editor.label-font-size', 'Font size')}>
|
||||
<NumberValueEditor
|
||||
id={fontSizeId}
|
||||
value={value?.textConfig?.fontSize ?? defaultStyleConfig.textConfig.fontSize}
|
||||
context={context}
|
||||
onChange={onTextFontSizeChange}
|
||||
@@ -341,6 +368,7 @@ export const StyleEditor = (props: Props) => {
|
||||
</Field>
|
||||
<Field label={t('geomap.style-editor.label-x-offset', 'X offset')}>
|
||||
<NumberValueEditor
|
||||
id={xOffsetId}
|
||||
value={value?.textConfig?.offsetX ?? defaultStyleConfig.textConfig.offsetX}
|
||||
context={context}
|
||||
onChange={onTextOffsetXChange}
|
||||
@@ -349,6 +377,7 @@ export const StyleEditor = (props: Props) => {
|
||||
</Field>
|
||||
<Field label={t('geomap.style-editor.label-y-offset', 'Y offset')}>
|
||||
<NumberValueEditor
|
||||
id={yOffsetId}
|
||||
value={value?.textConfig?.offsetY ?? defaultStyleConfig.textConfig.offsetY}
|
||||
context={context}
|
||||
onChange={onTextOffsetYChange}
|
||||
|
||||
@@ -4,13 +4,14 @@ import { StandardEditorProps } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Switch } from '@grafana/ui';
|
||||
|
||||
export function PaginationEditor({ onChange, value }: StandardEditorProps<boolean>) {
|
||||
export function PaginationEditor({ onChange, value, id }: StandardEditorProps<boolean>) {
|
||||
const changeValue = (event: React.FormEvent<HTMLInputElement> | undefined) => {
|
||||
onChange(event?.currentTarget.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
id={id}
|
||||
label={selectors.components.PanelEditor.OptionsPane.fieldLabel(`Enable pagination`)}
|
||||
value={Boolean(value)}
|
||||
onChange={changeValue}
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface TableCellEditorProps<T> {
|
||||
interface Props {
|
||||
value: TableCellOptions;
|
||||
onChange: (v: TableCellOptions) => void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const TEXT_WRAP_CELL_TYPES = new Set([
|
||||
@@ -40,7 +41,7 @@ function isTextWrapCellType(value: TableCellOptions): value is TableCellOptions
|
||||
return TEXT_WRAP_CELL_TYPES.has(value.type);
|
||||
}
|
||||
|
||||
export const TableCellOptionEditor = ({ value, onChange }: Props) => {
|
||||
export const TableCellOptionEditor = ({ value, onChange, id }: Props) => {
|
||||
const cellType = value.type;
|
||||
const styles = useStyles2(getStyles);
|
||||
const cellDisplayModeOptions: Array<ComboboxOption<TableCellOptions['type']>> = [
|
||||
@@ -92,7 +93,7 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => {
|
||||
return (
|
||||
<div className={styles.fixBottomMargin}>
|
||||
<Field>
|
||||
<Combobox options={cellDisplayModeOptions} value={currentMode} onChange={onCellTypeChange} />
|
||||
<Combobox id={id} options={cellDisplayModeOptions} value={currentMode} onChange={onCellTypeChange} />
|
||||
</Field>
|
||||
{isTextWrapCellType(value) && <TextWrapOptionsEditor cellOptions={value} onChange={onCellOptionsChange} />}
|
||||
{cellType === TableCellDisplayMode.Gauge && (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useId } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
@@ -26,6 +28,8 @@ export const ColorBackgroundCellOptionsEditor = ({
|
||||
onChange(cellOptions);
|
||||
};
|
||||
|
||||
const applyToRowSwitchId = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
@@ -47,6 +51,7 @@ export const ColorBackgroundCellOptionsEditor = ({
|
||||
)}
|
||||
>
|
||||
<Switch
|
||||
id={applyToRowSwitchId}
|
||||
label={selectors.components.PanelEditor.OptionsPane.fieldLabel(`Apply to entire row`)}
|
||||
value={cellOptions.applyToRow}
|
||||
onChange={onColorRowChange}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FormEvent } from 'react';
|
||||
import { FormEvent, useId } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { TableImageCellOptions } from '@grafana/schema';
|
||||
@@ -17,6 +17,9 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
|
||||
onChange(cellOptions);
|
||||
};
|
||||
|
||||
const altTextInputId = useId();
|
||||
const titleTextInputId = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
@@ -26,7 +29,7 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
|
||||
"Alternative text that will be displayed if an image can't be displayed or for users who use a screen reader"
|
||||
)}
|
||||
>
|
||||
<Input onChange={onAltChange} defaultValue={cellOptions.alt} />
|
||||
<Input id={altTextInputId} onChange={onAltChange} defaultValue={cellOptions.alt} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
@@ -36,7 +39,7 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
|
||||
'Text that will be displayed when the image is hovered by a cursor'
|
||||
)}
|
||||
>
|
||||
<Input onChange={onTitleChange} defaultValue={cellOptions.title} />
|
||||
<Input id={titleTextInputId} onChange={onTitleChange} defaultValue={cellOptions.title} />
|
||||
</Field>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
import { useId, useMemo } from 'react';
|
||||
|
||||
import { createFieldConfigRegistry, SetFieldConfigOptionsArgs } from '@grafana/data';
|
||||
import { GraphFieldConfig, TableSparklineCellOptions } from '@grafana/schema';
|
||||
@@ -51,6 +51,8 @@ export const SparklineCellOptionsEditor = (props: TableCellEditorProps<TableSpar
|
||||
|
||||
const values = { ...defaultSparklineCellConfig, ...cellOptions };
|
||||
|
||||
const htmlIdBase = useId();
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
{registry.list(optionIds.map((id) => `custom.${id}`)).map((item) => {
|
||||
@@ -67,6 +69,7 @@ export const SparklineCellOptionsEditor = (props: TableCellEditorProps<TableSpar
|
||||
value={(isOptionKey(path, values) ? values[path] : undefined) ?? item.defaultValue}
|
||||
item={item}
|
||||
context={{ data: [] }}
|
||||
id={`${htmlIdBase}${item.id}`}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useId } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { TableCellOptions, TableWrapTextOptions } from '@grafana/schema';
|
||||
@@ -15,10 +17,13 @@ export const TextWrapOptionsEditor = ({
|
||||
onChange(cellOptions);
|
||||
};
|
||||
|
||||
const htmlId = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field label={t('table.text-wrap-options.label-wrap-text', 'Wrap text')}>
|
||||
<Switch
|
||||
id={htmlId}
|
||||
label={selectors.components.PanelEditor.OptionsPane.fieldLabel(`Wrap text`)}
|
||||
value={cellOptions.wrapText}
|
||||
onChange={onWrapTextChange}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Fragment, useId, useState } from 'react';
|
||||
import { usePrevious } from 'react-use';
|
||||
|
||||
import {
|
||||
@@ -77,6 +77,12 @@ export const SeriesEditor = ({
|
||||
});
|
||||
});
|
||||
|
||||
const frameInputId = useId();
|
||||
const xFieldInputId = useId();
|
||||
const yFieldInputId = useId();
|
||||
const sizeFieldInputId = useId();
|
||||
const colorFieldInputId = useId();
|
||||
|
||||
return (
|
||||
<>
|
||||
{mapping === SeriesMapping.Manual && (
|
||||
@@ -129,6 +135,7 @@ export const SeriesEditor = ({
|
||||
<Fragment key={formKey}>
|
||||
<Field label={t('xychart.series-editor.label-frame', 'Frame')}>
|
||||
<Select
|
||||
inputId={frameInputId}
|
||||
placeholder={
|
||||
mapping === SeriesMapping.Auto
|
||||
? t('xychart.series-editor.placeholder-all-frames', 'All frames')
|
||||
@@ -158,6 +165,7 @@ export const SeriesEditor = ({
|
||||
</Field>
|
||||
<Field label={t('xychart.series-editor.label-x-field', 'X field')}>
|
||||
<FieldNamePicker
|
||||
id={xFieldInputId}
|
||||
value={series.x?.matcher.options as string}
|
||||
context={context}
|
||||
onChange={(fieldName) => {
|
||||
@@ -195,6 +203,7 @@ export const SeriesEditor = ({
|
||||
</Field>
|
||||
<Field label={t('xychart.series-editor.label-y-field', 'Y field')}>
|
||||
<FieldNamePicker
|
||||
id={yFieldInputId}
|
||||
value={series.y?.matcher?.options as string}
|
||||
context={context}
|
||||
onChange={(fieldName) => {
|
||||
@@ -233,6 +242,7 @@ export const SeriesEditor = ({
|
||||
</Field>
|
||||
<Field label={t('xychart.series-editor.label-size-field', 'Size field')}>
|
||||
<FieldNamePicker
|
||||
id={sizeFieldInputId}
|
||||
value={series.size?.matcher?.options as string}
|
||||
context={context}
|
||||
onChange={(fieldName) => {
|
||||
@@ -268,6 +278,7 @@ export const SeriesEditor = ({
|
||||
</Field>
|
||||
<Field label={t('xychart.series-editor.label-color-field', 'Color field')}>
|
||||
<FieldNamePicker
|
||||
id={colorFieldInputId}
|
||||
value={series.color?.matcher?.options as string}
|
||||
context={context}
|
||||
onChange={(fieldName) => {
|
||||
|
||||
Reference in New Issue
Block a user