From e7c30a8a05363948380607d4258907b4ebe458c4 Mon Sep 17 00:00:00 2001 From: Gurinder Singh <gurinder@coderabbit.ai> Date: Fri, 29 Nov 2024 12:51:59 -0500 Subject: [PATCH 1/5] custom dashboard checkpoint --- .../src/services/LocationService.tsx | 1 + public/app/core/components/Page/Page.tsx | 26 +-- public/app/core/reducers/fn-slice.ts | 2 + .../AddPanelButton/AddPanelButton.tsx | 7 +- .../dashboard/containers/DashboardPage.tsx | 95 +++++--- .../dashboard/dashgrid/DashboardGrid.tsx | 202 +++++++++++++----- .../dashboard/dashgrid/DashboardPanel.tsx | 2 +- .../dashboard/dashgrid/PanelStateWrapper.tsx | 2 +- public/app/fn-app/create-mfe.ts | 1 + public/app/fn-app/fn-app-provider.tsx | 39 +++- .../fn-dashboard-page/render-fn-dashboard.tsx | 2 +- public/app/fn-app/types.ts | 1 + 12 files changed, 268 insertions(+), 112 deletions(-) diff --git a/packages/grafana-runtime/src/services/LocationService.tsx b/packages/grafana-runtime/src/services/LocationService.tsx index 5b0bcd9e0ccf8..c51b6894705e7 100644 --- a/packages/grafana-runtime/src/services/LocationService.tsx +++ b/packages/grafana-runtime/src/services/LocationService.tsx @@ -21,6 +21,7 @@ export interface LocationService { getHistory: () => H.History; getSearch: () => URLSearchParams; getSearchObject: () => UrlQueryMap; + fnPathnameChange: (path: string, queryParams: any) => void; /** * This is from the old LocationSrv interface diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index 0f82fd4ba8ced..cd29bbfb55619 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -2,7 +2,6 @@ import { css, cx } from '@emotion/css'; import { useLayoutEffect } from 'react'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; @@ -94,24 +93,13 @@ Page.Contents = PageContents; const getStyles = (theme: GrafanaTheme2) => { return { - wrapper: css( - config.featureToggles.bodyScrolling - ? { - label: 'page-wrapper', - display: 'flex', - flex: '1 1 0', - flexDirection: 'column', - position: 'relative', - } - : { - label: 'page-wrapper', - height: '100%', - display: 'flex', - flex: '1 1 0', - flexDirection: 'column', - minHeight: 0, - } - ), + wrapper: css({ + label: 'page-wrapper', + display: 'flex', + flex: '1 1 0', + flexDirection: 'column', + position: 'relative', + }), pageContent: css({ label: 'page-content', flexGrow: 1, diff --git a/public/app/core/reducers/fn-slice.ts b/public/app/core/reducers/fn-slice.ts index d71ce9aef766e..732ca579c5a6a 100644 --- a/public/app/core/reducers/fn-slice.ts +++ b/public/app/core/reducers/fn-slice.ts @@ -6,6 +6,7 @@ import { AnyObject } from '../../fn-app/types'; export interface FnGlobalState { FNDashboard: boolean; + isCustomDashboard: boolean; uid: string; slug: string; version: number; @@ -48,6 +49,7 @@ export const FN_STATE_KEY = 'fnGlobalState'; export const INITIAL_FN_STATE: FnGlobalState = { // NOTE: initial value is false FNDashboard: false, + isCustomDashboard: false, uid: '', slug: '', version: 1, diff --git a/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx b/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx index e76b3769ad076..8d7643ef6f4b4 100644 --- a/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx +++ b/public/app/features/dashboard/components/AddPanelButton/AddPanelButton.tsx @@ -10,9 +10,10 @@ import AddPanelMenu from './AddPanelMenu'; export interface Props { dashboard: DashboardModel; onToolbarAddMenuOpen?: () => void; + isFNDashboard?: boolean; } -const AddPanelButton = ({ dashboard, onToolbarAddMenuOpen }: Props) => { +const AddPanelButton = ({ dashboard, onToolbarAddMenuOpen, isFNDashboard }: Props) => { const [isMenuOpen, setIsMenuOpen] = useState(false); useEffect(() => { @@ -29,8 +30,8 @@ const AddPanelButton = ({ dashboard, onToolbarAddMenuOpen }: Props) => { onVisibleChange={setIsMenuOpen} > <Button - variant="secondary" - size="sm" + variant="primary" + size={isFNDashboard ? 'md' : 'sm'} fill="outline" data-testid={selectors.components.PageToolbar.itemButton('Add button')} > diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 5ebe4cbcd006c..7c869e13be3eb 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -1,22 +1,25 @@ import { cx } from '@emotion/css'; import { Portal } from '@mui/material'; -import React, { PureComponent } from 'react'; +import { PureComponent } from 'react'; import { connect, ConnectedProps, MapDispatchToProps, MapStateToProps } from 'react-redux'; import { NavModel, NavModelItem, TimeRange, PageLayoutType, locationUtil } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, locationService } from '@grafana/runtime'; -import { Themeable2, withTheme2, ToolbarButtonRow } from '@grafana/ui'; +import { Themeable2, withTheme2, ToolbarButtonRow, ToolbarButton, ModalsController } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; +import { ScrollRefElement } from 'app/core/components/NativeScrollbar'; import { Page } from 'app/core/components/Page/Page'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { GrafanaContext, GrafanaContextType } from 'app/core/context/GrafanaContext'; import { createErrorNotification } from 'app/core/copy/appNotification'; +import { t } from 'app/core/internationalization'; import { getKioskMode } from 'app/core/navigation/kiosk'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { FnGlobalState } from 'app/core/reducers/fn-slice'; import { getNavModel } from 'app/core/selectors/navModel'; import { PanelModel } from 'app/features/dashboard/state'; +import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { updateTimeZoneForSession } from 'app/features/profile/state/reducers'; import { getPageNavFromSlug, getRootContentNavModel } from 'app/features/storage/StorageFolderPage'; @@ -26,6 +29,7 @@ import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events'; import { cancelVariables, templateVarsChangedInUrl } from '../../variables/state/actions'; import { findTemplateVarChanges } from '../../variables/utils'; +import AddPanelButton from '../components/AddPanelButton/AddPanelButton'; import { AddWidgetModal } from '../components/AddWidgetModal/AddWidgetModal'; import { DashNav } from '../components/DashNav'; import { DashNavTimeControls } from '../components/DashNav/DashNavTimeControls'; @@ -36,6 +40,7 @@ import { DashboardPrompt } from '../components/DashboardPrompt/DashboardPrompt'; import { DashboardSettings } from '../components/DashboardSettings'; import { PanelInspector } from '../components/Inspector/PanelInspector'; import { PanelEditor } from '../components/PanelEditor/PanelEditor'; +import { SaveDashboardDrawer } from '../components/SaveDashboard/SaveDashboardDrawer'; import { SubMenu } from '../components/SubMenu/SubMenu'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { liveTimer } from '../dashgrid/liveTimer'; @@ -72,7 +77,7 @@ export type MapStateToDashboardPageProps = MapStateToProps< Pick<DashboardState, 'initPhase' | 'initError'> & { dashboard: ReturnType<DashboardState['getModel']>; navIndex: StoreState['navIndex']; - } & Pick<FnGlobalState, 'FNDashboard' | 'controlsContainer'>, + } & Pick<FnGlobalState, 'FNDashboard' | 'controlsContainer' | 'isCustomDashboard'>, OwnProps, StoreState >; @@ -93,6 +98,7 @@ export const mapStateToProps: MapStateToDashboardPageProps = (state) => ({ dashboard: state.dashboard.getModel(), navIndex: state.navIndex, FNDashboard: state.fnGlobalState.FNDashboard, + isCustomDashboard: state.fnGlobalState.isCustomDashboard, controlsContainer: state.fnGlobalState.controlsContainer, }); @@ -128,7 +134,7 @@ export interface State { showLoadingState: boolean; panelNotFound: boolean; editPanelAccessDenied: boolean; - scrollElement?: HTMLDivElement; + scrollElement?: ScrollRefElement; pageNav?: NavModelItem; sectionNav?: NavModel; } @@ -152,9 +158,9 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { componentDidMount() { this.initDashboard(); - const { FNDashboard } = this.props; + const { FNDashboard, isCustomDashboard } = this.props; - if (!FNDashboard) { + if (!FNDashboard || isCustomDashboard) { this.forceRouteReloadCounter = (this.props.history.location?.state as any)?.routeReloadCounter || 0; } } @@ -192,13 +198,13 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { } componentDidUpdate(prevProps: Props, prevState: State) { - const { dashboard, match, templateVarsChangedInUrl, FNDashboard } = this.props; + const { dashboard, match, templateVarsChangedInUrl, FNDashboard, isCustomDashboard } = this.props; if (!dashboard) { return; } - if (!FNDashboard) { + if (!FNDashboard || isCustomDashboard) { const routeReloadCounter = (this.props.history.location?.state as any)?.routeReloadCounter; if ( @@ -354,7 +360,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { this.setState({ updateScrollTop: 0 }); }; - setScrollRef = (scrollElement: HTMLDivElement): void => { + setScrollRef = (scrollElement: ScrollRefElement): void => { this.setState({ scrollElement }); }; @@ -378,7 +384,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { } render() { - const { dashboard, initError, queryParams, FNDashboard, controlsContainer } = this.props; + const { dashboard, initError, queryParams, FNDashboard, controlsContainer, isCustomDashboard } = this.props; const { editPanel, viewPanel, pageNav, sectionNav } = this.state; const kioskMode = getKioskMode(this.props.queryParams); @@ -391,12 +397,22 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { const showToolbar = FNDashboard || (kioskMode !== KioskMode.Full && !queryParams.editview); + const isFNDashboardEditable = (isCustomDashboard && FNDashboard) || !FNDashboard; + + console.log('Edit Panel: ', { editPanel, sectionNav, pageNav, isFNDashboardEditable }); + console.log('Dashboard settings: ', { editView: queryParams.editview, pageNav, sectionNav, isFNDashboardEditable }); + console.log('Add Widget: ', { + isFNDashboardEditable, + addWidget: queryParams.addWidget, + configToggle: config.featureToggles.vizAndWidgetSplit, + }); + const pageClassName = cx({ 'panel-in-fullscreen': Boolean(viewPanel), 'page-hidden': Boolean(queryParams.editview || editPanel), }); - if (dashboard.meta.dashboardNotFound) { + if (dashboard.meta.dashboardNotFound && !FNDashboard) { return ( <Page navId="dashboards/browse" layout={PageLayoutType.Canvas} pageNav={{ text: 'Not found' }}> <EntityNotFound entity="Dashboard" /> @@ -417,27 +433,56 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { ); return ( - <React.Fragment> + <> <Page navModel={sectionNav} pageNav={pageNav} layout={PageLayoutType.Canvas} className={pageClassName} - // scrollRef={this.setScrollRef} - // scrollTop={updateScrollTop} - style={{ minHeight: '550px' }} + onSetScrollRef={this.setScrollRef} > {showToolbar && ( <header data-testid={selectors.pages.Dashboard.DashNav.navV2}> {FNDashboard ? ( - FNTimeRange + <div + style={{ + display: 'flex', + justifyContent: 'flex-end', + gap: 4, + }} + > + {isCustomDashboard && ( + <> + <ModalsController key="button-save"> + {({ showModal, hideModal }) => ( + <ToolbarButton + tooltip={t('dashboard.toolbar.save', 'Save dashboard')} + icon="save" + onClick={() => { + showModal(SaveDashboardDrawer, { + dashboard, + onDismiss: hideModal, + }); + }} + /> + )} + </ModalsController> + <AddPanelButton + onToolbarAddMenuOpen={DashboardInteractions.toolbarAddClick} + dashboard={dashboard} + key="panel-add-dropdown" + isFNDashboard + /> + </> + )} + {FNTimeRange} + </div> ) : ( <DashNav dashboard={dashboard} title={dashboard.title} folderTitle={dashboard.meta.folderTitle} isFullscreen={!!viewPanel} - // onAddPanel={this.onAddPanel} kioskMode={kioskMode} hideTimePicker={dashboard.timepicker.hidden} /> @@ -453,14 +498,14 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { )} <DashboardGrid dashboard={dashboard} - isEditable={!!dashboard.meta.canEdit && !FNDashboard} + isEditable={isFNDashboardEditable && !!dashboard.meta.canEdit} viewPanel={viewPanel} editPanel={editPanel} /> {inspectPanel && !FNDashboard && <PanelInspector dashboard={dashboard} panel={inspectPanel} />} </Page> - {editPanel && !FNDashboard && sectionNav && pageNav && ( + {editPanel && sectionNav && pageNav && isFNDashboardEditable && ( <PanelEditor dashboard={dashboard} sourcePanel={editPanel} @@ -469,7 +514,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { pageNav={pageNav} /> )} - {queryParams.editview && !FNDashboard && pageNav && sectionNav && ( + {queryParams.editview && pageNav && sectionNav && isFNDashboardEditable && ( <DashboardSettings dashboard={dashboard} editview={queryParams.editview} @@ -477,16 +522,18 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { sectionNav={sectionNav} /> )} - {!FNDashboard && queryParams.addWidget && config.featureToggles.vizAndWidgetSplit && <AddWidgetModal />} - </React.Fragment> + {isFNDashboardEditable && queryParams.addWidget && config.featureToggles.vizAndWidgetSplit && ( + <AddWidgetModal /> + )} + </> ); } } function updateStatePageNavFromProps(props: Props, state: State): State { - const { dashboard, FNDashboard } = props; + const { dashboard, FNDashboard, isCustomDashboard } = props; - if (!dashboard || FNDashboard) { + if (!dashboard || (FNDashboard && !isCustomDashboard)) { return state; } diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 70c88a5d54182..5bf46d2e6c97f 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -1,13 +1,15 @@ import classNames from 'classnames'; -import React, { PureComponent, CSSProperties } from 'react'; +import { PureComponent, CSSProperties } from 'react'; +import * as React from 'react'; import ReactGridLayout, { ItemCallback } from 'react-grid-layout'; import { connect } from 'react-redux'; -import AutoSizer from 'react-virtualized-auto-sizer'; import { Subscription } from 'rxjs'; import { config } from '@grafana/runtime'; +import appEvents from 'app/core/app_events'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; import { contextSrv } from 'app/core/services/context_srv'; +import { VariablesChanged } from 'app/features/variables/types'; import { StoreState } from 'app/types'; import { DashboardPanelsChangedEvent } from 'app/types/events'; @@ -19,6 +21,8 @@ import { GridPos } from '../state/PanelModel'; import DashboardEmpty from './DashboardEmpty'; import { DashboardPanel } from './DashboardPanel'; +export const PANEL_FILTER_VARIABLE = 'systemPanelFilterVar'; + export interface Props { dashboard: DashboardModel; isEditable: boolean; @@ -26,9 +30,15 @@ export interface Props { viewPanel: PanelModel | null; hidePanelMenus?: boolean; isFnDashboard?: boolean; + isCustomDashboard?: boolean; +} + +interface State { + panelFilter?: RegExp; + width: number; } -export class Component extends PureComponent<Props> { +export class DashboardGridUnconnected extends PureComponent<Props, State> { private panelMap: { [key: string]: PanelModel } = {}; private eventSubs = new Subscription(); private windowHeight = 1200; @@ -40,10 +50,41 @@ export class Component extends PureComponent<Props> { constructor(props: Props) { super(props); + this.state = { + panelFilter: undefined, + width: document.body.clientWidth, // initial very rough estimate + }; } componentDidMount() { const { dashboard } = this.props; + + if (config.featureToggles.panelFilterVariable) { + // If panel filter variable is set on load then + // update state to filter panels + for (const variable of dashboard.getVariables()) { + if (variable.id === PANEL_FILTER_VARIABLE) { + if ('query' in variable) { + this.setPanelFilter(variable.query); + } + break; + } + } + + this.eventSubs.add( + appEvents.subscribe(VariablesChanged, (e) => { + if (e.payload.variable?.id === PANEL_FILTER_VARIABLE) { + if ('current' in e.payload.variable) { + let variable = e.payload.variable.current; + if ('value' in variable && typeof variable.value === 'string') { + this.setPanelFilter(variable.value); + } + } + } + }) + ); + } + this.eventSubs.add(dashboard.events.subscribe(DashboardPanelsChangedEvent, this.triggerForceUpdate)); } @@ -51,10 +92,25 @@ export class Component extends PureComponent<Props> { this.eventSubs.unsubscribe(); } + setPanelFilter(regex: string) { + // Only set the panels filter if the systemPanelFilterVar variable + // is a non-empty string + let panelFilter = undefined; + if (regex.length > 0) { + panelFilter = new RegExp(regex, 'i'); + } + + this.setState({ + panelFilter: panelFilter, + }); + } + buildLayout() { const layout: ReactGridLayout.Layout[] = []; this.panelMap = {}; + const { panelFilter } = this.state; + let count = 0; for (const panel of this.props.dashboard.panels) { if (!panel.key) { panel.key = `panel-${panel.id}-${Date.now()}`; @@ -81,13 +137,27 @@ export class Component extends PureComponent<Props> { panelPos.isDraggable = panel.collapsed; } - layout.push(panelPos); + if (!panelFilter) { + layout.push(panelPos); + } else { + if (panelFilter.test(panel.title)) { + panelPos.isResizable = false; + panelPos.isDraggable = false; + panelPos.x = (count % 2) * GRID_COLUMN_COUNT; + panelPos.y = Math.floor(count / 2); + layout.push(panelPos); + count++; + } + } } return layout; } onLayoutChange = (newLayout: ReactGridLayout.Layout[]) => { + if (this.state.panelFilter) { + return; + } for (const newPos of newLayout) { this.panelMap[newPos.i!].updateGridPos(newPos, this.isLayoutInitialized); } @@ -139,6 +209,7 @@ export class Component extends PureComponent<Props> { } renderPanels(gridWidth: number, isDashboardDraggable: boolean) { + const { panelFilter } = this.state; const panelElements = []; // Reset last panel bottom @@ -155,7 +226,7 @@ export class Component extends PureComponent<Props> { for (const panel of this.props.dashboard.panels) { const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.isViewing }); - panelElements.push( + const p = ( <GrafanaGridItem key={panel.key} className={panelClasses} @@ -171,6 +242,14 @@ export class Component extends PureComponent<Props> { }} </GrafanaGridItem> ); + + if (!panelFilter) { + panelElements.push(p); + } else { + if (panelFilter.test(panel.title)) { + panelElements.push(p); + } + } } return panelElements; @@ -213,61 +292,69 @@ export class Component extends PureComponent<Props> { } }; + private resizeObserver?: ResizeObserver; + private rootEl: HTMLDivElement | null = null; + onMeasureRef = (rootEl: HTMLDivElement | null) => { + if (!rootEl) { + if (this.rootEl && this.resizeObserver) { + this.resizeObserver.unobserve(this.rootEl); + } + return; + } + + this.rootEl = rootEl; + this.resizeObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => { + this.setState({ width: entry.contentRect.width }); + }); + }); + + this.resizeObserver.observe(rootEl); + }; + render() { - const { isEditable, dashboard, isFnDashboard } = this.props; + const { isEditable, dashboard, isCustomDashboard, isFnDashboard } = this.props; + const { width } = this.state; - if (config.featureToggles.emptyDashboardPage && dashboard.panels.length === 0) { + if (dashboard.panels.length === 0) { return <DashboardEmpty dashboard={dashboard} canCreate={isEditable} />; } - /** - * We have a parent with "flex: 1 1 0" we need to reset it to "flex: 1 1 auto" to have the AutoSizer - * properly working. For more information go here: - * https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#can-i-use-autosizer-within-a-flex-container - */ - return ( - <div style={{ flex: '1 1 auto', display: this.props.editPanel ? 'none' : undefined }}> - <AutoSizer disableHeight> - {({ width }) => { - if (width === 0) { - return null; - } + const draggable = width <= config.theme2.breakpoints.values.md ? false : isEditable; - // Disable draggable if mobile device, solving an issue with unintentionally - // moving panels. https://github.com/grafana/grafana/issues/18497 - const isLg = width <= config.theme2.breakpoints.values.md; - const draggable = isLg ? false : isEditable; - - return ( - /** - * The children is using a width of 100% so we need to guarantee that it is wrapped - * in an element that has the calculated size given by the AutoSizer. The AutoSizer - * has a width of 0 and will let its content overflow its div. - */ - <div style={{ width: width, height: '100%' }} ref={this.onGetWrapperDivRef}> - <ReactGridLayout - width={width} - isDraggable={isFnDashboard ? false : draggable} - isResizable={isFnDashboard ? false : isEditable} - containerPadding={[0, 0]} - useCSSTransforms={true} - margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]} - cols={GRID_COLUMN_COUNT} - rowHeight={GRID_CELL_HEIGHT} - draggableHandle=".grid-drag-handle" - draggableCancel=".grid-drag-cancel" - layout={this.buildLayout()} - onDragStop={this.onDragStop} - onResize={this.onResize} - onResizeStop={this.onResizeStop} - onLayoutChange={this.onLayoutChange} - > - {this.renderPanels(width, draggable)} - </ReactGridLayout> - </div> - ); - }} - </AutoSizer> + // pos: rel + z-index is required to create a new stacking context to contain + // the escalating z-indexes of the panels + return ( + <div + ref={this.onMeasureRef} + style={{ + flex: '1 1 auto', + position: 'relative', + zIndex: 1, + display: this.props.editPanel ? 'none' : undefined, + }} + > + <div style={{ width: width, height: '100%' }} ref={this.onGetWrapperDivRef}> + <ReactGridLayout + width={width} + isDraggable={isFnDashboard && !isCustomDashboard ? false : draggable} + isResizable={isFnDashboard && !isCustomDashboard ? false : isEditable} + containerPadding={[0, 0]} + useCSSTransforms={true} + margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]} + cols={GRID_COLUMN_COUNT} + rowHeight={GRID_CELL_HEIGHT} + draggableHandle=".grid-drag-handle" + draggableCancel=".grid-drag-cancel" + layout={this.buildLayout()} + onDragStop={this.onDragStop} + onResize={this.onResize} + onResizeStop={this.onResizeStop} + onLayoutChange={this.onLayoutChange} + > + {this.renderPanels(width, draggable)} + </ReactGridLayout> + </div> </div> ); } @@ -279,7 +366,7 @@ interface GrafanaGridItemProps extends React.HTMLAttributes<HTMLDivElement> { isViewing: boolean; windowHeight: number; windowWidth: number; - children: any; + children: any; // eslint-disable-line @typescript-eslint/no-explicit-any } /** @@ -320,7 +407,7 @@ const GrafanaGridItem = React.forwardRef<HTMLDivElement, GrafanaGridItemProps>(( // props.children[0] is our main children. RGL adds the drag handle at props.children[1] return ( - <div {...divProps} ref={ref}> + <div {...divProps} style={{ ...divProps.style }} ref={ref}> {/* Pass width and height to children as render props */} {[props.children[0](width, height), props.children.slice(1)]} </div> @@ -339,7 +426,8 @@ GrafanaGridItem.displayName = 'GridItemWithDimensions'; function mapStateToProps() { return (state: StoreState) => ({ isFnDashboard: state.fnGlobalState.FNDashboard, + isCustomDashboard: state.fnGlobalState.isCustomDashboard, }); } -export const DashboardGrid = connect(mapStateToProps)(Component); +export const DashboardGrid = connect(mapStateToProps)(DashboardGridUnconnected); diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index b2c34f0b392b8..b0873eabb0712 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -58,7 +58,7 @@ export class DashboardPanelUnconnected extends PureComponent<Props> { } } - onInstanceStateChange = (value: any) => { + onInstanceStateChange = (value: unknown) => { this.props.setPanelInstanceState({ key: this.props.stateKey, value }); }; diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index e6034feb9b124..f62fc3b3753eb 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -618,7 +618,7 @@ export class PanelStateWrapperDisConnected extends PureComponent<Props, State> { function mapStateToProps() { return (state: StoreState) => ({ - isFnDashboard: state.fnGlobalState.FNDashboard, + isFnDashboard: state.fnGlobalState.FNDashboard && !state.fnGlobalState.isCustomDashboard, }); } diff --git a/public/app/fn-app/create-mfe.ts b/public/app/fn-app/create-mfe.ts index d6661429f88bc..a5d196cc7e8fd 100644 --- a/public/app/fn-app/create-mfe.ts +++ b/public/app/fn-app/create-mfe.ts @@ -280,6 +280,7 @@ class createMfe { version: other.version, queryParams: other.queryParams, controlsContainer: other.controlsContainer, + isCustomDashboard: other.isCustomDashboard, }) ); } diff --git a/public/app/fn-app/fn-app-provider.tsx b/public/app/fn-app/fn-app-provider.tsx index 04d749ecd15c0..37d64c23aa485 100644 --- a/public/app/fn-app/fn-app-provider.tsx +++ b/public/app/fn-app/fn-app-provider.tsx @@ -1,10 +1,19 @@ +import { Action, KBarProvider } from 'kbar'; import { useState, useEffect, FC, PropsWithChildren } from 'react'; import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; +import { BrowserRouter, Router } from 'react-router-dom'; +import { CompatRouter } from 'react-router-dom-v5-compat'; -import { config, navigationLogger } from '@grafana/runtime'; +import { + config, + locationService, + LocationServiceProvider, + navigationLogger, + reportInteraction, +} from '@grafana/runtime'; import { ErrorBoundaryAlert, GlobalStyles } from '@grafana/ui'; import { loadAndInitAngularIfEnabled } from 'app/angular/loadAndInitAngularIfEnabled'; +import { ModalsContextProvider } from 'app/core/context/ModalsContextProvider'; import { ThemeProvider } from 'app/core/utils/ConfigProvider'; import { FnLoader } from 'app/features/dashboard/components/DashboardLoading/FnLoader'; import { FnLoggerService } from 'app/fn_logger'; @@ -31,6 +40,13 @@ export const FnAppProvider: FC<PropsWithChildren<FnAppProviderProps>> = (props) .catch(FnLoggerService.error); }, []); + const commandPaletteActionSelected = (action: Action) => { + reportInteraction('command_palette_action_selected', { + actionId: action.id, + actionName: action.name, + }); + }; + if (!store || !ready) { return <FnLoader />; } @@ -41,10 +57,21 @@ export const FnAppProvider: FC<PropsWithChildren<FnAppProviderProps>> = (props) <ErrorBoundaryAlert style="page"> <GrafanaContext.Provider value={app.context}> <ThemeProvider value={config.theme2}> - <> - <GlobalStyles /> - {children} - </> + <KBarProvider + actions={[]} + options={{ enableHistory: true, callbacks: { onSelectAction: commandPaletteActionSelected } }} + > + <Router history={locationService.getHistory()}> + <LocationServiceProvider service={locationService}> + <CompatRouter> + <ModalsContextProvider> + <GlobalStyles /> + {children} + </ModalsContextProvider> + </CompatRouter> + </LocationServiceProvider> + </Router> + </KBarProvider> </ThemeProvider> </GrafanaContext.Provider> </ErrorBoundaryAlert> diff --git a/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx b/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx index e9dac0f5ce0cf..e6c2df45428b9 100644 --- a/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx +++ b/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx @@ -17,7 +17,7 @@ const DEFAULT_DASHBOARD_PAGE_PROPS: Pick<DashboardPageProps, 'history' | 'route' path: '/d/:uid/:slug?', url: '', }, - history: {} as DashboardPageProps['history'], + history: locationService.getHistory(), route: { routeName: DashboardRoutes.Normal, path: '/d/:uid/:slug?', diff --git a/public/app/fn-app/types.ts b/public/app/fn-app/types.ts index 4a1bf55f26909..aa800f7032e49 100644 --- a/public/app/fn-app/types.ts +++ b/public/app/fn-app/types.ts @@ -30,5 +30,6 @@ export interface FNDashboardProps { isLoading: (isLoading: boolean) => void; setErrors: (errors?: { [K: number | string]: string }) => void; hiddenVariables: readonly string[]; + isCustomDashboard?: boolean; container?: HTMLElement | null; } From a42bf0a61b0a0e980656391e89697aa8225aa6f2 Mon Sep 17 00:00:00 2001 From: Gurinder Singh <gurinder@coderabbit.ai> Date: Mon, 2 Dec 2024 11:36:36 -0500 Subject: [PATCH 2/5] fixes --- .../dashboard/containers/DashboardPage.tsx | 14 +- .../dashboard/dashgrid/DashboardEmpty.tsx | 147 ++++++++++-------- 2 files changed, 87 insertions(+), 74 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 7c869e13be3eb..22165bc1b47a5 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -158,9 +158,9 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { componentDidMount() { this.initDashboard(); - const { FNDashboard, isCustomDashboard } = this.props; + const { FNDashboard } = this.props; - if (!FNDashboard || isCustomDashboard) { + if (!FNDashboard) { this.forceRouteReloadCounter = (this.props.history.location?.state as any)?.routeReloadCounter || 0; } } @@ -198,13 +198,13 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { } componentDidUpdate(prevProps: Props, prevState: State) { - const { dashboard, match, templateVarsChangedInUrl, FNDashboard, isCustomDashboard } = this.props; + const { dashboard, match, templateVarsChangedInUrl, FNDashboard } = this.props; if (!dashboard) { return; } - if (!FNDashboard || isCustomDashboard) { + if (!FNDashboard) { const routeReloadCounter = (this.props.history.location?.state as any)?.routeReloadCounter; if ( @@ -407,6 +407,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { configToggle: config.featureToggles.vizAndWidgetSplit, }); + const pageClassName = cx({ 'panel-in-fullscreen': Boolean(viewPanel), 'page-hidden': Boolean(queryParams.editview || editPanel), @@ -440,6 +441,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { layout={PageLayoutType.Canvas} className={pageClassName} onSetScrollRef={this.setScrollRef} + style={{ minHeight: 600 }} > {showToolbar && ( <header data-testid={selectors.pages.Dashboard.DashNav.navV2}> @@ -489,7 +491,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { )} </header> )} - {!FNDashboard && <DashboardPrompt dashboard={dashboard} />} + {!isFNDashboardEditable && <DashboardPrompt dashboard={dashboard} />} {initError && <DashboardFailed />} {showSubMenu && ( <section aria-label={selectors.pages.Dashboard.SubMenu.submenu}> @@ -503,7 +505,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { editPanel={editPanel} /> - {inspectPanel && !FNDashboard && <PanelInspector dashboard={dashboard} panel={inspectPanel} />} + {inspectPanel && isFNDashboardEditable && <PanelInspector dashboard={dashboard} panel={inspectPanel} />} </Page> {editPanel && sectionNav && pageNav && isFNDashboardEditable && ( <PanelEditor diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx index e8065787e83d9..a7225245d8c57 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty.tsx @@ -26,7 +26,10 @@ export interface Props { const DashboardEmpty = ({ dashboard, canCreate }: Props) => { const styles = useStyles2(getStyles); const dispatch = useDispatch(); - const initialDatasource = useSelector((state) => state.dashboard.initialDatasource); + const { initialDatasource, isFNDashboard } = useSelector((state) => ({ + initialDatasource: state.dashboard.initialDatasource, + isFNDashboard: state.fnGlobalState.FNDashboard + })); const onAddVisualization = () => { let id; @@ -63,14 +66,18 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { Start your new dashboard by adding a visualization </Trans> </Text> - <Box marginBottom={2} paddingX={4}> - <Text element="p" textAlignment="center" color="secondary"> - <Trans i18nKey="dashboard.empty.add-visualization-body"> - Select a data source and then query and visualize your data with charts, stats and tables or create - lists, markdowns and other widgets. - </Trans> - </Text> - </Box> + + <Box marginBottom={2} paddingX={4}> + <Text element="p" textAlignment="center" color="secondary"> + { + !isFNDashboard ? <Trans i18nKey="dashboard.empty.add-visualization-body"> + Select a data source and then query and visualize your data with charts, stats and tables or create + lists, markdowns and other widgets. + </Trans>: `Visualize CodeRabbit Review metrics using charts, tables and other widgets.` + } + </Text> + </Box> + <Button size="lg" icon="plus" @@ -82,83 +89,87 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => { </Button> </Stack> </Box> - <Stack direction={{ xs: 'column', md: 'row' }} wrap="wrap" gap={4}> - {config.featureToggles.vizAndWidgetSplit && ( + { + !isFNDashboard && ( + <Stack direction={{ xs: 'column', md: 'row' }} wrap="wrap" gap={4}> + {config.featureToggles.vizAndWidgetSplit && ( + <Box borderColor="strong" borderStyle="dashed" padding={3} flex={1}> + <Stack direction="column" alignItems="center" gap={1}> + <Text element="h3" textAlignment="center" weight="medium"> + <Trans i18nKey="dashboard.empty.add-widget-header">Add a widget</Trans> + </Text> + <Box marginBottom={2}> + <Text element="p" textAlignment="center" color="secondary"> + <Trans i18nKey="dashboard.empty.add-widget-body">Create lists, markdowns and other widgets</Trans> + </Text> + </Box> + <Button + icon="plus" + fill="outline" + data-testid={selectors.pages.AddDashboard.itemButton('Create new widget button')} + onClick={() => { + DashboardInteractions.emptyDashboardButtonClicked({ item: 'add_widget' }); + locationService.partial({ addWidget: true }); + }} + disabled={!canCreate} + > + <Trans i18nKey="dashboard.empty.add-widget-button">Add widget</Trans> + </Button> + </Stack> + </Box> + )} <Box borderColor="strong" borderStyle="dashed" padding={3} flex={1}> <Stack direction="column" alignItems="center" gap={1}> <Text element="h3" textAlignment="center" weight="medium"> - <Trans i18nKey="dashboard.empty.add-widget-header">Add a widget</Trans> + <Trans i18nKey="dashboard.empty.add-library-panel-header">Import panel</Trans> </Text> <Box marginBottom={2}> <Text element="p" textAlignment="center" color="secondary"> - <Trans i18nKey="dashboard.empty.add-widget-body">Create lists, markdowns and other widgets</Trans> + <Trans i18nKey="dashboard.empty.add-library-panel-body"> + Add visualizations that are shared with other dashboards. + </Trans> </Text> </Box> <Button icon="plus" fill="outline" - data-testid={selectors.pages.AddDashboard.itemButton('Create new widget button')} + data-testid={selectors.pages.AddDashboard.itemButton('Add a panel from the panel library button')} + onClick={onAddLibraryPanel} + disabled={!canCreate} + > + <Trans i18nKey="dashboard.empty.add-library-panel-button">Add library panel</Trans> + </Button> + </Stack> + </Box> + <Box borderColor="strong" borderStyle="dashed" padding={3} flex={1}> + <Stack direction="column" alignItems="center" gap={1}> + <Text element="h3" textAlignment="center" weight="medium"> + <Trans i18nKey="dashboard.empty.import-a-dashboard-header">Import a dashboard</Trans> + </Text> + <Box marginBottom={2}> + <Text element="p" textAlignment="center" color="secondary"> + <Trans i18nKey="dashboard.empty.import-a-dashboard-body"> + Import dashboards from files or <a href="https://grafana.com/grafana/dashboards/">grafana.com</a>. + </Trans> + </Text> + </Box> + <Button + icon="upload" + fill="outline" + data-testid={selectors.pages.AddDashboard.itemButton('Import dashboard button')} onClick={() => { - DashboardInteractions.emptyDashboardButtonClicked({ item: 'add_widget' }); - locationService.partial({ addWidget: true }); + DashboardInteractions.emptyDashboardButtonClicked({ item: 'import_dashboard' }); + onImportDashboard(); }} disabled={!canCreate} > - <Trans i18nKey="dashboard.empty.add-widget-button">Add widget</Trans> + <Trans i18nKey="dashboard.empty.import-dashboard-button">Import dashboard</Trans> </Button> </Stack> </Box> - )} - <Box borderColor="strong" borderStyle="dashed" padding={3} flex={1}> - <Stack direction="column" alignItems="center" gap={1}> - <Text element="h3" textAlignment="center" weight="medium"> - <Trans i18nKey="dashboard.empty.add-library-panel-header">Import panel</Trans> - </Text> - <Box marginBottom={2}> - <Text element="p" textAlignment="center" color="secondary"> - <Trans i18nKey="dashboard.empty.add-library-panel-body"> - Add visualizations that are shared with other dashboards. - </Trans> - </Text> - </Box> - <Button - icon="plus" - fill="outline" - data-testid={selectors.pages.AddDashboard.itemButton('Add a panel from the panel library button')} - onClick={onAddLibraryPanel} - disabled={!canCreate} - > - <Trans i18nKey="dashboard.empty.add-library-panel-button">Add library panel</Trans> - </Button> - </Stack> - </Box> - <Box borderColor="strong" borderStyle="dashed" padding={3} flex={1}> - <Stack direction="column" alignItems="center" gap={1}> - <Text element="h3" textAlignment="center" weight="medium"> - <Trans i18nKey="dashboard.empty.import-a-dashboard-header">Import a dashboard</Trans> - </Text> - <Box marginBottom={2}> - <Text element="p" textAlignment="center" color="secondary"> - <Trans i18nKey="dashboard.empty.import-a-dashboard-body"> - Import dashboards from files or <a href="https://grafana.com/grafana/dashboards/">grafana.com</a>. - </Trans> - </Text> - </Box> - <Button - icon="upload" - fill="outline" - data-testid={selectors.pages.AddDashboard.itemButton('Import dashboard button')} - onClick={() => { - DashboardInteractions.emptyDashboardButtonClicked({ item: 'import_dashboard' }); - onImportDashboard(); - }} - disabled={!canCreate} - > - <Trans i18nKey="dashboard.empty.import-dashboard-button">Import dashboard</Trans> - </Button> - </Stack> - </Box> - </Stack> + </Stack> + ) + } </Stack> </div> </Stack> From c602459abcfc337df02e1bb6efb811a972f0419a Mon Sep 17 00:00:00 2001 From: Gurinder Singh <gurinder@coderabbit.ai> Date: Mon, 2 Dec 2024 22:36:24 -0500 Subject: [PATCH 3/5] checkpoint --- .../dashboard/containers/DashboardPage.tsx | 14 +----- public/app/fn-app/fn-app-provider.tsx | 9 +++- .../fn-dashboard-page/render-fn-dashboard.tsx | 49 ++++++++++++------- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 22165bc1b47a5..9d396536083d4 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -274,17 +274,16 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { }; static getDerivedStateFromProps(props: Props, state: State) { - const { dashboard, queryParams } = props; + const { dashboard, queryParams, isCustomDashboard, FNDashboard } = props; const urlEditPanelId = queryParams.editPanel; const urlViewPanelId = queryParams.viewPanel; - if (!dashboard) { + if (!dashboard || (FNDashboard && !isCustomDashboard)) { return state; } const updatedState = { ...state }; - // Entering edit mode if (!state.editPanel && urlEditPanelId) { const panel = dashboard.getPanelByUrlId(urlEditPanelId); @@ -399,15 +398,6 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { const isFNDashboardEditable = (isCustomDashboard && FNDashboard) || !FNDashboard; - console.log('Edit Panel: ', { editPanel, sectionNav, pageNav, isFNDashboardEditable }); - console.log('Dashboard settings: ', { editView: queryParams.editview, pageNav, sectionNav, isFNDashboardEditable }); - console.log('Add Widget: ', { - isFNDashboardEditable, - addWidget: queryParams.addWidget, - configToggle: config.featureToggles.vizAndWidgetSplit, - }); - - const pageClassName = cx({ 'panel-in-fullscreen': Boolean(viewPanel), 'page-hidden': Boolean(queryParams.editview || editPanel), diff --git a/public/app/fn-app/fn-app-provider.tsx b/public/app/fn-app/fn-app-provider.tsx index 37d64c23aa485..b9392fe9147b1 100644 --- a/public/app/fn-app/fn-app-provider.tsx +++ b/public/app/fn-app/fn-app-provider.tsx @@ -12,7 +12,9 @@ import { reportInteraction, } from '@grafana/runtime'; import { ErrorBoundaryAlert, GlobalStyles } from '@grafana/ui'; +import { AngularRoot } from 'app/angular/AngularRoot'; import { loadAndInitAngularIfEnabled } from 'app/angular/loadAndInitAngularIfEnabled'; +import { AppChrome } from 'app/core/components/AppChrome/AppChrome'; import { ModalsContextProvider } from 'app/core/context/ModalsContextProvider'; import { ThemeProvider } from 'app/core/utils/ConfigProvider'; import { FnLoader } from 'app/features/dashboard/components/DashboardLoading/FnLoader'; @@ -66,7 +68,12 @@ export const FnAppProvider: FC<PropsWithChildren<FnAppProviderProps>> = (props) <CompatRouter> <ModalsContextProvider> <GlobalStyles /> - {children} + <div className="grafana-app"> + <AppChrome> + <AngularRoot /> + {children} + </AppChrome> + </div> </ModalsContextProvider> </CompatRouter> </LocationServiceProvider> diff --git a/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx b/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx index e6c2df45428b9..05d5206261aa4 100644 --- a/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx +++ b/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx @@ -1,4 +1,4 @@ -import { merge, isFunction } from 'lodash'; +import { merge, isFunction, isEqual } from 'lodash'; import { useEffect, FC, useMemo } from 'react'; import { locationService as locationSrv, HistoryWrapper } from '@grafana/runtime'; @@ -50,26 +50,39 @@ export const RenderFNDashboard: FC<FNDashboardProps> = (props) => { }, [firstError, setErrors]); useEffect(() => { - locationService.fnPathnameChange(window.location.pathname, queryParams); + const searchParams = getSearchParamsObject(); + if (isEqual(searchParams, queryParams)) { + return; + } + locationService.fnPathnameChange(window.location.pathname, { + ...searchParams, + ...queryParams, + }); }, [queryParams]); - const dashboardPageProps: DashboardPageProps = useMemo( - () => - merge({}, DEFAULT_DASHBOARD_PAGE_PROPS, { - ...DEFAULT_DASHBOARD_PAGE_PROPS, - match: { - params: { - ...props, - }, + const dashboardPageProps: DashboardPageProps = useMemo(() => { + return merge({}, DEFAULT_DASHBOARD_PAGE_PROPS, { + ...DEFAULT_DASHBOARD_PAGE_PROPS, + match: { + params: { + ...props, }, - location: locationService.getLocation(), - queryParams, - hiddenVariables, - controlsContainer, - isLoading, - }), - [controlsContainer, hiddenVariables, isLoading, props, queryParams] - ); + }, + location: locationService.getLocation(), + queryParams: { + ...getSearchParamsObject(), + ...queryParams, + }, + hiddenVariables, + controlsContainer, + isLoading, + }); + }, [controlsContainer, hiddenVariables, isLoading, props, queryParams]); return <DashboardPage {...dashboardPageProps} />; }; + +function getSearchParamsObject() { + const searchParams = new URLSearchParams(window.location.search); + return Object.fromEntries(searchParams.entries()); +} From 38e481cb1e47efce3c004049ab8be4244832cb71 Mon Sep 17 00:00:00 2001 From: Gurinder Singh <gurinder@coderabbit.ai> Date: Wed, 4 Dec 2024 19:38:28 -0500 Subject: [PATCH 4/5] checkpoint --- .../components/PanelEditor/PanelEditor.tsx | 52 ++++++++++++++----- .../dashboard/containers/DashboardPage.tsx | 6 ++- public/app/fn-app/fn-app-provider.tsx | 3 +- .../fn-app/fn-dashboard-page/fn-dashboard.tsx | 2 +- 4 files changed, 46 insertions(+), 17 deletions(-) diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index a77c269d0ca0e..9299447f5b8cb 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -72,6 +72,8 @@ const mapStateToProps = (state: StoreState, ownProps: OwnProps) => { uiState: state.panelEditor.ui, tableViewEnabled: state.panelEditor.tableViewEnabled, variables: getVariablesByKey(ownProps.dashboard.uid, state), + isMFEDashboard: state.fnGlobalState.FNDashboard, + isMFECustomDashboard: state.fnGlobalState.isCustomDashboard, }; }; @@ -431,27 +433,33 @@ export class PanelEditorUnconnected extends PureComponent<Props> { }; render() { - const { initDone, uiState, theme, sectionNav, pageNav, className, updatePanelEditorUIState } = this.props; + const { initDone, uiState, theme, sectionNav, pageNav, className, updatePanelEditorUIState, isMFECustomDashboard } = this.props; const styles = getStyles(theme, this.props); if (!initDone) { return null; } - return ( - <Page - navModel={sectionNav} - pageNav={pageNav} - data-testid={selectors.components.PanelEditor.General.content} - layout={PageLayoutType.Custom} - className={className} - > - <AppChromeUpdate + console.log('PanelEditor.tsx this.props', { + isPanelOptionVisible: uiState.isPanelOptionsVisible, + isMFECustomDashboard, + modelState: this.state.showSaveLibraryPanelModal, + }); + + const editPanelContent = ( + <> + { + !isMFECustomDashboard ? ( + <AppChromeUpdate actions={<ToolbarButtonRow alignment="right">{this.renderEditorActions()}</ToolbarButtonRow>} - /> + /> + ): ( + <ToolbarButtonRow alignment="right">{this.renderEditorActions()}</ToolbarButtonRow> + ) + } <div className={styles.wrapper}> <div className={styles.verticalSplitPanesWrapper}> - {!uiState.isPanelOptionsVisible ? ( + {!uiState.isPanelOptionsVisible || isMFECustomDashboard ? ( this.renderPanelAndEditor(uiState, styles) ) : ( <SplitPaneWrapper @@ -480,7 +488,25 @@ export class PanelEditorUnconnected extends PureComponent<Props> { /> )} </div> - </Page> + </> + ) + + return ( + <> + { + !isMFECustomDashboard ? ( + <Page + navModel={sectionNav} + pageNav={pageNav} + data-testid={selectors.components.PanelEditor.General.content} + layout={PageLayoutType.Custom} + className={className} + > + {editPanelContent} + </Page> + ): <div>{editPanelContent}</div> + } + </> ); } } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 9d396536083d4..1b086ed812084 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -423,6 +423,8 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { </Portal> ); + const isPanelEditorVisible = editPanel && sectionNav && pageNav && isFNDashboardEditable + return ( <> <Page @@ -431,7 +433,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { layout={PageLayoutType.Canvas} className={pageClassName} onSetScrollRef={this.setScrollRef} - style={{ minHeight: 600 }} + style={{ minHeight: 600, position: 'relative' }} > {showToolbar && ( <header data-testid={selectors.pages.Dashboard.DashNav.navV2}> @@ -497,7 +499,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { {inspectPanel && isFNDashboardEditable && <PanelInspector dashboard={dashboard} panel={inspectPanel} />} </Page> - {editPanel && sectionNav && pageNav && isFNDashboardEditable && ( + {isPanelEditorVisible && ( <PanelEditor dashboard={dashboard} sourcePanel={editPanel} diff --git a/public/app/fn-app/fn-app-provider.tsx b/public/app/fn-app/fn-app-provider.tsx index b9392fe9147b1..01fcf0616c155 100644 --- a/public/app/fn-app/fn-app-provider.tsx +++ b/public/app/fn-app/fn-app-provider.tsx @@ -11,7 +11,7 @@ import { navigationLogger, reportInteraction, } from '@grafana/runtime'; -import { ErrorBoundaryAlert, GlobalStyles } from '@grafana/ui'; +import { ErrorBoundaryAlert, GlobalStyles, ModalRoot } from '@grafana/ui'; import { AngularRoot } from 'app/angular/AngularRoot'; import { loadAndInitAngularIfEnabled } from 'app/angular/loadAndInitAngularIfEnabled'; import { AppChrome } from 'app/core/components/AppChrome/AppChrome'; @@ -74,6 +74,7 @@ export const FnAppProvider: FC<PropsWithChildren<FnAppProviderProps>> = (props) {children} </AppChrome> </div> + <ModalRoot /> </ModalsContextProvider> </CompatRouter> </LocationServiceProvider> diff --git a/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx b/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx index 93c50e5d3c52a..77f77f1191317 100644 --- a/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx +++ b/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx @@ -54,7 +54,7 @@ export const DashboardPortal: FC<FNDashboardComponentProps> = (p) => { return ( <RenderPortal ID="grafana-portal"> - <div className="page-dashboard">{content}</div> + {content} </RenderPortal> ); }; From d57e22e3971937f1d138107e710ff11ac7aa8c1a Mon Sep 17 00:00:00 2001 From: Gurinder Singh <gurinder@coderabbit.ai> Date: Thu, 12 Dec 2024 16:56:23 -0500 Subject: [PATCH 5/5] checkpoint --- .../src/components/Portal/Portal.tsx | 1 + public/app/AppWrapper.tsx | 35 +++++++----- public/app/app.ts | 32 ++++++++--- public/app/core/context/ModalsProvider.ts | 0 .../components/PanelEditor/PanelEditor.tsx | 56 +++++++------------ .../PanelEditor/PanelEditorTabs.tsx | 9 +-- .../dashboard/containers/DashboardPage.tsx | 2 +- .../features/query/components/QueryGroup.tsx | 10 +++- public/app/fn-app/create-mfe.ts | 4 +- .../fn-app/fn-dashboard-page/fn-dashboard.tsx | 10 +++- .../fn-dashboard-page/render-fn-dashboard.tsx | 16 ++++-- 11 files changed, 103 insertions(+), 72 deletions(-) delete mode 100644 public/app/core/context/ModalsProvider.ts diff --git a/packages/grafana-ui/src/components/Portal/Portal.tsx b/packages/grafana-ui/src/components/Portal/Portal.tsx index f4f88831e72e8..62ac76b6a4f1c 100644 --- a/packages/grafana-ui/src/components/Portal/Portal.tsx +++ b/packages/grafana-ui/src/components/Portal/Portal.tsx @@ -60,6 +60,7 @@ export function PortalContainer() { return ( <div id="grafana-portal-container" + data-qiankun="grafana-full-app" className={cx({ [styles.grafanaPortalContainer]: isBodyScrolling, })} diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index b73839203c2cb..99d9f6a2f4b9a 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -30,6 +30,8 @@ import { LiveConnectionWarning } from './features/live/LiveConnectionWarning'; interface AppWrapperProps { app: GrafanaApp; + isMFE?: boolean; + children?: React.ReactNode | null; } interface AppWrapperState { @@ -88,7 +90,7 @@ export class AppWrapper extends Component<AppWrapperProps, AppWrapperState> { } render() { - const { app } = this.props; + const { app, isMFE, children } = this.props; const { ready } = this.state; navigationLogger('AppWrapper', false, 'rendering'); @@ -116,22 +118,29 @@ export class AppWrapper extends Component<AppWrapperProps, AppWrapperState> { <GlobalStyles /> <div className="grafana-app"> <AppChrome> - <AngularRoot /> - <AppNotificationList /> - <Stack gap={0} grow={1} direction="column"> - {pageBanners.map((Banner, index) => ( - <Banner key={index.toString()} /> + <> + <AngularRoot /> + <AppNotificationList /> + <Stack gap={0} grow={1} direction="column"> + {pageBanners.map((Banner, index) => ( + <Banner key={index.toString()} /> + ))} + {ready && !isMFE && this.renderRoutes()} + </Stack> + {bodyRenderHooks.map((Hook, index) => ( + <Hook key={index.toString()} /> ))} - {ready && this.renderRoutes()} - </Stack> - {bodyRenderHooks.map((Hook, index) => ( - <Hook key={index.toString()} /> - ))} + </> + {children} </AppChrome> </div> <LiveConnectionWarning /> - <ModalRoot /> - <PortalContainer /> + {!isMFE && ( + <> + <ModalRoot /> + <PortalContainer /> + </> + )} </ModalsContextProvider> </CompatRouter> </LocationServiceProvider> diff --git a/public/app/app.ts b/public/app/app.ts index 278884385f3d5..2ebe82e9e1729 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -44,7 +44,7 @@ import { import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView'; import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer'; import { setPluginPage } from '@grafana/runtime/src/components/PluginPage'; -import config, { updateConfig } from 'app/core/config'; +import config, { Settings, updateConfig } from 'app/core/config'; import { arrayMove } from 'app/core/utils/arrayMove'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -125,7 +125,7 @@ if (process.env.NODE_ENV === 'development') { export class GrafanaApp { context!: GrafanaContextType; - async init() { + async init(isMFE = false) { try { // Let iframe container know grafana has started loading parent.postMessage('GrafanaAppInit', '*'); @@ -133,7 +133,19 @@ export class GrafanaApp { const initI18nPromise = initializeI18n(config.bootData.user.language); initI18nPromise.then(({ language }) => updateConfig({ language })); - setBackendSrv(backendSrv); + if(isMFE){ + backendSrv.setGrafanaPrefix(true); + setBackendSrv(backendSrv); + const settings: Settings = await backendSrv.get('/api/frontend/settings'); + + config.panels = settings.panels; + config.datasources = settings.datasources; + config.defaultDatasource = settings.defaultDatasource; + } else { + setBackendSrv(backendSrv); + } + + initEchoSrv(); initIconCache(); // This needs to be done after the `initEchoSrv` since it is being used under the hood. @@ -270,12 +282,14 @@ export class GrafanaApp { initializeScopes(); - const root = createRoot(document.getElementById('reactRoot')!); - root.render( - createElement(AppWrapper, { - app: this, - }) - ); + if(!isMFE){ + const root = createRoot(document.getElementById('reactRoot')!); + root.render( + createElement(AppWrapper, { + app: this, + }) + ); + } } catch (error) { console.error('Failed to start Grafana', error); window.__grafana_load_failed(); diff --git a/public/app/core/context/ModalsProvider.ts b/public/app/core/context/ModalsProvider.ts deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 9299447f5b8cb..c95d0c6e66cfc 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -277,6 +277,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> { dashboard={dashboard} tabs={tabs} onChangeTab={this.onChangeTab} + isMfeEditPanel={this.props.isMFEDashboard} /> </div> </SplitPaneWrapper> @@ -433,33 +434,32 @@ export class PanelEditorUnconnected extends PureComponent<Props> { }; render() { - const { initDone, uiState, theme, sectionNav, pageNav, className, updatePanelEditorUIState, isMFECustomDashboard } = this.props; + const { initDone, uiState, theme, sectionNav, pageNav, className, updatePanelEditorUIState, isMFECustomDashboard } = + this.props; const styles = getStyles(theme, this.props); if (!initDone) { return null; } - console.log('PanelEditor.tsx this.props', { - isPanelOptionVisible: uiState.isPanelOptionsVisible, - isMFECustomDashboard, - modelState: this.state.showSaveLibraryPanelModal, - }); - - const editPanelContent = ( - <> - { - !isMFECustomDashboard ? ( + return ( + <Page + navModel={sectionNav} + pageNav={pageNav} + data-testid={selectors.components.PanelEditor.General.content} + layout={PageLayoutType.Custom} + className={!isMFECustomDashboard ? className : styles.mfeWrapper} + > + {!isMFECustomDashboard ? ( <AppChromeUpdate - actions={<ToolbarButtonRow alignment="right">{this.renderEditorActions()}</ToolbarButtonRow>} + actions={<ToolbarButtonRow alignment="right">{this.renderEditorActions()}</ToolbarButtonRow>} /> - ): ( + ) : ( <ToolbarButtonRow alignment="right">{this.renderEditorActions()}</ToolbarButtonRow> - ) - } + )} <div className={styles.wrapper}> <div className={styles.verticalSplitPanesWrapper}> - {!uiState.isPanelOptionsVisible || isMFECustomDashboard ? ( + {!uiState.isPanelOptionsVisible ? ( this.renderPanelAndEditor(uiState, styles) ) : ( <SplitPaneWrapper @@ -488,25 +488,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> { /> )} </div> - </> - ) - - return ( - <> - { - !isMFECustomDashboard ? ( - <Page - navModel={sectionNav} - pageNav={pageNav} - data-testid={selectors.components.PanelEditor.General.content} - layout={PageLayoutType.Custom} - className={className} - > - {editPanelContent} - </Page> - ): <div>{editPanelContent}</div> - } - </> + </Page> ); } } @@ -521,12 +503,16 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2, props: Props) => { const paneSpacing = theme.spacing(2); return { + mfeWrapper: css({ + height: '100vh', + }), wrapper: css({ width: '100%', flexGrow: 1, minHeight: 0, display: 'flex', paddingTop: theme.spacing(2), + height: '100%', }), verticalSplitPanesWrapper: css({ display: 'flex', diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx index cad04df45f339..33240377edf1b 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx @@ -20,9 +20,10 @@ interface PanelEditorTabsProps { dashboard: DashboardModel; tabs: PanelEditorTab[]; onChangeTab: (tab: PanelEditorTab) => void; + isMfeEditPanel?: boolean; } -export const PanelEditorTabs = memo(({ panel, dashboard, tabs, onChangeTab }: PanelEditorTabsProps) => { +export const PanelEditorTabs = memo(({ panel, dashboard, tabs, onChangeTab, isMfeEditPanel }: PanelEditorTabsProps) => { const forceUpdate = useForceUpdate(); const styles = useStyles2(getStyles); @@ -49,7 +50,7 @@ export const PanelEditorTabs = memo(({ panel, dashboard, tabs, onChangeTab }: Pa return () => eventSubs.unsubscribe(); }, [panel, dashboard, forceUpdate]); - const activeTab = tabs.find((item) => item.active)!; + const activeTab = !isMfeEditPanel? tabs.find((item) => item.active)!: tabs.find((item) => item.id === PanelEditorTabId.Query)!; if (tabs.length === 0) { return null; @@ -60,7 +61,7 @@ export const PanelEditorTabs = memo(({ panel, dashboard, tabs, onChangeTab }: Pa return ( <div className={styles.wrapper}> <TabsBar className={styles.tabBar} hideBorder> - {tabs.map((tab) => { + {!isMfeEditPanel ? tabs.map((tab) => { if (tab.id === PanelEditorTabId.Alert && alertingEnabled) { return ( <PanelAlertTab @@ -84,7 +85,7 @@ export const PanelEditorTabs = memo(({ panel, dashboard, tabs, onChangeTab }: Pa counter={getCounter(panel, tab)} /> ); - })} + }): null} </TabsBar> <TabContent className={styles.tabContent}> {activeTab.id === PanelEditorTabId.Query && <PanelEditorQueries panel={panel} queries={panel.targets} />} diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 1b086ed812084..01983604dc0bf 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -483,7 +483,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> { )} </header> )} - {!isFNDashboardEditable && <DashboardPrompt dashboard={dashboard} />} + <DashboardPrompt dashboard={dashboard} /> {initError && <DashboardFailed />} {showSubMenu && ( <section aria-label={selectors.pages.Dashboard.SubMenu.submenu}> diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index ffa97a3fce235..943bc721c74d2 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -27,7 +27,7 @@ import { dataSource as expressionDatasource } from 'app/features/expressions/Exp import { AngularDeprecationPluginNotice } from 'app/features/plugins/angularDeprecation/AngularDeprecationPluginNotice'; import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard'; import { GrafanaQuery } from 'app/plugins/datasource/grafana/types'; -import { QueryGroupOptions } from 'app/types'; +import { QueryGroupOptions, StoreState, useSelector } from 'app/types'; import { isAngularDatasourcePluginAndNotHidden } from '../../plugins/angularDeprecation/utils'; import { PanelQueryRunner } from '../state/PanelQueryRunner'; @@ -37,6 +37,7 @@ import { GroupActionComponents } from './QueryActionComponent'; import { QueryEditorRows } from './QueryEditorRows'; import { QueryGroupOptionsEditor } from './QueryGroupOptions'; + export interface Props { queryRunner: PanelQueryRunner; options: QueryGroupOptions; @@ -404,6 +405,13 @@ export function QueryGroupTopSection({ }: QueryGroupTopSectionProps) { const styles = getStyles(); const [isHelpOpen, setIsHelpOpen] = useState(false); + const { FNDashboard, isCustomDashboard } = useSelector((state: StoreState) => state.fnGlobalState); + + // do not render data source selection options in micro frontend dashboard + if(isCustomDashboard && FNDashboard){ + return null; + } + return ( <> <div data-testid={selectors.components.QueryTab.queryGroupTopSection}> diff --git a/public/app/fn-app/create-mfe.ts b/public/app/fn-app/create-mfe.ts index a5d196cc7e8fd..47b84e8006fc4 100644 --- a/public/app/fn-app/create-mfe.ts +++ b/public/app/fn-app/create-mfe.ts @@ -14,6 +14,7 @@ import { GrafanaTheme2 } from '@grafana/data/src/themes/types'; import { ThemeChangedEvent } from '@grafana/runtime'; import { GrafanaBootConfig } from '@grafana/runtime/src/config'; import { getTheme } from '@grafana/ui'; +import app from 'app/app'; import appEvents from 'app/core/app_events'; import config from 'app/core/config'; import { @@ -25,7 +26,6 @@ import { fnStateProps, } from 'app/core/reducers/fn-slice'; import { backendSrv } from 'app/core/services/backend_srv'; -import fn_app from 'app/fn_app'; import { FnLoggerService } from 'app/fn_logger'; import { dispatch } from 'app/store/store'; @@ -89,7 +89,7 @@ class createMfe { } static boot() { - return () => fn_app.init(); + return () => app.init(true); } private static toggleTheme = (mode: FNDashboardProps['mode']): GrafanaThemeType.Light | GrafanaThemeType.Dark => diff --git a/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx b/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx index 77f77f1191317..e27ff542d8aff 100644 --- a/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx +++ b/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx @@ -1,9 +1,11 @@ import { FC, useMemo } from 'react'; +import { ModalRoot, PortalContainer } from '@grafana/ui'; +import { AppWrapper } from 'app/AppWrapper'; +import app from 'app/app'; import { FnGlobalState, FnPropMappedFromState } from 'app/core/reducers/fn-slice'; import { useSelector } from 'app/types'; -import { FnAppProvider } from '../fn-app-provider'; import { FNDashboardProps } from '../types'; import { RenderPortal } from '../utils'; @@ -13,9 +15,9 @@ type FNDashboardComponentProps = Omit<FNDashboardProps, FnPropMappedFromState>; export const FNDashboard: FC<FNDashboardComponentProps> = (props) => { return ( - <FnAppProvider fnError={props.fnError}> + <AppWrapper app={app} isMFE> <DashboardPortal {...props} /> - </FnAppProvider> + </AppWrapper> ); }; @@ -54,6 +56,8 @@ export const DashboardPortal: FC<FNDashboardComponentProps> = (p) => { return ( <RenderPortal ID="grafana-portal"> + <ModalRoot /> + <PortalContainer /> {content} </RenderPortal> ); diff --git a/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx b/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx index 05d5206261aa4..d28dcd6c41630 100644 --- a/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx +++ b/public/app/fn-app/fn-dashboard-page/render-fn-dashboard.tsx @@ -1,5 +1,5 @@ import { merge, isFunction, isEqual } from 'lodash'; -import { useEffect, FC, useMemo } from 'react'; +import { useEffect, FC, useMemo, useRef } from 'react'; import { locationService as locationSrv, HistoryWrapper } from '@grafana/runtime'; import DashboardPage, { DashboardPageProps } from 'app/features/dashboard/containers/DashboardPage'; @@ -28,6 +28,7 @@ const DEFAULT_DASHBOARD_PAGE_PROPS: Pick<DashboardPageProps, 'history' | 'route' export const RenderFNDashboard: FC<FNDashboardProps> = (props) => { const { queryParams, controlsContainer, setErrors, hiddenVariables, isLoading } = props; + const uidRef = useRef<string | null>(null); const firstError = useSelector((state: StoreState) => { const { appNotifications } = state; @@ -50,15 +51,22 @@ export const RenderFNDashboard: FC<FNDashboardProps> = (props) => { }, [firstError, setErrors]); useEffect(() => { - const searchParams = getSearchParamsObject(); - if (isEqual(searchParams, queryParams)) { + let searchParams = getSearchParamsObject(); + if (isEqual(searchParams, queryParams) && uidRef.current === props.uid) { return; } + if (uidRef.current !== props.uid) { + searchParams = { + ...(searchParams.from ? { from: searchParams.from } : {}), + ...(searchParams.to ? { to: searchParams.to } : {}), + }; + } locationService.fnPathnameChange(window.location.pathname, { ...searchParams, ...queryParams, }); - }, [queryParams]); + uidRef.current = props.uid; + }, [queryParams, props.uid]); const dashboardPageProps: DashboardPageProps = useMemo(() => { return merge({}, DEFAULT_DASHBOARD_PAGE_PROPS, {