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/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 (
{ } 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 {
- - - - {pageBanners.map((Banner, index) => ( - + <> + + + + {pageBanners.map((Banner, index) => ( + + ))} + {ready && !isMFE && this.renderRoutes()} + + {bodyRenderHooks.map((Hook, index) => ( + ))} - {ready && this.renderRoutes()} - - {bodyRenderHooks.map((Hook, index) => ( - - ))} + + {children}
- - + {!isMFE && ( + <> + + + + )} 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/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/context/ModalsProvider.ts b/public/app/core/context/ModalsProvider.ts deleted file mode 100644 index e69de29bb2d1d..0000000000000 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} >
@@ -431,7 +434,8 @@ export class PanelEditorUnconnected extends PureComponent { }; 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) { @@ -444,11 +448,15 @@ export class PanelEditorUnconnected extends PureComponent { pageNav={pageNav} data-testid={selectors.components.PanelEditor.General.content} layout={PageLayoutType.Custom} - className={className} + className={!isMFECustomDashboard ? className : styles.mfeWrapper} > - {this.renderEditorActions()}} - /> + {!isMFECustomDashboard ? ( + {this.renderEditorActions()}} + /> + ) : ( + {this.renderEditorActions()} + )}
{!uiState.isPanelOptionsVisible ? ( @@ -495,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 (
- {tabs.map((tab) => { + {!isMfeEditPanel ? tabs.map((tab) => { if (tab.id === PanelEditorTabId.Alert && alertingEnabled) { return ( ); - })} + }): null} {activeTab.id === PanelEditorTabId.Query && } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 5ebe4cbcd006c..01983604dc0bf 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 & { dashboard: ReturnType; navIndex: StoreState['navIndex']; - } & Pick, + } & Pick, 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; } @@ -268,17 +274,16 @@ export class UnthemedDashboardPage extends PureComponent { }; 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); @@ -354,7 +359,7 @@ export class UnthemedDashboardPage extends PureComponent { this.setState({ updateScrollTop: 0 }); }; - setScrollRef = (scrollElement: HTMLDivElement): void => { + setScrollRef = (scrollElement: ScrollRefElement): void => { this.setState({ scrollElement }); }; @@ -378,7 +383,7 @@ export class UnthemedDashboardPage extends PureComponent { } 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 +396,14 @@ export class UnthemedDashboardPage extends PureComponent { const showToolbar = FNDashboard || (kioskMode !== KioskMode.Full && !queryParams.editview); + const isFNDashboardEditable = (isCustomDashboard && FNDashboard) || !FNDashboard; + const pageClassName = cx({ 'panel-in-fullscreen': Boolean(viewPanel), 'page-hidden': Boolean(queryParams.editview || editPanel), }); - if (dashboard.meta.dashboardNotFound) { + if (dashboard.meta.dashboardNotFound && !FNDashboard) { return ( @@ -416,35 +423,67 @@ export class UnthemedDashboardPage extends PureComponent { ); + const isPanelEditorVisible = editPanel && sectionNav && pageNav && isFNDashboardEditable + return ( - + <> {showToolbar && (
{FNDashboard ? ( - FNTimeRange +
+ {isCustomDashboard && ( + <> + + {({ showModal, hideModal }) => ( + { + showModal(SaveDashboardDrawer, { + dashboard, + onDismiss: hideModal, + }); + }} + /> + )} + + + + )} + {FNTimeRange} +
) : ( )}
)} - {!FNDashboard && } + {initError && } {showSubMenu && (
@@ -453,14 +492,14 @@ export class UnthemedDashboardPage extends PureComponent { )} - {inspectPanel && !FNDashboard && } + {inspectPanel && isFNDashboardEditable && } - {editPanel && !FNDashboard && sectionNav && pageNav && ( + {isPanelEditorVisible && ( { pageNav={pageNav} /> )} - {queryParams.editview && !FNDashboard && pageNav && sectionNav && ( + {queryParams.editview && pageNav && sectionNav && isFNDashboardEditable && ( { sectionNav={sectionNav} /> )} - {!FNDashboard && queryParams.addWidget && config.featureToggles.vizAndWidgetSplit && } - + {isFNDashboardEditable && queryParams.addWidget && config.featureToggles.vizAndWidgetSplit && ( + + )} + ); } } 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/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 - - - - Select a data source and then query and visualize your data with charts, stats and tables or create - lists, markdowns and other widgets. - - - + + + + { + !isFNDashboard ? + Select a data source and then query and visualize your data with charts, stats and tables or create + lists, markdowns and other widgets. + : `Visualize CodeRabbit Review metrics using charts, tables and other widgets.` + } + + + - - {config.featureToggles.vizAndWidgetSplit && ( + { + !isFNDashboard && ( + + {config.featureToggles.vizAndWidgetSplit && ( + + + + Add a widget + + + + Create lists, markdowns and other widgets + + + + + + )} - Add a widget + Import panel - Create lists, markdowns and other widgets + + Add visualizations that are shared with other dashboards. + + + + + + + Import a dashboard + + + + + Import dashboards from files or grafana.com. + + + + - )} - - - - Import panel - - - - - Add visualizations that are shared with other dashboards. - - - - - - - - - - Import a dashboard - - - - - Import dashboards from files or grafana.com. - - - - - - - + + ) + }
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 { +export class DashboardGridUnconnected extends PureComponent { private panelMap: { [key: string]: PanelModel } = {}; private eventSubs = new Subscription(); private windowHeight = 1200; @@ -40,10 +50,41 @@ export class Component extends PureComponent { 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 { 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 { 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 { } renderPanels(gridWidth: number, isDashboardDraggable: boolean) { + const { panelFilter } = this.state; const panelElements = []; // Reset last panel bottom @@ -155,7 +226,7 @@ export class Component extends PureComponent { for (const panel of this.props.dashboard.panels) { const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.isViewing }); - panelElements.push( + const p = ( { }} ); + + 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 { } }; + 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 ; } - /** - * 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 ( -
- - {({ 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. - */ -
- - {this.renderPanels(width, draggable)} - -
- ); - }} -
+ // pos: rel + z-index is required to create a new stacking context to contain + // the escalating z-indexes of the panels + return ( +
+
+ + {this.renderPanels(width, draggable)} + +
); } @@ -279,7 +366,7 @@ interface GrafanaGridItemProps extends React.HTMLAttributes { 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(( // props.children[0] is our main children. RGL adds the drag handle at props.children[1] return ( -
+
{/* Pass width and height to children as render props */} {[props.children[0](width, height), props.children.slice(1)]}
@@ -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 { } } - 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 { function mapStateToProps() { return (state: StoreState) => ({ - isFnDashboard: state.fnGlobalState.FNDashboard, + isFnDashboard: state.fnGlobalState.FNDashboard && !state.fnGlobalState.isCustomDashboard, }); } 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 ( <>
diff --git a/public/app/fn-app/create-mfe.ts b/public/app/fn-app/create-mfe.ts index d6661429f88bc..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 => @@ -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..01fcf0616c155 100644 --- a/public/app/fn-app/fn-app-provider.tsx +++ b/public/app/fn-app/fn-app-provider.tsx @@ -1,10 +1,21 @@ +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 { ErrorBoundaryAlert, GlobalStyles } from '@grafana/ui'; +import { + config, + locationService, + LocationServiceProvider, + navigationLogger, + reportInteraction, +} from '@grafana/runtime'; +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'; +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 +42,13 @@ export const FnAppProvider: FC> = (props) .catch(FnLoggerService.error); }, []); + const commandPaletteActionSelected = (action: Action) => { + reportInteraction('command_palette_action_selected', { + actionId: action.id, + actionName: action.name, + }); + }; + if (!store || !ready) { return ; } @@ -41,10 +59,27 @@ export const FnAppProvider: FC> = (props) - <> - - {children} - + + + + + + +
+ + + {children} + +
+ +
+
+
+
+
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..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; export const FNDashboard: FC = (props) => { return ( - + - + ); }; @@ -54,7 +56,9 @@ export const DashboardPortal: FC = (p) => { return ( -
{content}
+ + + {content}
); }; 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..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 } from 'lodash'; -import { useEffect, FC, useMemo } from 'react'; +import { merge, isFunction, isEqual } from 'lodash'; +import { useEffect, FC, useMemo, useRef } from 'react'; import { locationService as locationSrv, HistoryWrapper } from '@grafana/runtime'; import DashboardPage, { DashboardPageProps } from 'app/features/dashboard/containers/DashboardPage'; @@ -17,7 +17,7 @@ const DEFAULT_DASHBOARD_PAGE_PROPS: Pick = (props) => { const { queryParams, controlsContainer, setErrors, hiddenVariables, isLoading } = props; + const uidRef = useRef(null); const firstError = useSelector((state: StoreState) => { const { appNotifications } = state; @@ -50,26 +51,46 @@ export const RenderFNDashboard: FC = (props) => { }, [firstError, setErrors]); useEffect(() => { - locationService.fnPathnameChange(window.location.pathname, queryParams); - }, [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, + }); + uidRef.current = props.uid; + }, [queryParams, props.uid]); - 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 ; }; + +function getSearchParamsObject() { + const searchParams = new URLSearchParams(window.location.search); + return Object.fromEntries(searchParams.entries()); +} 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; }