mirror of https://github.com/grafana/grafana.git
				
				
				
			Dashboard: New UX for switching layouts (#102268)
* Layout switching * Update * Update * Update * Update * Update * unline styles * fixing lint issue
This commit is contained in:
		
							parent
							
								
									2b3a36b572
								
							
						
					
					
						commit
						74705bd5b3
					
				|  | @ -61,6 +61,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ | |||
|     height: theme.spacing(2), | ||||
|     border: `1px solid ${theme.colors.border.medium}`, | ||||
|     borderRadius: theme.shape.radius.circle, | ||||
|     cursor: 'pointer', | ||||
|     margin: '3px 0' /* Space for box-shadow when focused */, | ||||
| 
 | ||||
|     ':checked': { | ||||
|  | @ -100,6 +101,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ | |||
|     display: 'grid', | ||||
|     gridTemplateColumns: `${theme.spacing(2)} auto`, | ||||
|     gap: theme.spacing(1), | ||||
|     cursor: 'pointer', | ||||
|   }), | ||||
|   description: css({ | ||||
|     fontSize: theme.typography.size.sm, | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components | |||
| import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; | ||||
| 
 | ||||
| import { DashboardScene } from '../scene/DashboardScene'; | ||||
| import { DashboardLayoutSelector } from '../scene/layouts-shared/DashboardLayoutSelector'; | ||||
| import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector'; | ||||
| import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; | ||||
| 
 | ||||
| export class DashboardEditableElement implements EditableDashboardElement { | ||||
|  | @ -41,24 +41,14 @@ export class DashboardEditableElement implements EditableDashboardElement { | |||
|             title: t('dashboard.options.description', 'Description'), | ||||
|             render: () => <DashboardDescriptionInput dashboard={dashboard} />, | ||||
|           }) | ||||
|         ) | ||||
|         .addItem( | ||||
|           new OptionsPaneItemDescriptor({ | ||||
|             title: t('dashboard.layout.common.layout', 'Layout'), | ||||
|             render: () => <DashboardLayoutSelector layoutManager={body} />, | ||||
|           }) | ||||
|         ); | ||||
| 
 | ||||
|       if (body.getOptions) { | ||||
|         for (const option of body.getOptions()) { | ||||
|           editPaneHeaderOptions.addItem(option); | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return editPaneHeaderOptions; | ||||
|     }, [body, dashboard]); | ||||
|     }, [dashboard]); | ||||
| 
 | ||||
|     return [dashboardOptions]; | ||||
|     const layoutCategory = useLayoutCategory(body); | ||||
| 
 | ||||
|     return [dashboardOptions, layoutCategory]; | ||||
|   } | ||||
| 
 | ||||
|   public renderActions(): ReactNode { | ||||
|  |  | |||
|  | @ -51,11 +51,12 @@ export class DefaultGridLayoutManager | |||
|       return t('dashboard.default-layout.name', 'Custom'); | ||||
|     }, | ||||
|     get description() { | ||||
|       return t('dashboard.default-layout.description', 'Manually size and position panels'); | ||||
|       return t('dashboard.default-layout.description', 'Position and size each panel individually'); | ||||
|     }, | ||||
|     id: 'default-grid', | ||||
|     createFromLayout: DefaultGridLayoutManager.createFromLayout, | ||||
|     kind: 'GridLayout', | ||||
|     isGridLayout: true, | ||||
|   }; | ||||
| 
 | ||||
|   public readonly descriptor = DefaultGridLayoutManager.descriptor; | ||||
|  |  | |||
|  | @ -26,15 +26,15 @@ export class ResponsiveGridLayoutManager | |||
| 
 | ||||
|   public static readonly descriptor: LayoutRegistryItem = { | ||||
|     get name() { | ||||
|       return t('dashboard.responsive-layout.name', 'Auto'); | ||||
|       return t('dashboard.responsive-layout.name', 'Auto grid'); | ||||
|     }, | ||||
|     get description() { | ||||
|       return t('dashboard.responsive-layout.description', 'Automatically positions panels into a grid.'); | ||||
|       return t('dashboard.responsive-layout.description', 'Panels resize to fit and form uniform grids'); | ||||
|     }, | ||||
|     id: 'responsive-grid', | ||||
|     createFromLayout: ResponsiveGridLayoutManager.createFromLayout, | ||||
| 
 | ||||
|     kind: 'ResponsiveGridLayout', | ||||
|     isGridLayout: true, | ||||
|   }; | ||||
| 
 | ||||
|   public readonly descriptor = ResponsiveGridLayoutManager.descriptor; | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou | |||
| 
 | ||||
| import { useConditionalRenderingEditor } from '../../conditional-rendering/ConditionalRenderingEditor'; | ||||
| import { getQueryRunnerFor, useDashboard } from '../../utils/utils'; | ||||
| import { DashboardLayoutSelector } from '../layouts-shared/DashboardLayoutSelector'; | ||||
| import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector'; | ||||
| import { useEditPaneInputAutoFocus } from '../layouts-shared/utils'; | ||||
| 
 | ||||
| import { RowItem } from './RowItem'; | ||||
|  | @ -32,20 +32,8 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] | |||
|           title: t('dashboard.rows-layout.option.height', 'Height'), | ||||
|           render: () => <RowHeightSelect row={model} />, | ||||
|         }) | ||||
|       ) | ||||
|       .addItem( | ||||
|         new OptionsPaneItemDescriptor({ | ||||
|           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({ | ||||
|  | @ -61,13 +49,15 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] | |||
|       ); | ||||
| 
 | ||||
|     return editPaneHeaderOptions; | ||||
|   }, [layout, model]); | ||||
|   }, [model]); | ||||
| 
 | ||||
|   const layoutCategory = useLayoutCategory(layout); | ||||
| 
 | ||||
|   const conditionalRenderingOptions = useMemo(() => { | ||||
|     return useConditionalRenderingEditor(model.state.conditionalRendering); | ||||
|   }, [model]); | ||||
| 
 | ||||
|   const editOptions = [rowOptions]; | ||||
|   const editOptions = [rowOptions, layoutCategory]; | ||||
| 
 | ||||
|   if (conditionalRenderingOptions) { | ||||
|     editOptions.push(conditionalRenderingOptions); | ||||
|  |  | |||
|  | @ -29,12 +29,12 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i | |||
|       return t('dashboard.rows-layout.name', 'Rows'); | ||||
|     }, | ||||
|     get description() { | ||||
|       return t('dashboard.rows-layout.description', 'Rows layout'); | ||||
|       return t('dashboard.rows-layout.description', 'Collapsable panel groups with headings'); | ||||
|     }, | ||||
|     id: 'rows-layout', | ||||
|     createFromLayout: RowsLayoutManager.createFromLayout, | ||||
| 
 | ||||
|     kind: 'RowsLayout', | ||||
|     isGridLayout: false, | ||||
|   }; | ||||
| 
 | ||||
|   public readonly descriptor = RowsLayoutManager.descriptor; | ||||
|  |  | |||
|  | @ -30,11 +30,12 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i | |||
|       return t('dashboard.tabs-layout.name', 'Tabs'); | ||||
|     }, | ||||
|     get description() { | ||||
|       return t('dashboard.tabs-layout.description', 'Tabs layout'); | ||||
|       return t('dashboard.tabs-layout.description', 'Organize panels into horizontal tabs'); | ||||
|     }, | ||||
|     id: 'tabs-layout', | ||||
|     createFromLayout: TabsLayoutManager.createFromLayout, | ||||
|     kind: 'TabsLayout', | ||||
|     isGridLayout: false, | ||||
|   }; | ||||
| 
 | ||||
|   public readonly descriptor = TabsLayoutManager.descriptor; | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| import { css, cx } from '@emotion/css'; | ||||
| import { useMemo } from 'react'; | ||||
| 
 | ||||
| import { Select } from '@grafana/ui'; | ||||
| import { GrafanaTheme2 } from '@grafana/data'; | ||||
| import { RadioButtonDot, Stack, useStyles2, Text } 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'; | ||||
|  | @ -10,53 +12,171 @@ import { isLayoutParent } from '../types/LayoutParent'; | |||
| import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; | ||||
| 
 | ||||
| import { layoutRegistry } from './layoutRegistry'; | ||||
| import { findParentLayout } from './utils'; | ||||
| 
 | ||||
| export interface Props { | ||||
|   layoutManager: DashboardLayoutManager; | ||||
| } | ||||
| 
 | ||||
| export function DashboardLayoutSelector({ layoutManager }: Props) { | ||||
|   const options = useMemo(() => { | ||||
|     const parentLayout = findParentLayout(layoutManager); | ||||
|     const parentLayoutId = parentLayout?.descriptor.id; | ||||
|   const isGridLayout = layoutManager.descriptor.isGridLayout; | ||||
|   const options = layoutRegistry.list().filter((layout) => layout.isGridLayout === isGridLayout); | ||||
| 
 | ||||
|     return layoutRegistry | ||||
|       .list() | ||||
|       .filter((layout) => layout.id !== parentLayoutId) | ||||
|       .map((layout) => ({ | ||||
|         label: layout.name, | ||||
|         value: layout, | ||||
|       })); | ||||
|   }, [layoutManager]); | ||||
| 
 | ||||
|   const currentLayoutId = layoutManager.descriptor.id; | ||||
|   const currentOption = options.find((option) => option.value.id === currentLayoutId); | ||||
|   const styles = useStyles2(getStyles); | ||||
| 
 | ||||
|   return ( | ||||
|     <Select | ||||
|       options={options} | ||||
|       value={currentOption} | ||||
|       onChange={(option) => { | ||||
|         if (option.value?.id !== currentOption?.value.id) { | ||||
|           changeLayoutTo(layoutManager, option.value!); | ||||
|     <div role="radiogroup" className={styles.radioGroup}> | ||||
|       {options.map((opt) => { | ||||
|         switch (opt.id) { | ||||
|           case 'rows-layout': | ||||
|             return ( | ||||
|               <LayoutRadioButton | ||||
|                 label={opt.name} | ||||
|                 id={opt.id} | ||||
|                 description={opt.description!} | ||||
|                 isSelected={layoutManager.descriptor.id === opt.id} | ||||
|                 onSelect={() => changeLayoutTo(layoutManager, opt)} | ||||
|               > | ||||
|                 <div className={styles.rowsLayoutViz}> | ||||
|                   {/* eslint-disable-next-line @grafana/no-untranslated-strings */} | ||||
|                   <div style={{ gridColumn: 'span 3', fontSize: '6px' }}>⌄   .-.-.-.-.-</div> | ||||
|                   <GridCell /> | ||||
|                   <GridCell /> | ||||
|                   <GridCell /> | ||||
|                   {/* eslint-disable-next-line @grafana/no-untranslated-strings */} | ||||
|                   <div style={{ gridColumn: 'span 3', fontSize: '6px' }}>⌄   .-.-.-.-.-</div> | ||||
|                   <GridCell /> | ||||
|                   <GridCell /> | ||||
|                   <GridCell /> | ||||
|                 </div> | ||||
|               </LayoutRadioButton> | ||||
|             ); | ||||
|           case 'tabs-layout': | ||||
|             return ( | ||||
|               <LayoutRadioButton | ||||
|                 label={opt.name} | ||||
|                 id={opt.id} | ||||
|                 description={opt.description!} | ||||
|                 isSelected={layoutManager.descriptor.id === opt.id} | ||||
|                 onSelect={() => changeLayoutTo(layoutManager, opt)} | ||||
|               > | ||||
|                 <Stack direction="column" gap={0.5} height={'100%'}> | ||||
|                   <div className={styles.tabsBar}> | ||||
|                     {/* eslint-disable-next-line @grafana/no-untranslated-strings */} | ||||
|                     <div className={cx(styles.tab, styles.tabActive)}>-.-.-</div> | ||||
|                     {/* eslint-disable-next-line @grafana/no-untranslated-strings */} | ||||
|                     <div className={styles.tab}>-.-.-</div> | ||||
|                     {/* eslint-disable-next-line @grafana/no-untranslated-strings */} | ||||
|                     <div className={styles.tab}>-.-.-</div> | ||||
|                   </div> | ||||
|                   <div className={styles.tabsVizTabContent}> | ||||
|                     <GridCell /> | ||||
|                     <GridCell /> | ||||
|                   </div> | ||||
|                 </Stack> | ||||
|               </LayoutRadioButton> | ||||
|             ); | ||||
|           case 'responsive-grid': | ||||
|             return ( | ||||
|               <LayoutRadioButton | ||||
|                 label={opt.name} | ||||
|                 id={opt.id} | ||||
|                 description={opt.description!} | ||||
|                 isSelected={layoutManager.descriptor.id === opt.id} | ||||
|                 onSelect={() => changeLayoutTo(layoutManager, opt)} | ||||
|               > | ||||
|                 <div className={styles.autoGridViz}> | ||||
|                   <GridCell /> | ||||
|                   <GridCell /> | ||||
|                   <GridCell /> | ||||
|                   <GridCell /> | ||||
|                 </div> | ||||
|               </LayoutRadioButton> | ||||
|             ); | ||||
|           case 'custom-grid': | ||||
|           default: | ||||
|             return ( | ||||
|               <LayoutRadioButton | ||||
|                 label={opt.name} | ||||
|                 id={opt.id} | ||||
|                 description={opt.description!} | ||||
|                 isSelected={layoutManager.descriptor.id === opt.id} | ||||
|                 onSelect={() => changeLayoutTo(layoutManager, opt)} | ||||
|               > | ||||
|                 <div className={styles.customGridViz}> | ||||
|                   <GridCell colSpan={2} /> | ||||
|                   <div className={styles.customGridVizInner}> | ||||
|                     <GridCell /> | ||||
|                     <GridCell /> | ||||
|                   </div> | ||||
|                   <GridCell /> | ||||
|                   <GridCell colSpan={2} /> | ||||
|                 </div> | ||||
|               </LayoutRadioButton> | ||||
|             ); | ||||
|         } | ||||
|       }} | ||||
|     /> | ||||
|       })} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| interface LayoutRadioButtonProps { | ||||
|   label: string; | ||||
|   id: string; | ||||
|   description: string; | ||||
|   isSelected: boolean; | ||||
|   onSelect: () => void; | ||||
|   children: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| function LayoutRadioButton({ label, id, description, isSelected, children, onSelect }: LayoutRadioButtonProps) { | ||||
|   const styles = useStyles2(getStyles); | ||||
| 
 | ||||
|   return ( | ||||
|     // This outer div is just so that the radio dot can be outside the
 | ||||
|     // label (as the RadioButtonDot has a label element and they can't nest)
 | ||||
|     <div className={styles.radioButtonOuter}> | ||||
|       <label | ||||
|         htmlFor={`layout-${id}`} | ||||
|         tabIndex={0} | ||||
|         className={cx(styles.radioButton, isSelected && styles.radioButtonActive)} | ||||
|       > | ||||
|         {children} | ||||
|         <Stack direction="column" gap={1} justifyContent="space-between" grow={1}> | ||||
|           <Text weight="medium">{label}</Text> | ||||
|           <Text variant="bodySmall" color="secondary"> | ||||
|             {description} | ||||
|           </Text> | ||||
|         </Stack> | ||||
|       </label> | ||||
|       <div className={styles.radioDot}> | ||||
|         <RadioButtonDot id={`layout-${id}`} name={'layout'} label={<></>} onChange={onSelect} checked={isSelected} /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function GridCell({ colSpan = 1 }: { colSpan?: number }) { | ||||
|   const styles = useStyles2(getStyles); | ||||
| 
 | ||||
|   return <div className={styles.gridCell} style={{ gridColumn: `span ${colSpan}` }}></div>; | ||||
| } | ||||
| 
 | ||||
| export function useLayoutCategory(layoutManager: DashboardLayoutManager) { | ||||
|   return useMemo(() => { | ||||
|     const categoryName = layoutManager.descriptor.isGridLayout | ||||
|       ? t('dashboard.layout.common.grid', 'Grid') | ||||
|       : t('dashboard.layout.common.layout', 'Layout'); | ||||
| 
 | ||||
|     const layoutCategory = new OptionsPaneCategoryDescriptor({ | ||||
|       title: 'Layout', | ||||
|       title: categoryName, | ||||
|       id: 'layout-options', | ||||
|       isOpenDefault: true, | ||||
|     }); | ||||
| 
 | ||||
|     layoutCategory.addItem( | ||||
|       new OptionsPaneItemDescriptor({ | ||||
|         title: t('dashboard.layout.common.layout', 'Layout'), | ||||
|         title: '', | ||||
|         skipField: true, | ||||
|         render: () => <DashboardLayoutSelector layoutManager={layoutManager} />, | ||||
|       }) | ||||
|     ); | ||||
|  | @ -77,3 +197,100 @@ function changeLayoutTo(currentLayout: DashboardLayoutManager, newLayoutDescript | |||
|     layoutParent.switchLayout(newLayoutDescriptor.createFromLayout(currentLayout)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const getStyles = (theme: GrafanaTheme2) => { | ||||
|   return { | ||||
|     radioButtonOuter: css({ | ||||
|       position: 'relative', | ||||
|     }), | ||||
|     radioDot: css({ | ||||
|       position: 'absolute', | ||||
|       top: theme.spacing(0.5), | ||||
|       right: theme.spacing(0), | ||||
|     }), | ||||
|     radioGroup: css({ | ||||
|       backgroundColor: theme.colors.background.primary, | ||||
|       display: 'flex', | ||||
|       flexDirection: 'column', | ||||
|       gap: theme.spacing(2), | ||||
|       marginBottom: theme.spacing(2), | ||||
|     }), | ||||
|     radioButton: css({ | ||||
|       alignItems: 'flex-start', | ||||
|       gap: theme.spacing(1.5), | ||||
|       padding: theme.spacing(1), | ||||
|       border: `1px solid ${theme.colors.border.weak}`, | ||||
|       cursor: 'pointer', | ||||
|       borderRadius: theme.shape.radius.default, | ||||
|       display: 'grid', | ||||
|       gridTemplateColumns: `80px 1fr`, | ||||
|       gridTemplateRows: '70px', | ||||
|     }), | ||||
|     radioButtonActive: css({ | ||||
|       border: `1px solid ${theme.colors.primary.border}`, | ||||
|     }), | ||||
|     gridCell: css({ | ||||
|       backgroundColor: theme.colors.background.secondary, | ||||
|       border: `1px solid ${theme.colors.border.medium}`, | ||||
|     }), | ||||
|     tab: css({ | ||||
|       width: theme.spacing(2), | ||||
|       height: theme.spacing(1), | ||||
|       fontSize: '5px', | ||||
|       display: 'flex', | ||||
|       alignItems: 'center', | ||||
|       position: 'relative', | ||||
|       justifyContent: 'center', | ||||
|     }), | ||||
|     tabActive: css({ | ||||
|       '&:before': { | ||||
|         content: '" "', | ||||
|         position: 'absolute', | ||||
|         height: 1, | ||||
|         bottom: 0, | ||||
|         left: 0, | ||||
|         right: 0, | ||||
|         background: theme.colors.gradients.brandHorizontal, | ||||
|       }, | ||||
|     }), | ||||
|     tabsBar: css({ | ||||
|       display: 'flex', | ||||
|       gap: theme.spacing(0.5), | ||||
|       borderBottom: `1px solid ${theme.colors.border.medium}`, | ||||
|     }), | ||||
|     rowsLayoutViz: css({ | ||||
|       display: 'grid', | ||||
|       gridTemplateColumns: 'repeat(3, 1fr)', | ||||
|       gridTemplateRows: '10px 1fr 10px 1fr', | ||||
|       gap: '4px', | ||||
|       height: '100%', | ||||
|     }), | ||||
|     tabsVizTabContent: css({ | ||||
|       display: 'grid', | ||||
|       gridTemplateColumns: '1fr 1fr', | ||||
|       gridTemplateRows: '1fr', | ||||
|       gap: '4px', | ||||
|       flexGrow: 1, | ||||
|     }), | ||||
|     autoGridViz: css({ | ||||
|       display: 'grid', | ||||
|       gridTemplateColumns: 'repeat(2, 1fr)', | ||||
|       gridTemplateRows: 'repeat(2, 1fr)', | ||||
|       gap: '4px', | ||||
|       height: '100%', | ||||
|     }), | ||||
|     customGridViz: css({ | ||||
|       display: 'grid', | ||||
|       gridTemplateColumns: 'repeat(3, 1fr)', | ||||
|       gridTemplateRows: 'repeat(2, 1fr)', | ||||
|       gap: '4px', | ||||
|       height: '100%', | ||||
|     }), | ||||
|     customGridVizInner: css({ | ||||
|       display: 'grid', | ||||
|       gridTemplateColumns: 'repeat(1, 1fr)', | ||||
|       gridTemplateRows: 'repeat(2, 1fr)', | ||||
|       gap: '4px', | ||||
|     }), | ||||
|   }; | ||||
| }; | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import { config } from '@grafana/runtime'; | ||||
| import { SceneGridRow } from '@grafana/scenes'; | ||||
| 
 | ||||
| import { NewObjectAddedToCanvasEvent } from '../../edit-pane/shared'; | ||||
|  | @ -31,13 +32,20 @@ export function addNewTabTo(layout: DashboardLayoutManager): TabItem { | |||
| } | ||||
| 
 | ||||
| export function addNewRowTo(layout: DashboardLayoutManager): RowItem | SceneGridRow { | ||||
|   if (layout instanceof RowsLayoutManager) { | ||||
|     const row = layout.addNewRow(); | ||||
|     layout.publishEvent(new NewObjectAddedToCanvasEvent(row), true); | ||||
|     return row; | ||||
|   /** | ||||
|    * If new layouts feature is disabled we add old school rows to the custom grid layout | ||||
|    */ | ||||
|   if (!config.featureToggles.dashboardNewLayouts) { | ||||
|     if (layout instanceof DefaultGridLayoutManager) { | ||||
|       const row = layout.addNewRow(); | ||||
|       layout.publishEvent(new NewObjectAddedToCanvasEvent(row), true); | ||||
|       return row; | ||||
|     } else { | ||||
|       throw new Error('New dashboard layouts feature not enabled but new layout found'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (layout instanceof DefaultGridLayoutManager) { | ||||
|   if (layout instanceof RowsLayoutManager) { | ||||
|     const row = layout.addNewRow(); | ||||
|     layout.publishEvent(new NewObjectAddedToCanvasEvent(row), true); | ||||
|     return row; | ||||
|  | @ -48,6 +56,9 @@ export function addNewRowTo(layout: DashboardLayoutManager): RowItem | SceneGrid | |||
|     return addNewRowTo(currentTab.state.layout); | ||||
|   } | ||||
| 
 | ||||
|   // If we want to add a row and current layout is custom grid or auto we migrate to rows layout
 | ||||
|   // And wrap current layout in a row
 | ||||
| 
 | ||||
|   const layoutParent = layout.parent!; | ||||
|   if (!isLayoutParent(layoutParent)) { | ||||
|     throw new Error('Parent layout is not a LayoutParent'); | ||||
|  |  | |||
|  | @ -23,4 +23,9 @@ export interface LayoutRegistryItem<S = {}> extends RegistryItem { | |||
|    * Schema kind of layout | ||||
|    */ | ||||
|   kind?: DashboardV2Spec['layout']['kind']; | ||||
| 
 | ||||
|   /** | ||||
|    * Is grid layout (that contains panels) | ||||
|    */ | ||||
|   isGridLayout: boolean; | ||||
| } | ||||
|  |  | |||
|  | @ -1066,7 +1066,7 @@ | |||
|       } | ||||
|     }, | ||||
|     "default-layout": { | ||||
|       "description": "Manually size and position panels", | ||||
|       "description": "Position and size each panel individually", | ||||
|       "item-options": { | ||||
|         "repeat": { | ||||
|           "direction": { | ||||
|  | @ -1215,6 +1215,7 @@ | |||
|         "copy-or-duplicate": "Copy or Duplicate", | ||||
|         "delete": "Delete", | ||||
|         "duplicate": "Duplicate", | ||||
|         "grid": "Grid", | ||||
|         "layout": "Layout" | ||||
|       } | ||||
|     }, | ||||
|  | @ -1238,8 +1239,8 @@ | |||
|       } | ||||
|     }, | ||||
|     "responsive-layout": { | ||||
|       "description": "Automatically positions panels into a grid.", | ||||
|       "name": "Auto", | ||||
|       "description": "Panels resize to fit and form uniform grids", | ||||
|       "name": "Auto grid", | ||||
|       "options": { | ||||
|         "columns": "Columns", | ||||
|         "fixed": "Fixed: {{size}}px", | ||||
|  | @ -1251,7 +1252,7 @@ | |||
|       } | ||||
|     }, | ||||
|     "rows-layout": { | ||||
|       "description": "Rows layout", | ||||
|       "description": "Collapsable panel groups with headings", | ||||
|       "name": "Rows", | ||||
|       "option": { | ||||
|         "height": "Height", | ||||
|  | @ -1286,7 +1287,7 @@ | |||
|       } | ||||
|     }, | ||||
|     "tabs-layout": { | ||||
|       "description": "Tabs layout", | ||||
|       "description": "Organize panels into horizontal tabs", | ||||
|       "menu": { | ||||
|         "move-tab": "Move tab" | ||||
|       }, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue