|
|
@ -1,25 +1,27 @@ |
|
|
|
import { SelectableValue } from '@grafana/data'; |
|
|
|
import { capitalize } from 'lodash'; |
|
|
|
import { Select } from '@grafana/ui'; |
|
|
|
import React, { useEffect } from 'react'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { Button, Combobox, ComboboxOption, Field, InlineSwitch, Input, Stack } from '@grafana/ui'; |
|
|
|
import { t } from 'app/core/internationalization'; |
|
|
|
import { t } from 'app/core/internationalization'; |
|
|
|
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; |
|
|
|
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; |
|
|
|
|
|
|
|
|
|
|
|
import { ResponsiveGridLayoutManager } from './ResponsiveGridLayoutManager'; |
|
|
|
import { AutoGridColumnWidth, AutoGridRowHeight, ResponsiveGridLayoutManager } from './ResponsiveGridLayoutManager'; |
|
|
|
|
|
|
|
|
|
|
|
const sizes = [100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 650]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getEditOptions(layoutManager: ResponsiveGridLayoutManager): OptionsPaneItemDescriptor[] { |
|
|
|
export function getEditOptions(layoutManager: ResponsiveGridLayoutManager): OptionsPaneItemDescriptor[] { |
|
|
|
const options: OptionsPaneItemDescriptor[] = []; |
|
|
|
const options: OptionsPaneItemDescriptor[] = []; |
|
|
|
|
|
|
|
|
|
|
|
options.push( |
|
|
|
options.push( |
|
|
|
new OptionsPaneItemDescriptor({ |
|
|
|
new OptionsPaneItemDescriptor({ |
|
|
|
title: t('dashboard.responsive-layout.options.columns', 'Columns'), |
|
|
|
title: 'Column options', |
|
|
|
|
|
|
|
skipField: true, |
|
|
|
render: () => <GridLayoutColumns layoutManager={layoutManager} />, |
|
|
|
render: () => <GridLayoutColumns layoutManager={layoutManager} />, |
|
|
|
}) |
|
|
|
}) |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
options.push( |
|
|
|
options.push( |
|
|
|
new OptionsPaneItemDescriptor({ |
|
|
|
new OptionsPaneItemDescriptor({ |
|
|
|
title: t('dashboard.responsive-layout.options.rows', 'Rows'), |
|
|
|
title: 'Row height options', |
|
|
|
|
|
|
|
skipField: true, |
|
|
|
render: () => <GridLayoutRows layoutManager={layoutManager} />, |
|
|
|
render: () => <GridLayoutRows layoutManager={layoutManager} />, |
|
|
|
}) |
|
|
|
}) |
|
|
|
); |
|
|
|
); |
|
|
@ -28,56 +30,210 @@ export function getEditOptions(layoutManager: ResponsiveGridLayoutManager): Opti |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function GridLayoutColumns({ layoutManager }: { layoutManager: ResponsiveGridLayoutManager }) { |
|
|
|
function GridLayoutColumns({ layoutManager }: { layoutManager: ResponsiveGridLayoutManager }) { |
|
|
|
const { templateColumns } = layoutManager.state.layout.useState(); |
|
|
|
const { maxColumnCount, columnWidth } = layoutManager.useState(); |
|
|
|
|
|
|
|
const [inputRef, setInputRef] = React.useState<HTMLInputElement | null>(null); |
|
|
|
|
|
|
|
const [focusInput, setFocusInput] = React.useState(false); |
|
|
|
|
|
|
|
const [customMinWidthError, setCustomMinWidthError] = React.useState(false); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
|
|
if (focusInput && inputRef) { |
|
|
|
|
|
|
|
inputRef.focus(); |
|
|
|
|
|
|
|
setFocusInput(false); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, [focusInput, inputRef]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const minWidthOptions: Array<ComboboxOption<AutoGridColumnWidth>> = [ |
|
|
|
|
|
|
|
'narrow' as const, |
|
|
|
|
|
|
|
'standard' as const, |
|
|
|
|
|
|
|
'wide' as const, |
|
|
|
|
|
|
|
'custom' as const, |
|
|
|
|
|
|
|
].map((value) => ({ |
|
|
|
|
|
|
|
label: capitalize(value), |
|
|
|
|
|
|
|
value, |
|
|
|
|
|
|
|
})); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isStandardMinWidth = typeof columnWidth === 'string'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const minWidthLabel = isStandardMinWidth |
|
|
|
|
|
|
|
? t('dashboard.responsive-layout.options.min-width', 'Min column width') |
|
|
|
|
|
|
|
: t('dashboard.responsive-layout.options.min-width-custom', 'Custom min width'); |
|
|
|
|
|
|
|
const colOptions = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'].map((value) => ({ label: value, value })); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onCustomMinWidthChanged = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
|
|
|
|
|
const pixels = parseInt(e.target.value, 10); |
|
|
|
|
|
|
|
if (isNaN(pixels) || pixels < 50 || pixels > 2000) { |
|
|
|
|
|
|
|
setCustomMinWidthError(true); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} else if (customMinWidthError) { |
|
|
|
|
|
|
|
setCustomMinWidthError(false); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const colOptions: Array<SelectableValue<string>> = [ |
|
|
|
layoutManager.onColumnWidthChanged(pixels); |
|
|
|
{ label: t('dashboard.responsive-layout.options.one-column', '1 column'), value: `1fr` }, |
|
|
|
}; |
|
|
|
{ label: t('dashboard.responsive-layout.options.two-columns', '2 columns'), value: `1fr 1fr` }, |
|
|
|
|
|
|
|
{ label: t('dashboard.responsive-layout.options.three-columns', '3 columns'), value: `1fr 1fr 1fr` }, |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const size of sizes) { |
|
|
|
const onNamedMinWidthChanged = (value: ComboboxOption<AutoGridColumnWidth>) => { |
|
|
|
colOptions.push({ |
|
|
|
if (value.value === 'custom') { |
|
|
|
label: t('dashboard.responsive-layout.options.min', 'Min: {{size}}px', { size }), |
|
|
|
setFocusInput(true); |
|
|
|
value: `repeat(auto-fit, minmax(${size}px, auto))`, |
|
|
|
} |
|
|
|
}); |
|
|
|
layoutManager.onColumnWidthChanged(value.value); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onClearCustomMinWidth = () => { |
|
|
|
|
|
|
|
if (customMinWidthError) { |
|
|
|
|
|
|
|
setCustomMinWidthError(false); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
layoutManager.onColumnWidthChanged('standard'); |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<Select |
|
|
|
<Stack gap={2} justifyContent={'stretch'}> |
|
|
|
options={colOptions} |
|
|
|
<Field |
|
|
|
value={String(templateColumns)} |
|
|
|
label={minWidthLabel} |
|
|
|
onChange={({ value }) => layoutManager.changeColumns(value!)} |
|
|
|
invalid={customMinWidthError} |
|
|
|
allowCustomValue={true} |
|
|
|
error={ |
|
|
|
/> |
|
|
|
customMinWidthError |
|
|
|
|
|
|
|
? t('dashboard.responsive-layout.options.min-width-error', 'A number between 50 and 2000 is required') |
|
|
|
|
|
|
|
: undefined |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{isStandardMinWidth ? ( |
|
|
|
|
|
|
|
<Combobox options={minWidthOptions} value={columnWidth} onChange={onNamedMinWidthChanged} /> |
|
|
|
|
|
|
|
) : ( |
|
|
|
|
|
|
|
<Input |
|
|
|
|
|
|
|
defaultValue={columnWidth} |
|
|
|
|
|
|
|
onBlur={onCustomMinWidthChanged} |
|
|
|
|
|
|
|
ref={(ref) => setInputRef(ref)} |
|
|
|
|
|
|
|
type="number" |
|
|
|
|
|
|
|
min={50} |
|
|
|
|
|
|
|
max={2000} |
|
|
|
|
|
|
|
invalid={customMinWidthError} |
|
|
|
|
|
|
|
suffix={ |
|
|
|
|
|
|
|
<Button |
|
|
|
|
|
|
|
size="sm" |
|
|
|
|
|
|
|
fill="text" |
|
|
|
|
|
|
|
icon="times" |
|
|
|
|
|
|
|
tooltip={t( |
|
|
|
|
|
|
|
'dashboard.responsive-layout.options.min-width-custom-clear', |
|
|
|
|
|
|
|
'Back to standard min column width' |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
onClick={onClearCustomMinWidth} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{t('dashboard.responsive-layout.options.custom-min-width.clear', 'Clear')} |
|
|
|
|
|
|
|
</Button> |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
</Field> |
|
|
|
|
|
|
|
<Field label={t('dashboard.responsive-layout.options.max-columns', 'Max columns')}> |
|
|
|
|
|
|
|
<Combobox |
|
|
|
|
|
|
|
options={colOptions} |
|
|
|
|
|
|
|
value={String(maxColumnCount)} |
|
|
|
|
|
|
|
onChange={({ value }) => layoutManager.onMaxColumnCountChanged(parseInt(value, 10))} |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
</Field> |
|
|
|
|
|
|
|
</Stack> |
|
|
|
); |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function GridLayoutRows({ layoutManager }: { layoutManager: ResponsiveGridLayoutManager }) { |
|
|
|
function GridLayoutRows({ layoutManager }: { layoutManager: ResponsiveGridLayoutManager }) { |
|
|
|
const { autoRows } = layoutManager.state.layout.useState(); |
|
|
|
const { rowHeight, fillScreen } = layoutManager.useState(); |
|
|
|
|
|
|
|
const [inputRef, setInputRef] = React.useState<HTMLInputElement | null>(null); |
|
|
|
|
|
|
|
const [focusInput, setFocusInput] = React.useState(false); |
|
|
|
|
|
|
|
const [customMinWidthError, setCustomMinWidthError] = React.useState(false); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
|
|
if (focusInput && inputRef) { |
|
|
|
|
|
|
|
inputRef.focus(); |
|
|
|
|
|
|
|
setFocusInput(false); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, [focusInput, inputRef]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const minWidthOptions: Array<ComboboxOption<AutoGridRowHeight>> = [ |
|
|
|
|
|
|
|
'short' as const, |
|
|
|
|
|
|
|
'standard' as const, |
|
|
|
|
|
|
|
'tall' as const, |
|
|
|
|
|
|
|
'custom' as const, |
|
|
|
|
|
|
|
].map((value) => ({ |
|
|
|
|
|
|
|
label: capitalize(value), |
|
|
|
|
|
|
|
value, |
|
|
|
|
|
|
|
})); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isStandardHeight = typeof rowHeight === 'string'; |
|
|
|
|
|
|
|
const rowHeightLabel = rowHeight |
|
|
|
|
|
|
|
? t('dashboard.responsive-layout.options.min-height', 'Row height') |
|
|
|
|
|
|
|
: t('dashboard.responsive-layout.options.min-height-custom', 'Custom row height'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onCustomHeightChanged = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
|
|
|
|
|
const pixels = parseInt(e.target.value, 10); |
|
|
|
|
|
|
|
if (isNaN(pixels) || pixels < 50 || pixels > 2000) { |
|
|
|
|
|
|
|
setCustomMinWidthError(true); |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} else if (customMinWidthError) { |
|
|
|
|
|
|
|
setCustomMinWidthError(false); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
layoutManager.onRowHeightChanged(pixels); |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const rowOptions: Array<SelectableValue<string>> = []; |
|
|
|
const onNamedMinHeightChanged = (value: ComboboxOption<AutoGridRowHeight>) => { |
|
|
|
|
|
|
|
if (value.value === 'custom') { |
|
|
|
|
|
|
|
setFocusInput(true); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
layoutManager.onRowHeightChanged(value.value); |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
for (const size of sizes) { |
|
|
|
const onClearCustomRowHeight = () => { |
|
|
|
rowOptions.push({ |
|
|
|
if (customMinWidthError) { |
|
|
|
label: t('dashboard.responsive-layout.options.min', 'Min: {{size}}px', { size }), |
|
|
|
setCustomMinWidthError(false); |
|
|
|
value: `minmax(${size}px, auto)`, |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const size of sizes) { |
|
|
|
layoutManager.onRowHeightChanged('standard'); |
|
|
|
rowOptions.push({ |
|
|
|
}; |
|
|
|
label: t('dashboard.responsive-layout.options.fixed', 'Fixed: {{size}}px', { size }), |
|
|
|
|
|
|
|
value: `${size}px`, |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<Select |
|
|
|
<Stack gap={2} wrap={true}> |
|
|
|
options={rowOptions} |
|
|
|
<Field |
|
|
|
value={String(autoRows)} |
|
|
|
label={rowHeightLabel} |
|
|
|
onChange={({ value }) => layoutManager.changeRows(value!)} |
|
|
|
invalid={customMinWidthError} |
|
|
|
allowCustomValue={true} |
|
|
|
error={ |
|
|
|
/> |
|
|
|
customMinWidthError |
|
|
|
|
|
|
|
? t('dashboard.responsive-layout.options.min-height-error', 'A number between 50 and 2000 is required') |
|
|
|
|
|
|
|
: undefined |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{isStandardHeight ? ( |
|
|
|
|
|
|
|
<Combobox options={minWidthOptions} value={rowHeight} onChange={onNamedMinHeightChanged} width={18} /> |
|
|
|
|
|
|
|
) : ( |
|
|
|
|
|
|
|
<Input |
|
|
|
|
|
|
|
defaultValue={rowHeight} |
|
|
|
|
|
|
|
onBlur={onCustomHeightChanged} |
|
|
|
|
|
|
|
ref={(ref) => setInputRef(ref)} |
|
|
|
|
|
|
|
width={18} |
|
|
|
|
|
|
|
type="number" |
|
|
|
|
|
|
|
min={50} |
|
|
|
|
|
|
|
max={2000} |
|
|
|
|
|
|
|
invalid={customMinWidthError} |
|
|
|
|
|
|
|
suffix={ |
|
|
|
|
|
|
|
<Button |
|
|
|
|
|
|
|
size="sm" |
|
|
|
|
|
|
|
fill="text" |
|
|
|
|
|
|
|
icon="times" |
|
|
|
|
|
|
|
tooltip={t( |
|
|
|
|
|
|
|
'dashboard.responsive-layout.options.min-width-custom-clear', |
|
|
|
|
|
|
|
'Back to standard min column width' |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
onClick={onClearCustomRowHeight} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{t('dashboard.responsive-layout.options.custom-min-height.clear', 'Clear')} |
|
|
|
|
|
|
|
</Button> |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
</Field> |
|
|
|
|
|
|
|
<Field label={t('dashboard.responsive-layout.options.height-fill', 'Fill screen')}> |
|
|
|
|
|
|
|
<InlineSwitch value={fillScreen} onChange={() => layoutManager.onFillScreenChanged(!fillScreen)} /> |
|
|
|
|
|
|
|
</Field> |
|
|
|
|
|
|
|
</Stack> |
|
|
|
); |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|