From 60c04f4420bc8d28939bd1b1eca1f65adfd2f5e9 Mon Sep 17 00:00:00 2001 From: Katarzyna Ziomek-Zdanowicz Date: Wed, 11 Jan 2023 17:32:55 +0100 Subject: [PATCH] 7829 Read hiddenVariables, mode, FNDashboard from grfana's state --- public/app/core/reducers/fn-slice.ts | 86 +++++++++-------- .../dashboard/components/SubMenu/SubMenu.tsx | 5 +- .../components/SubMenu/SubMenuItems.tsx | 3 +- .../dashboard/containers/DashboardPage.tsx | 46 ++++----- public/app/fn-app/create-mfe.ts | 96 +++++++++++-------- .../fn-app/fn-dashboard-page/fn-dashboard.tsx | 90 +++++++++++------ .../fn-dashboard-page/render-fn-dashboard.tsx | 39 +------- public/app/fn-app/types.ts | 2 +- 8 files changed, 200 insertions(+), 167 deletions(-) diff --git a/public/app/core/reducers/fn-slice.ts b/public/app/core/reducers/fn-slice.ts index 57f9d767eec0b..0d9128ade0ae6 100644 --- a/public/app/core/reducers/fn-slice.ts +++ b/public/app/core/reducers/fn-slice.ts @@ -1,8 +1,6 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { WritableDraft } from 'immer/dist/internal'; +import { createSlice, PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; import { GrafanaThemeType } from '@grafana/data'; -import { dispatch } from 'app/store/store'; import { AnyObject } from '../../fn-app/types'; @@ -14,17 +12,41 @@ export interface FnGlobalState { controlsContainer: string | null; pageTitle: string; queryParams: AnyObject; - hiddenVariables: string[]; + hiddenVariables: readonly string[]; } -export type UpdateFNGlobalStateAction = PayloadAction<{ - type: keyof FnGlobalState; - payload: FnGlobalState[keyof FnGlobalState]; -}>; +export type UpdateFNGlobalStateAction = PayloadAction>; + +export type SetFnStateAction = PayloadAction>; + +export type FnPropMappedFromState = Extract; +export type FnStateProp = keyof FnGlobalState; + +export type FnPropsMappedFromState = Pick; + +export const fnStateProps: FnStateProp[] = [ + 'FNDashboard', + 'controlsContainer', + 'hiddenVariables', + 'mode', + 'pageTitle', + 'queryParams', + 'slug', + 'uid', +]; + +export const fnPropsMappedFromState: readonly FnPropMappedFromState[] = [ + 'FNDashboard', + 'hiddenVariables', + 'mode', +] as const; const INITIAL_MODE = GrafanaThemeType.Light; -const initialState: FnGlobalState = { +export const FN_STATE_KEY = 'fnGlobalState'; + +export const INITIAL_FN_STATE: FnGlobalState = { + // NOTE: initial value is false FNDashboard: false, uid: '', slug: '', @@ -33,37 +55,25 @@ const initialState: FnGlobalState = { pageTitle: '', queryParams: {}, hiddenVariables: [], -}; +} as const; -const fnSlice = createSlice({ - name: 'fnGlobalState', - initialState, - reducers: { - setInitialMountState: (state, action: PayloadAction>) => { - return { ...state, ...action.payload }; - }, - updateFnState: (state: WritableDraft, action: UpdateFNGlobalStateAction) => { - const { type, payload } = action.payload; - - return { - ...state, - [type]: payload, - }; - }, +const reducers: SliceCaseReducers = { + updateFnState: (state, action: SetFnStateAction) => { + return { ...state, ...action.payload }; + }, + updatePartialFnStates: (state, action: UpdateFNGlobalStateAction) => { + return { + ...state, + ...action.payload, + }; }, +}; + +const fnSlice = createSlice, string>({ + name: FN_STATE_KEY, + initialState: INITIAL_FN_STATE, + reducers, }); -export const { updateFnState, setInitialMountState } = fnSlice.actions; +export const { updatePartialFnStates, updateFnState } = fnSlice.actions; export const fnSliceReducer = fnSlice.reducer; - -export const updateFNGlobalState = ( - type: keyof FnGlobalState, - payload: UpdateFNGlobalStateAction['payload']['payload'] -): void => { - dispatch( - updateFnState({ - type, - payload, - }) - ); -}; diff --git a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx index aaa4654108e7c..28f611ae6179d 100644 --- a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx +++ b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx @@ -3,6 +3,7 @@ import React, { PureComponent } from 'react'; import { connect, MapStateToProps } from 'react-redux'; import { AnnotationQuery, DataQuery } from '@grafana/data'; +import { FnGlobalState } from 'app/core/reducers/fn-slice'; import { StoreState } from '../../../../types'; import { getSubMenuVariables, getVariablesState } from '../../../variables/state/selectors'; @@ -18,11 +19,11 @@ interface OwnProps { dashboard: DashboardModel; links: DashboardLink[]; annotations: AnnotationQuery[]; - hiddenVariables?: string[]; } interface ConnectedProps { variables: VariableModel[]; + hiddenVariables: FnGlobalState['hiddenVariables']; } interface DispatchProps {} @@ -76,8 +77,10 @@ class SubMenuUnConnected extends PureComponent { const mapStateToProps: MapStateToProps = (state, ownProps) => { const { uid } = ownProps.dashboard; const templatingState = getVariablesState(uid, state); + return { variables: getSubMenuVariables(uid, templatingState.variables), + hiddenVariables: state.fnGlobalState.hiddenVariables, }; }; diff --git a/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx b/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx index 16328ff2796d6..0ecff8147295d 100644 --- a/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx +++ b/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx @@ -1,6 +1,7 @@ import React, { FunctionComponent, useEffect, useState } from 'react'; import { selectors } from '@grafana/e2e-selectors'; +import { FnGlobalState } from 'app/core/reducers/fn-slice'; import { PickerRenderer } from '../../../variables/pickers/PickerRenderer'; import { VariableHide, VariableModel } from '../../../variables/types'; @@ -8,7 +9,7 @@ import { VariableHide, VariableModel } from '../../../variables/types'; interface Props { variables: VariableModel[]; readOnly?: boolean; - hiddenVariables?: string[]; + hiddenVariables?: FnGlobalState['hiddenVariables']; } export const SubMenuItems: FunctionComponent = ({ variables, readOnly, hiddenVariables }) => { diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 2925968a9da54..3951ea820162b 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -12,6 +12,7 @@ import { GrafanaContext, GrafanaContextType } from 'app/core/context/GrafanaCont import { createErrorNotification } from 'app/core/copy/appNotification'; 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 { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; @@ -60,7 +61,10 @@ export type DashboardPageRouteSearchParams = { }; export type MapStateToDashboardPageProps = MapStateToProps< - Pick & { dashboard: ReturnType }, + Pick & { dashboard: ReturnType } & Pick< + FnGlobalState, + 'FNDashboard' + >, OwnProps, StoreState >; @@ -80,6 +84,7 @@ export const mapStateToProps: MapStateToDashboardPageProps = (state) => ({ initError: state.dashboard.initError, dashboard: state.dashboard.getModel(), navIndex: state.navIndex, + FNDashboard: state.fnGlobalState.FNDashboard, }); const mapDispatchToProps: MapDispatchToDashboardPageProps = { @@ -94,9 +99,7 @@ const connector = connect(mapStateToProps, mapDispatchToProps); type OwnProps = { isPublic?: boolean; - isFNDashboard?: boolean; controlsContainer?: string | null; - hiddenVariables?: string[]; fnLoader?: ReactNode; }; @@ -141,9 +144,11 @@ export class UnthemedDashboardPage extends PureComponent { componentDidMount() { this.initDashboard(); - const { isPublic, isFNDashboard } = this.props; - if (!isPublic && !isFNDashboard) { - this.forceRouteReloadCounter = (this.props.history.location.state as any)?.routeReloadCounter || 0; + + const { isPublic, FNDashboard } = this.props; + + if (!isPublic && !FNDashboard) { + this.forceRouteReloadCounter = (this.props.history.location?.state as any)?.routeReloadCounter || 0; } } @@ -157,7 +162,7 @@ export class UnthemedDashboardPage extends PureComponent { } initDashboard() { - const { dashboard, isPublic, match, queryParams, isFNDashboard } = this.props; + const { dashboard, isPublic, match, queryParams, FNDashboard } = this.props; if (dashboard) { this.closeDashboard(); @@ -170,7 +175,7 @@ export class UnthemedDashboardPage extends PureComponent { urlFolderId: queryParams.folderId, panelType: queryParams.panelType, routeName: this.props.route.routeName, - fixUrl: !isPublic && !isFNDashboard, + fixUrl: !isPublic && !FNDashboard, accessToken: match.params.accessToken, keybindingSrv: this.context.keybindings, }); @@ -180,13 +185,13 @@ export class UnthemedDashboardPage extends PureComponent { } componentDidUpdate(prevProps: Props, prevState: State) { - const { dashboard, match, templateVarsChangedInUrl, isPublic, isFNDashboard } = this.props; + const { dashboard, match, templateVarsChangedInUrl, isPublic, FNDashboard } = this.props; if (!dashboard) { return; } - if (!isPublic && !isFNDashboard) { + if (!isPublic && !FNDashboard) { const routeReloadCounter = (this.props.history.location?.state as any)?.routeReloadCounter; if ( @@ -372,11 +377,11 @@ export class UnthemedDashboardPage extends PureComponent { } render() { - const { dashboard, initError, queryParams, isPublic, isFNDashboard, fnLoader } = this.props; + const { dashboard, initError, queryParams, isPublic, FNDashboard, fnLoader } = this.props; const { editPanel, viewPanel, updateScrollTop, pageNav, sectionNav } = this.state; - const kioskMode = isFNDashboard ? KioskMode.FN : !isPublic ? getKioskMode(this.props.queryParams) : KioskMode.Full; + const kioskMode = FNDashboard ? KioskMode.FN : !isPublic ? getKioskMode(this.props.queryParams) : KioskMode.Full; - if (!dashboard ) { + if (!dashboard) { return fnLoader ? <>{fnLoader} : ; } @@ -387,7 +392,7 @@ export class UnthemedDashboardPage extends PureComponent {
{ scrollRef={this.setScrollRef} scrollTop={updateScrollTop} > - {!isFNDashboard && } + {!FNDashboard && } {initError && } {showSubMenu && (
- +
)} @@ -465,9 +465,9 @@ export class UnthemedDashboardPage extends PureComponent { } function updateStatePageNavFromProps(props: Props, state: State): State { - const { dashboard, isFNDashboard } = props; + const { dashboard, FNDashboard } = props; - if (!dashboard || isFNDashboard) { + if (!dashboard || FNDashboard) { return state; } diff --git a/public/app/fn-app/create-mfe.ts b/public/app/fn-app/create-mfe.ts index 8fcc18f3cece9..09c1084dba386 100644 --- a/public/app/fn-app/create-mfe.ts +++ b/public/app/fn-app/create-mfe.ts @@ -4,24 +4,30 @@ declare let __webpack_public_path__: string; window.__grafana_public_path__ = __webpack_public_path__.substring(0, __webpack_public_path__.lastIndexOf('build/')) || __webpack_public_path__; -import { isNull, merge, noop } from 'lodash'; +import { isNull, merge, noop, omit, pick } from 'lodash'; import React, { ComponentType } from 'react'; import ReactDOM from 'react-dom'; import { createTheme, GrafanaThemeType } from '@grafana/data'; +import { createFnColors } from '@grafana/data/src/themes/fnCreateColors'; +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 appEvents from 'app/core/app_events'; import config from 'app/core/config'; -import { updateFNGlobalState } from 'app/core/reducers/fn-slice'; +import { + FnGlobalState, + updatePartialFnStates, + updateFnState, + INITIAL_FN_STATE, + FnPropMappedFromState, + 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 { store } from 'app/store/store'; - -import { createFnColors } from '../../../packages/grafana-data/src/themes/fnCreateColors'; -import { GrafanaTheme2 } from '../../../packages/grafana-data/src/themes/types'; -import { GrafanaBootConfig } from '../../../packages/grafana-runtime/src/config'; +import { dispatch } from 'app/store/store'; import { FNDashboardProps, FailedToMountGrafanaErrorName } from './types'; @@ -30,22 +36,22 @@ import { FNDashboardProps, FailedToMountGrafanaErrorName } from './types'; * Qiankun expects Promise. Otherwise warnings are logged and life cycle hooks do not work */ /* eslint-disable-next-line */ -export declare type LifeCycleFn = (app: any, global: typeof window) => Promise; +export declare type LifeCycleFn = (app: any, global: typeof window) => Promise; /** * NOTE: single-spa and qiankun lifeCycles */ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -export declare type FrameworkLifeCycles = { - beforeLoad: LifeCycleFn | Array>; - beforeMount: LifeCycleFn | Array>; - afterMount: LifeCycleFn | Array>; - beforeUnmount: LifeCycleFn | Array>; - afterUnmount: LifeCycleFn | Array>; - bootstrap: LifeCycleFn | Array>; - mount: LifeCycleFn | Array>; - unmount: LifeCycleFn | Array>; - update: LifeCycleFn | Array>; +export declare type FrameworkLifeCycles = { + beforeLoad: LifeCycleFn | LifeCycleFn[]; + beforeMount: LifeCycleFn | LifeCycleFn[]; + afterMount: LifeCycleFn | LifeCycleFn[]; + beforeUnmount: LifeCycleFn | LifeCycleFn[]; + afterUnmount: LifeCycleFn | LifeCycleFn[]; + bootstrap: LifeCycleFn | LifeCycleFn[]; + mount: LifeCycleFn | LifeCycleFn[]; + unmount: LifeCycleFn | LifeCycleFn[]; + update: LifeCycleFn | LifeCycleFn[]; }; type DeepPartial = { @@ -58,7 +64,7 @@ class createMfe { private static readonly logPrefix = '[FN Grafana]'; mode: FNDashboardProps['mode']; - static Component: ComponentType; + static Component: ComponentType>; constructor(readonly props: FNDashboardProps) { this.mode = props.mode; } @@ -66,7 +72,7 @@ class createMfe { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ private static logger = (...args: any[]) => console.log(createMfe.logPrefix, ...args); - static getLifeCycles(component: ComponentType) { + static getLifeCycles(component: ComponentType>) { const lifeCycles: FrameworkLifeCycles = { bootstrap: this.boot(), mount: this.mountFnApp(component), @@ -82,7 +88,7 @@ class createMfe { return lifeCycles; } - static create(component: ComponentType) { + static create(component: ComponentType>) { return createMfe.getLifeCycles(component); } @@ -193,7 +199,7 @@ class createMfe { return parentElement.querySelector(createMfe.containerSelector); } - static mountFnApp(Component: ComponentType) { + static mountFnApp(Component: ComponentType>) { const lifeCycleFn: FrameworkLifeCycles['mount'] = (props: FNDashboardProps) => { createMfe.logger('Trying to mount grafana...'); @@ -202,6 +208,16 @@ class createMfe { createMfe.loadFnTheme(props.mode); createMfe.Component = Component; + const initialState: FnGlobalState = { + ...INITIAL_FN_STATE, + FNDashboard: true, + ...pick(props, ...fnStateProps), + }; + + FnLoggerService.log(null, '[FN Grafana] Dispatching initial state.', { initialState }); + + dispatch(updateFnState(initialState)); + createMfe.renderMfeComponent(props, () => { createMfe.logger('Mounted grafana.', { props }); @@ -256,7 +272,11 @@ class createMfe { * We do not use the "mode" state right now, * but I believe that as long as we store the "mode, we should update it */ - updateFNGlobalState('mode', mode); + dispatch( + updatePartialFnStates({ + mode, + }) + ); /** * NOTE: * Here happens the theme change. @@ -271,7 +291,11 @@ class createMfe { if (hiddenVariables) { createMfe.logger('Trying to update grafana with hidden variables.', { hiddenVariables }); - updateFNGlobalState('hiddenVariables', hiddenVariables); + dispatch( + updatePartialFnStates({ + hiddenVariables, + }) + ); } // NOTE: The false/true value does not change anything @@ -281,21 +305,17 @@ class createMfe { return lifeCycleFn; } - static renderMfeComponent(props: Partial, onSuccess = noop) { - const { fnGlobalState } = store.getState(); + static renderMfeComponent(props: FNDashboardProps, onSuccess = noop) { + const container = createMfe.getContainer(props); - /** - * NOTE: - * It may happen that only partial props are received in arguments. - * Then we should keep the current props that are read them from the state. - */ - const mergedProps = merge({}, fnGlobalState, props) as FNDashboardProps; - const container = createMfe.getContainer(mergedProps); - - ReactDOM.render(React.createElement(createMfe.Component, mergedProps), container, () => { - createMfe.logger('Rendered mfe component.', { mergedProps, container }); - onSuccess(); - }); + ReactDOM.render( + React.createElement(createMfe.Component, omit(props, 'hiddenVariables', 'FNDashboard')), + container, + () => { + createMfe.logger('Created mfe component.', { props, container }); + onSuccess(); + } + ); } } 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 a001bc33db882..e861fe9ae1545 100644 --- a/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx +++ b/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx @@ -1,8 +1,18 @@ +import { pick } from 'lodash'; import { parse as parseQueryParams } from 'query-string'; import React, { FC, Suspense, useMemo } from 'react'; import { lazily } from 'react-lazily'; +import { connect, MapStateToProps } from 'react-redux'; import { useLocation } from 'react-router-dom'; +import { + FnGlobalState, + FN_STATE_KEY, + FnPropMappedFromState, + fnPropsMappedFromState, + FnPropsMappedFromState, +} from 'app/core/reducers/fn-slice'; + import { FNDashboardProps } from '../types'; import { RenderPortal } from '../utils'; @@ -10,39 +20,61 @@ const { RenderFNDashboard } = lazily(() => import('./render-fn-dashboard')); const { FnAppProvider } = lazily(() => import('../fn-app-provider')); const { AngularRoot } = lazily(() => import('../../angular/AngularRoot')); -export const FNDashboard: FC = (props) => ( - {props.fnLoader}}> - -
- - -
-
-
-); - -export const DashboardPortal: FC = (props) =>{ +type FNDashboardComponentProps = Omit; + +export const FNDashboard: FC = (props) => { + return ( + {props.fnLoader}}> + +
+ + +
+
+
+ ); +}; + +function mapStateToProps(): MapStateToProps< + FnPropsMappedFromState, + Omit, + { [K in typeof FN_STATE_KEY]: FnGlobalState } +> { + return ({ fnGlobalState }) => pick(fnGlobalState, ...fnPropsMappedFromState); +} + +export const DashboardPortalComponent: FC = (props) => { const location = useLocation(); - const portal = useMemo(() =>{ + const content = useMemo(() => { + if (!props.FNDashboard) { + // TODO Use no data + return null; + } + const { search } = location; const queryParams = parseQueryParams(search); - const { dashboardUID, slug } = queryParams + const { dashboardUID: uid, slug } = queryParams as { dashboardUID?: string; slug?: string }; - const newProps: FNDashboardProps = { - ...props, - uid: dashboardUID as string, - slug: slug as string, - queryParams, + if (!uid || !slug) { + // TODO Use no data + return null; } - - return dashboardUID &&( - - - - ) - },[location, props]) - - return <>{portal} -} + + return ( + + ); + }, [location, props]); + + return {content}; +}; + +export const DashboardPortal = connect(mapStateToProps())(DashboardPortalComponent); 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 8695f9ad54782..739b698e741c3 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,27 +1,22 @@ import { merge, isEmpty, isFunction } from 'lodash'; import React, { useEffect, FC, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; import { locationService as locationSrv, HistoryWrapper } from '@grafana/runtime'; -import { FnGlobalState, setInitialMountState } from 'app/core/reducers/fn-slice'; import DashboardPage, { DashboardPageProps } from 'app/features/dashboard/containers/DashboardPage'; -import { FnLoggerService } from 'app/fn_logger'; import { DashboardRoutes, StoreState, useSelector } from 'app/types'; import { FNDashboardProps } from '../types'; const locationService = locationSrv as HistoryWrapper; -const DEFAULT_DASHBOARD_PAGE_PROPS: Pick & { +const DEFAULT_DASHBOARD_PAGE_PROPS: Pick & { match: Pick; } = { - isFNDashboard: true, match: { isExact: true, path: '/d/:uid/:slug?', url: '', }, - history: {} as DashboardPageProps['history'], route: { routeName: DashboardRoutes.Normal, @@ -32,9 +27,7 @@ const DEFAULT_DASHBOARD_PAGE_PROPS: Pick = (props) => { - const { queryParams, uid, slug, mode, controlsContainer, pageTitle = '', setErrors, fnLoader } = props; - - const dispatch = useDispatch(); + const { queryParams, controlsContainer, setErrors, fnLoader, hiddenVariables } = props; const firstError = useSelector((state: StoreState) => { const { appNotifications } = state; @@ -42,8 +35,6 @@ export const RenderFNDashboard: FC = (props) => { return Object.values(appNotifications.byId).find(({ severity }) => severity === 'error'); }); - const hiddenVariables = useSelector(({ fnGlobalState: { hiddenVariables } }: StoreState) => hiddenVariables); - /** * NOTE: * Grafana renders notifications in StoredNotifications component. @@ -59,32 +50,8 @@ export const RenderFNDashboard: FC = (props) => { }, [firstError, setErrors]); useEffect(() => { - const initialState: FnGlobalState = { - FNDashboard: true, - uid, - slug, - mode, - controlsContainer, - pageTitle, - queryParams, - hiddenVariables, - }; - - FnLoggerService.log(null, '[FN Grafana] Trying to set initial state.', { initialState }); - - dispatch(setInitialMountState(initialState)); - - // TODO: catch success in redux-thunk way - FnLoggerService.log( - null, - '[FN Grafana] Successfully set initial state.', - locationService.getLocation(), - locationService.getHistory, - 'location params' - ); - locationService.fnPathnameChange(window.location.pathname, queryParams); - }, [dispatch, uid, slug, controlsContainer, pageTitle, queryParams, mode, hiddenVariables]); + }, [queryParams]); const dashboardPageProps: DashboardPageProps = useMemo( () => diff --git a/public/app/fn-app/types.ts b/public/app/fn-app/types.ts index d3b2ee934712a..37e1143ff4492 100644 --- a/public/app/fn-app/types.ts +++ b/public/app/fn-app/types.ts @@ -30,6 +30,6 @@ export interface FNDashboardProps { controlsContainer: string; isLoading: (isLoading: boolean) => void; setErrors: (errors?: { [K: number | string]: string }) => void; - hiddenVariables: string[]; + hiddenVariables: readonly string[]; container?: HTMLElement | null; }