Dashboard: Redesign row edit panes (#101510)
* add row item changes * redesign row edit panes and header actions * clean up after merges * adjust to sentence casing * bring back layout selection extra options
This commit is contained in:
@@ -25,7 +25,7 @@ export class DashboardEditableElement implements EditableDashboardElement {
|
||||
const { body } = dashboard.useState();
|
||||
|
||||
const dashboardOptions = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.options.title', 'Dashboard options'),
|
||||
id: 'dashboard-options',
|
||||
isOpenable: false,
|
||||
@@ -48,6 +48,14 @@ export class DashboardEditableElement implements EditableDashboardElement {
|
||||
render: () => <DashboardLayoutSelector layoutManager={body} />,
|
||||
})
|
||||
);
|
||||
|
||||
if (body.getOptions) {
|
||||
for (const option of body.getOptions()) {
|
||||
editPaneHeaderOptions.addItem(option);
|
||||
}
|
||||
}
|
||||
|
||||
return editPaneHeaderOptions;
|
||||
}, [body, dashboard]);
|
||||
|
||||
return [dashboardOptions];
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ export class MultiSelectedVizPanelsEditableElement implements MultiSelectedEdita
|
||||
isOpenable: false,
|
||||
renderTitle: () => (
|
||||
<EditPaneHeader
|
||||
title={t('dashboard.layout.common.panels-title', '{{length}} Panels Selected', {
|
||||
title={t('dashboard.layout.common.panels-title', '{{length}} panels selected', {
|
||||
length: this._panels.length,
|
||||
})}
|
||||
onDelete={() => this.onDelete()}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { SceneObjectState, SceneObjectBase, sceneGraph, VariableDependencyConfig, SceneObject } from '@grafana/scenes';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { getDefaultVizPanel } from '../../utils/utils';
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { BulkActionElement } from '../types/BulkActionElement';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
|
||||
import { LayoutParent } from '../types/LayoutParent';
|
||||
|
||||
import { getEditOptions, renderActions } from './RowItemEditor';
|
||||
import { getEditOptions } from './RowItemEditor';
|
||||
import { RowItemRenderer } from './RowItemRenderer';
|
||||
import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior';
|
||||
import { RowItems } from './RowItems';
|
||||
@@ -60,19 +59,42 @@ export class RowItem
|
||||
return getEditOptions(this);
|
||||
}
|
||||
|
||||
public renderActions(): ReactNode {
|
||||
return renderActions(this);
|
||||
}
|
||||
|
||||
public onDelete() {
|
||||
const layout = sceneGraph.getAncestor(this, RowsLayoutManager);
|
||||
layout.removeRow(this);
|
||||
this._getParentLayout().removeRow(this);
|
||||
}
|
||||
|
||||
public createMultiSelectedElement(items: SceneObject[]): RowItems {
|
||||
return new RowItems(items.filter((item) => item instanceof RowItem));
|
||||
}
|
||||
|
||||
public onAddPanel(panel = getDefaultVizPanel()) {
|
||||
this.getLayout().addPanel(panel);
|
||||
}
|
||||
|
||||
public onAddRowAbove() {
|
||||
this._getParentLayout().addRowAbove(this);
|
||||
}
|
||||
|
||||
public onAddRowBelow() {
|
||||
this._getParentLayout().addRowBelow(this);
|
||||
}
|
||||
|
||||
public onMoveUp() {
|
||||
this._getParentLayout().moveRowUp(this);
|
||||
}
|
||||
|
||||
public onMoveDown() {
|
||||
this._getParentLayout().moveRowDown(this);
|
||||
}
|
||||
|
||||
public isFirstRow(): boolean {
|
||||
return this._getParentLayout().isFirstRow(this);
|
||||
}
|
||||
|
||||
public isLastRow(): boolean {
|
||||
return this._getParentLayout().isLastRow(this);
|
||||
}
|
||||
|
||||
public getRepeatVariable(): string | undefined {
|
||||
return this._getRepeatBehavior()?.state.variableName;
|
||||
}
|
||||
@@ -110,6 +132,10 @@ export class RowItem
|
||||
this.setState({ isCollapsed: !this.state.isCollapsed });
|
||||
}
|
||||
|
||||
private _getParentLayout(): RowsLayoutManager {
|
||||
return sceneGraph.getAncestor(this, RowsLayoutManager);
|
||||
}
|
||||
|
||||
private _getRepeatBehavior(): RowItemRepeaterBehavior | undefined {
|
||||
return this.state.$behaviors?.find((b) => b instanceof RowItemRepeaterBehavior);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Alert, Button, Input, RadioButtonGroup, Switch, TextLink } from '@grafana/ui';
|
||||
import { Alert, Input, Switch, TextLink } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
@@ -10,18 +9,25 @@ import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSel
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
|
||||
import { EditPaneHeader } from '../../edit-pane/EditPaneHeader';
|
||||
import { getDashboardSceneFor, getQueryRunnerFor } from '../../utils/utils';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
|
||||
import { DashboardLayoutSelector } from '../layouts-shared/DashboardLayoutSelector';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
|
||||
export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] {
|
||||
const { layout } = model.useState();
|
||||
const rowOptions = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.title', 'Row options'),
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
|
||||
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.title', 'Row'),
|
||||
id: 'row-options',
|
||||
isOpenDefault: true,
|
||||
isOpenable: false,
|
||||
renderTitle: () => (
|
||||
<EditPaneHeader title={t('dashboard.rows-layout.row-options.title', 'Row')} onDelete={() => model.onDelete()} />
|
||||
),
|
||||
})
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
@@ -31,8 +37,22 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
|
||||
)
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.height.title', 'Height'),
|
||||
render: () => <RowHeightSelect row={model} />,
|
||||
title: t('dashboard.layout.common.layout', 'Layout'),
|
||||
render: () => <DashboardLayoutSelector layoutManager={layout} />,
|
||||
})
|
||||
);
|
||||
|
||||
if (layout.getOptions) {
|
||||
for (const option of layout.getOptions()) {
|
||||
editPaneHeaderOptions.addItem(option);
|
||||
}
|
||||
}
|
||||
|
||||
editPaneHeaderOptions
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Repeat for'),
|
||||
render: () => <RowRepeatSelect row={model} dashboard={dashboard} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
@@ -41,42 +61,23 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
|
||||
render: () => <RowHeaderSwitch row={model} />,
|
||||
})
|
||||
);
|
||||
}, [model]);
|
||||
|
||||
const rowRepeatOptions = useMemo(() => {
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
return editPaneHeaderOptions;
|
||||
}, [layout, model]);
|
||||
|
||||
return new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.repeat.title', 'Repeat options'),
|
||||
id: 'row-repeat-options',
|
||||
isOpenDefault: true,
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Variable'),
|
||||
render: () => <RowRepeatSelect row={model} dashboard={dashboard} />,
|
||||
})
|
||||
);
|
||||
}, [model]);
|
||||
|
||||
const { layout } = model.useState();
|
||||
const layoutOptions = useLayoutCategory(layout);
|
||||
|
||||
return [rowOptions, rowRepeatOptions, layoutOptions];
|
||||
}
|
||||
|
||||
export function renderActions(model: RowItem) {
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="secondary" icon="copy" />
|
||||
<Button size="sm" variant="destructive" fill="outline" onClick={() => model.onDelete()} icon="trash-alt" />
|
||||
</>
|
||||
);
|
||||
return [rowOptions];
|
||||
}
|
||||
|
||||
function RowTitleInput({ row }: { row: RowItem }) {
|
||||
const { title } = row.useState();
|
||||
|
||||
return <Input value={title} onChange={(e) => row.onChangeTitle(e.currentTarget.value)} />;
|
||||
return (
|
||||
<Input
|
||||
title={t('dashboard.rows-layout.row-options.title-option', 'Title')}
|
||||
value={title}
|
||||
onChange={(e) => row.onChangeTitle(e.currentTarget.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RowHeaderSwitch({ row }: { row: RowItem }) {
|
||||
@@ -85,17 +86,6 @@ function RowHeaderSwitch({ row }: { row: RowItem }) {
|
||||
return <Switch value={isHeaderHidden} onChange={() => row.onHeaderHiddenToggle()} />;
|
||||
}
|
||||
|
||||
function RowHeightSelect({ row }: { row: RowItem }) {
|
||||
const { height = 'expand' } = row.useState();
|
||||
|
||||
const options: Array<SelectableValue<'expand' | 'min'>> = [
|
||||
{ label: t('dashboard.rows-layout.row-options.height.expand', 'Expand'), value: 'expand' },
|
||||
{ label: t('dashboard.rows-layout.row-options.height.min', 'Min'), value: 'min' },
|
||||
];
|
||||
|
||||
return <RadioButtonGroup options={options} value={height} onChange={(option) => row.onChangeHeight(option)} />;
|
||||
}
|
||||
|
||||
function RowRepeatSelect({ row, dashboard }: { row: RowItem; dashboard: DashboardScene }) {
|
||||
const { layout } = row.useState();
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Dropdown, Menu, ToolbarButton, ToolbarButtonRow, useStyles2 } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
|
||||
interface RowItemMenuProps {
|
||||
model: RowItem;
|
||||
}
|
||||
|
||||
export function RowItemMenu({ model }: RowItemMenuProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<ToolbarButtonRow className={styles.container}>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
overlay={() => (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
ariaLabel={t('dashboard.rows-layout.row.menu.add-panel', 'Panel')}
|
||||
label={t('dashboard.rows-layout.row.menu.add-panel', 'Panel')}
|
||||
onClick={() => model.onAddPanel()}
|
||||
/>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
ariaLabel={t('dashboard.rows-layout.row.menu.add-row-above', 'Row above')}
|
||||
label={t('dashboard.rows-layout.row.menu.add-row-above', 'Row above')}
|
||||
onClick={() => model.onAddRowAbove()}
|
||||
/>
|
||||
<Menu.Item
|
||||
ariaLabel={t('dashboard.rows-layout.row.menu.add-row-below', 'Row below')}
|
||||
label={t('dashboard.rows-layout.row.menu.add-row-below', 'Row below')}
|
||||
onClick={() => model.onAddRowBelow()}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
<ToolbarButton
|
||||
aria-label={t('dashboard.rows-layout.row.menu.add', 'Add row')}
|
||||
title={t('dashboard.rows-layout.row.menu.add', 'Add row')}
|
||||
tooltip={t('dashboard.rows-layout.row.menu.add', 'Add row')}
|
||||
icon="plus"
|
||||
iconSize="sm"
|
||||
variant="default"
|
||||
>
|
||||
<Trans i18nKey="grafana-ui.tags-input.add">Add</Trans>
|
||||
</ToolbarButton>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
overlay={() => (
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
aria-label={t('dashboard.rows-layout.row.menu.move-up', 'Move row up')}
|
||||
label={t('dashboard.rows-layout.row.menu.move-up', 'Move row up')}
|
||||
onClick={() => model.onMoveUp()}
|
||||
disabled={model.isFirstRow()}
|
||||
/>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
aria-label={t('dashboard.rows-layout.row.menu.move-down', 'Move row down')}
|
||||
label={t('dashboard.rows-layout.row.menu.move-down', 'Move row down')}
|
||||
onClick={() => model.onMoveDown()}
|
||||
disabled={model.isLastRow()}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
<ToolbarButton
|
||||
aria-label={t('dashboard.rows-layout.row.menu.move-row', 'Move row')}
|
||||
title={t('dashboard.rows-layout.row.menu.move-row', 'Move row')}
|
||||
tooltip={t('dashboard.rows-layout.row.menu.move-row', 'Move row')}
|
||||
icon="arrows-v"
|
||||
iconSize="md"
|
||||
variant="default"
|
||||
/>
|
||||
</Dropdown>
|
||||
</ToolbarButtonRow>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
});
|
||||
@@ -4,13 +4,14 @@ import { useMemo, useRef } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { Button, Icon, useElementSelection, useStyles2 } from '@grafana/ui';
|
||||
import { Checkbox, clearButtonStyles, Icon, useElementSelection, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { isClonedKey } from '../../utils/clone';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
import { RowItemMenu } from './RowItemMenu';
|
||||
|
||||
export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
||||
const { layout, title, isCollapsed, height = 'expand', isHeaderHidden, key } = model.useState();
|
||||
@@ -18,6 +19,7 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const { isEditing, showHiddenElements } = dashboard.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const clearStyles = useStyles2(clearButtonStyles);
|
||||
const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text');
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const shouldGrow = !isCollapsed && height === 'expand';
|
||||
@@ -35,9 +37,14 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
||||
>
|
||||
{(!isHeaderHidden || (isEditing && showHiddenElements)) && (
|
||||
<div className={styles.rowHeader}>
|
||||
{!isClone && isEditing && (
|
||||
<div className={styles.checkboxWrapper} onPointerDown={onSelect}>
|
||||
<Checkbox value={!!isSelected} />
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => model.onCollapseToggle()}
|
||||
className={styles.rowTitleButton}
|
||||
className={cx(clearStyles, styles.rowTitleButton)}
|
||||
aria-label={
|
||||
isCollapsed
|
||||
? t('dashboard.rows-layout.row.expand', 'Expand row')
|
||||
@@ -50,9 +57,7 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
||||
{titleInterpolated}
|
||||
</span>
|
||||
</button>
|
||||
{!isClone && isEditing && (
|
||||
<Button icon="pen" variant="secondary" size="sm" fill="text" onPointerDown={(evt) => onSelect?.(evt)} />
|
||||
)}
|
||||
{!isClone && isEditing && <RowItemMenu model={model} />}
|
||||
</div>
|
||||
)}
|
||||
{!isCollapsed && <layout.Component model={layout} />}
|
||||
@@ -69,17 +74,6 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
padding: theme.spacing(0, 0, 0.5, 0),
|
||||
margin: theme.spacing(0, 0, 1, 0),
|
||||
alignItems: 'center',
|
||||
|
||||
'&:hover, &:focus-within': {
|
||||
'& > div': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
|
||||
'& > div': {
|
||||
marginBottom: 0,
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
rowTitleButton: css({
|
||||
display: 'flex',
|
||||
@@ -118,5 +112,9 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
display: 'flex',
|
||||
opacity: 0,
|
||||
}),
|
||||
checkboxWrapper: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { t } from 'app/core/internationalization';
|
||||
@@ -8,7 +7,7 @@ import { EditableDashboardElementInfo } from '../types/EditableDashboardElement'
|
||||
import { MultiSelectedEditableDashboardElement } from '../types/MultiSelectedEditableDashboardElement';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
import { getEditOptions, renderActions } from './RowItemsEditor';
|
||||
import { getEditOptions } from './RowItemsEditor';
|
||||
|
||||
export class RowItems implements MultiSelectedEditableDashboardElement {
|
||||
public readonly isMultiSelectedEditableDashboardElement = true;
|
||||
@@ -26,10 +25,6 @@ export class RowItems implements MultiSelectedEditableDashboardElement {
|
||||
return getEditOptions(this);
|
||||
}
|
||||
|
||||
public renderActions(): ReactNode {
|
||||
return renderActions(this);
|
||||
}
|
||||
|
||||
public getRows(): RowItem[] {
|
||||
return this._rows;
|
||||
}
|
||||
@@ -41,4 +36,6 @@ export class RowItems implements MultiSelectedEditableDashboardElement {
|
||||
public onHeaderHiddenToggle(value: boolean, indeterminate: boolean) {
|
||||
this._rows.forEach((row) => row.onHeaderHiddenToggle(indeterminate ? true : !value));
|
||||
}
|
||||
|
||||
public getNumberOfRowsSelected = () => this._rows.length;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { Button, Checkbox, Stack, Text } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { Checkbox } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { EditPaneHeader } from '../../edit-pane/EditPaneHeader';
|
||||
|
||||
import { RowItems } from './RowItems';
|
||||
|
||||
export function getEditOptions(model: RowItems): OptionsPaneCategoryDescriptor[] {
|
||||
const options = new OptionsPaneCategoryDescriptor({
|
||||
title: t('dashboard.edit-pane.row.multi-select.options-header', 'Multi-selected Row options'),
|
||||
title: '',
|
||||
id: `ms-row-options-${model.key}`,
|
||||
isOpenDefault: true,
|
||||
isOpenable: false,
|
||||
renderTitle: () => (
|
||||
<EditPaneHeader
|
||||
title={t('dashboard.edit-pane.row.multi-select.title', '{{length}} rows selected', {
|
||||
length: model.getNumberOfRowsSelected(),
|
||||
})}
|
||||
onDelete={() => model.onDelete()}
|
||||
/>
|
||||
),
|
||||
}).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.row.header.title', 'Row header'),
|
||||
@@ -20,24 +30,6 @@ export function getEditOptions(model: RowItems): OptionsPaneCategoryDescriptor[]
|
||||
return [options];
|
||||
}
|
||||
|
||||
export function renderActions(model: RowItems) {
|
||||
const rows = model.getRows();
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<Text>
|
||||
<Trans i18nKey="dashboard.edit-pane.row.multi-select.selection-number" values={{ length: rows.length }}>
|
||||
No. of rows selected: {{ length }}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Stack direction="row">
|
||||
<Button size="sm" variant="secondary" icon="copy" />
|
||||
<Button size="sm" variant="destructive" fill="outline" onClick={() => model.onDelete()} icon="trash-alt" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function RowHeaderCheckboxMulti({ model }: { model: RowItems }) {
|
||||
const rows = model.getRows();
|
||||
|
||||
|
||||
@@ -41,17 +41,17 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
// Try to add new panels to the selected row
|
||||
const selectedRows = dashboardSceneGraph.getAllSelectedObjects(this).filter((obj) => obj instanceof RowItem);
|
||||
if (selectedRows.length > 0) {
|
||||
return selectedRows.forEach((row) => row.getLayout().addPanel(vizPanel));
|
||||
return selectedRows.forEach((row) => row.onAddPanel(vizPanel));
|
||||
}
|
||||
|
||||
// If we don't have selected row add it to the first row
|
||||
if (this.state.rows.length > 0) {
|
||||
return this.state.rows[0].getLayout().addPanel(vizPanel);
|
||||
return this.state.rows[0].onAddPanel(vizPanel);
|
||||
}
|
||||
|
||||
// Otherwise fallback to adding a new row and a panel
|
||||
this.addNewRow();
|
||||
this.state.rows[this.state.rows.length - 1].getLayout().addPanel(vizPanel);
|
||||
this.state.rows[this.state.rows.length - 1].onAddPanel(vizPanel);
|
||||
}
|
||||
|
||||
public getVizPanels(): VizPanel[] {
|
||||
@@ -103,11 +103,81 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
});
|
||||
}
|
||||
|
||||
public addRowAbove(row: RowItem) {
|
||||
const rows = this.state.rows;
|
||||
const index = rows.indexOf(row);
|
||||
rows.splice(index, 0, new RowItem());
|
||||
this.setState({ rows });
|
||||
}
|
||||
|
||||
public addRowBelow(row: RowItem) {
|
||||
const rows = this.state.rows;
|
||||
let index = rows.indexOf(row);
|
||||
|
||||
// Be sure we don't add a row between an original row and one of its clones
|
||||
while (rows[index + 1] && isClonedKey(rows[index + 1].state.key!)) {
|
||||
index = index + 1;
|
||||
}
|
||||
|
||||
rows.splice(index + 1, 0, new RowItem());
|
||||
this.setState({ rows });
|
||||
}
|
||||
|
||||
public removeRow(row: RowItem) {
|
||||
const rows = this.state.rows.filter((r) => r !== row);
|
||||
this.setState({ rows: rows.length === 0 ? [new RowItem()] : rows });
|
||||
}
|
||||
|
||||
public moveRowUp(row: RowItem) {
|
||||
const rows = [...this.state.rows];
|
||||
const originalIndex = rows.indexOf(row);
|
||||
|
||||
if (originalIndex === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let moveToIndex = originalIndex - 1;
|
||||
|
||||
// Be sure we don't add a row between an original row and one of its clones
|
||||
while (rows[moveToIndex] && isClonedKey(rows[moveToIndex].state.key!)) {
|
||||
moveToIndex = moveToIndex - 1;
|
||||
}
|
||||
|
||||
rows.splice(originalIndex, 1);
|
||||
rows.splice(moveToIndex, 0, row);
|
||||
this.setState({ rows });
|
||||
}
|
||||
|
||||
public moveRowDown(row: RowItem) {
|
||||
const rows = [...this.state.rows];
|
||||
const originalIndex = rows.indexOf(row);
|
||||
|
||||
if (originalIndex === rows.length - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let moveToIndex = originalIndex + 1;
|
||||
|
||||
// Be sure we don't add a row between an original row and one of its clones
|
||||
while (rows[moveToIndex] && isClonedKey(rows[moveToIndex].state.key!)) {
|
||||
moveToIndex = moveToIndex + 1;
|
||||
}
|
||||
|
||||
rows.splice(moveToIndex + 1, 0, row);
|
||||
rows.splice(originalIndex, 1);
|
||||
|
||||
this.setState({ rows });
|
||||
}
|
||||
|
||||
public isFirstRow(row: RowItem): boolean {
|
||||
return this.state.rows[0] === row;
|
||||
}
|
||||
|
||||
public isLastRow(row: RowItem): boolean {
|
||||
const filteredRow = this.state.rows.filter((r) => !isClonedKey(r.state.key!));
|
||||
return filteredRow[filteredRow.length - 1] === row;
|
||||
}
|
||||
|
||||
public static createEmpty(): RowsLayoutManager {
|
||||
return new RowsLayoutManager({ rows: [new RowItem()] });
|
||||
}
|
||||
|
||||
@@ -1084,8 +1084,7 @@
|
||||
"title": "Row header"
|
||||
},
|
||||
"multi-select": {
|
||||
"options-header": "Multi-selected Row options",
|
||||
"selection-number": "No. of rows selected: {{length}}"
|
||||
"title": "{{length}} rows selected"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1167,7 +1166,7 @@
|
||||
"delete": "Delete",
|
||||
"duplicate": "Duplicate",
|
||||
"layout": "Layout",
|
||||
"panels-title": "{{length}} Panels Selected"
|
||||
"panels-title": "{{length}} panels selected"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -1219,6 +1218,15 @@
|
||||
"row": {
|
||||
"collapse": "Collapse row",
|
||||
"expand": "Expand row",
|
||||
"menu": {
|
||||
"add": "Add row",
|
||||
"add-panel": "Panel",
|
||||
"add-row-above": "Row above",
|
||||
"add-row-below": "Row below",
|
||||
"move-down": "Move row down",
|
||||
"move-row": "Move row",
|
||||
"move-up": "Move row up"
|
||||
},
|
||||
"new": "New row",
|
||||
"repeat": {
|
||||
"learn-more": "Learn more",
|
||||
@@ -1227,18 +1235,14 @@
|
||||
},
|
||||
"row-options": {
|
||||
"height": {
|
||||
"expand": "Expand",
|
||||
"hide-row-header": "Hide row header",
|
||||
"min": "Min",
|
||||
"title": "Height"
|
||||
"hide-row-header": "Hide row header"
|
||||
},
|
||||
"repeat": {
|
||||
"title": "Repeat options",
|
||||
"variable": {
|
||||
"title": "Variable"
|
||||
"title": "Repeat for"
|
||||
}
|
||||
},
|
||||
"title": "Row options",
|
||||
"title": "Row",
|
||||
"title-option": "Title"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1084,8 +1084,7 @@
|
||||
"title": "Ŗőŵ ĥęäđęř"
|
||||
},
|
||||
"multi-select": {
|
||||
"options-header": "Mūľŧį-şęľęčŧęđ Ŗőŵ őpŧįőʼnş",
|
||||
"selection-number": "Ńő. őƒ řőŵş şęľęčŧęđ: {{length}}"
|
||||
"title": "{{length}} řőŵş şęľęčŧęđ"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1167,7 +1166,7 @@
|
||||
"delete": "Đęľęŧę",
|
||||
"duplicate": "Đūpľįčäŧę",
|
||||
"layout": "Ŀäyőūŧ",
|
||||
"panels-title": "{{length}} Päʼnęľş Ŝęľęčŧęđ"
|
||||
"panels-title": "{{length}} päʼnęľş şęľęčŧęđ"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -1219,6 +1218,15 @@
|
||||
"row": {
|
||||
"collapse": "Cőľľäpşę řőŵ",
|
||||
"expand": "Ēχpäʼnđ řőŵ",
|
||||
"menu": {
|
||||
"add": "Åđđ řőŵ",
|
||||
"add-panel": "Päʼnęľ",
|
||||
"add-row-above": "Ŗőŵ äþővę",
|
||||
"add-row-below": "Ŗőŵ þęľőŵ",
|
||||
"move-down": "Mővę řőŵ đőŵʼn",
|
||||
"move-row": "Mővę řőŵ",
|
||||
"move-up": "Mővę řőŵ ūp"
|
||||
},
|
||||
"new": "Ńęŵ řőŵ",
|
||||
"repeat": {
|
||||
"learn-more": "Ŀęäřʼn mőřę",
|
||||
@@ -1227,18 +1235,14 @@
|
||||
},
|
||||
"row-options": {
|
||||
"height": {
|
||||
"expand": "Ēχpäʼnđ",
|
||||
"hide-row-header": "Ħįđę řőŵ ĥęäđęř",
|
||||
"min": "Mįʼn",
|
||||
"title": "Ħęįģĥŧ"
|
||||
"hide-row-header": "Ħįđę řőŵ ĥęäđęř"
|
||||
},
|
||||
"repeat": {
|
||||
"title": "Ŗępęäŧ őpŧįőʼnş",
|
||||
"variable": {
|
||||
"title": "Väřįäþľę"
|
||||
"title": "Ŗępęäŧ ƒőř"
|
||||
}
|
||||
},
|
||||
"title": "Ŗőŵ őpŧįőʼnş",
|
||||
"title": "Ŗőŵ",
|
||||
"title-option": "Ŧįŧľę"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user