diff --git a/dashboards/src/components/GridLayout/GridItemContent.tsx b/dashboards/src/components/GridLayout/GridItemContent.tsx index 49af090e..b45db86c 100644 --- a/dashboards/src/components/GridLayout/GridItemContent.tsx +++ b/dashboards/src/components/GridLayout/GridItemContent.tsx @@ -99,14 +99,6 @@ export function GridItemContent(props: GridItemContentProps): ReactElement { const { data: plugin } = usePlugin('Panel', panelDefinition.spec.plugin.kind); - const queryDefinitions = queries ?? []; - const definitions = queryDefinitions.map((query) => { - return { - kind: query.spec.plugin.kind, - spec: query.spec.plugin.spec, - }; - }); - const pluginQueryOptions = typeof plugin?.queryOptions === 'function' ? plugin?.queryOptions(panelDefinition.spec.plugin.spec) @@ -125,7 +117,7 @@ export function GridItemContent(props: GridItemContentProps): ReactElement { }} > @@ -142,7 +134,7 @@ export function GridItemContent(props: GridItemContentProps): ReactElement { setOpenQueryViewer(false)} /> diff --git a/dashboards/src/components/PanelDrawer/PanelQueriesSharedControls.tsx b/dashboards/src/components/PanelDrawer/PanelQueriesSharedControls.tsx index fc28bc4e..6bc11578 100644 --- a/dashboards/src/components/PanelDrawer/PanelQueriesSharedControls.tsx +++ b/dashboards/src/components/PanelDrawer/PanelQueriesSharedControls.tsx @@ -51,15 +51,7 @@ export function PanelQueriesSharedControls({ [panelDefinition.spec.plugin.spec, pluginPreview] ); - const [previewDefinition, setPreviewDefinition] = useState( - () => - panelDefinition.spec.queries?.map((query) => { - return { - kind: query.spec.plugin.kind, - spec: query.spec.plugin.spec, - }; - }) ?? [] - ); + const [previewDefinition, setPreviewDefinition] = useState(panelDefinition.spec.queries ?? []); const handleOnQueriesChange = useCallback( (queries: QueryDefinition[]) => { @@ -67,14 +59,7 @@ export function PanelQueriesSharedControls({ // If the number of queries has changed, force preview definition update to remove results of deleted queries. if (queries.length !== previewDefinition.length) { - setPreviewDefinition( - queries.map((query) => { - return { - kind: query.spec.plugin.kind, - spec: query.spec.plugin.spec, - }; - }) - ); + setPreviewDefinition(queries); } }, [onQueriesChange, previewDefinition.length] @@ -83,10 +68,7 @@ export function PanelQueriesSharedControls({ const handleRunQuery = useCallback((index: number, newDef: QueryDefinition) => { setPreviewDefinition((prev) => { const newDefinitions = [...prev]; - newDefinitions[index] = { - kind: newDef.spec.plugin.kind, - spec: newDef.spec.plugin.spec, - }; + newDefinitions[index] = newDef; return newDefinitions; }); }, []); diff --git a/plugin-system/src/components/MultiQueryEditor/QueryEditorContainer.tsx b/plugin-system/src/components/MultiQueryEditor/QueryEditorContainer.tsx index 1c0a8ce2..b14b847c 100644 --- a/plugin-system/src/components/MultiQueryEditor/QueryEditorContainer.tsx +++ b/plugin-system/src/components/MultiQueryEditor/QueryEditorContainer.tsx @@ -13,15 +13,28 @@ import { produce } from 'immer'; import { QueryDefinition, QueryPluginType } from '@perses-dev/spec'; -import { Stack, IconButton, Typography, BoxProps, Box, CircularProgress } from '@mui/material'; +import { + Stack, + IconButton, + Typography, + BoxProps, + Box, + CircularProgress, + TextField, + Button, + InputAdornment, +} from '@mui/material'; import DeleteIcon from 'mdi-material-ui/DeleteOutline'; import ChevronDown from 'mdi-material-ui/ChevronDown'; import ChevronRight from 'mdi-material-ui/ChevronRight'; -import { forwardRef, ReactElement } from 'react'; +import { forwardRef, ReactElement, useState } from 'react'; import AlertIcon from 'mdi-material-ui/Alert'; import { InfoTooltip } from '@perses-dev/components'; +import PencilIcon from 'mdi-material-ui/Pencil'; +import CheckIcon from 'mdi-material-ui/Check'; import { QueryData } from '../../runtime'; import { PluginEditor, PluginEditorProps, PluginEditorRef } from '../PluginEditor'; +import { defaultQueryName } from './utils'; /** * Properties for {@link QueryEditorContainer} @@ -66,6 +79,20 @@ export const QueryEditorContainer = forwardRef { + draft.spec.name = name; + }) + ); + } + return ( theme.palette.divider} > - - onCollapseExpand(index)}> + + onCollapseExpand(index)} + > {isCollapsed ? : } - - Query #{index + 1} - + + {isEditingName ? ( + setName(e.target.value)} + InputProps={{ + endAdornment: ( + + + + + + ), + }} + /> + ) : ( + <> + + {name} + + + + )} + {queryResult?.isFetching && } diff --git a/plugin-system/src/components/MultiQueryEditor/index.tsx b/plugin-system/src/components/MultiQueryEditor/index.tsx index 1e2c4cd4..7390db87 100644 --- a/plugin-system/src/components/MultiQueryEditor/index.tsx +++ b/plugin-system/src/components/MultiQueryEditor/index.tsx @@ -12,3 +12,4 @@ // limitations under the License. export * from './MultiQueryEditor'; +export * from './utils'; diff --git a/plugin-system/src/components/MultiQueryEditor/utils.tsx b/plugin-system/src/components/MultiQueryEditor/utils.tsx new file mode 100644 index 00000000..ce184fe3 --- /dev/null +++ b/plugin-system/src/components/MultiQueryEditor/utils.tsx @@ -0,0 +1,30 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { QueryDefinition } from '@perses-dev/spec'; + +export function defaultQueryName(index: number): string { + return `Query #${index + 1}`; +} + +export function getQueryName(definitions: QueryDefinition[], query: QueryDefinition): string { + if (query.spec.name) { + return query.spec.name; + } + const index = definitions.findIndex((definition) => definition === query); + return defaultQueryName(index); +} + +export function generateQueryNames(definitions: QueryDefinition[]): string[] { + return definitions.map((queryDef: QueryDefinition, index: number) => queryDef.spec.name ?? defaultQueryName(index)); +} diff --git a/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.test.tsx b/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.test.tsx index a9c1f7db..3ff23401 100644 --- a/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.test.tsx +++ b/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.test.tsx @@ -23,6 +23,7 @@ describe('PanelSpecEditor', () => { const renderComponent = (props: Omit): void => { const DataQueriesProviderMock = (childProps: { children: ReactElement }): ReactElement => { const ctx = { + queryDefinitions: [], queryResults: [], refetchAll: (): void => {}, isFetching: false, diff --git a/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.tsx b/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.tsx index 6129c591..b77a1f7d 100644 --- a/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.tsx +++ b/plugin-system/src/components/PanelSpecEditor/PanelSpecEditor.tsx @@ -15,7 +15,7 @@ import { ErrorAlert, JSONEditor, LinksEditor } from '@perses-dev/components'; import { PanelDefinition, PanelEditorValues, QueryDefinition, UnknownSpec } from '@perses-dev/spec'; import { Control, Controller } from 'react-hook-form'; import { forwardRef, ReactElement } from 'react'; -import { QueryCountProvider, useDataQueriesContext, usePlugin } from '../../runtime'; +import { useDataQueriesContext, usePlugin } from '../../runtime'; import { PanelPlugin } from '../../model'; import { OptionsEditorTabsProps, OptionsEditorTabs } from '../OptionsEditorTabs'; import { MultiQueryEditor } from '../MultiQueryEditor'; @@ -131,11 +131,7 @@ export const PanelSpecEditor = forwardRef ), }); - return ( - - - - ); + return ; }); PanelSpecEditor.displayName = 'PanelSpecEditor'; diff --git a/plugin-system/src/runtime/DataQueriesProvider/DataQueriesProvider.test.tsx b/plugin-system/src/runtime/DataQueriesProvider/DataQueriesProvider.test.tsx index 5e01a21b..fa5bf474 100644 --- a/plugin-system/src/runtime/DataQueriesProvider/DataQueriesProvider.test.tsx +++ b/plugin-system/src/runtime/DataQueriesProvider/DataQueriesProvider.test.tsx @@ -13,6 +13,7 @@ import React, { ReactElement } from 'react'; import { renderHook } from '@testing-library/react'; +import { QueryDefinition } from '@perses-dev/spec'; import { MOCK_TIME_SERIES_DATA, MOCK_TRACE_DATA, @@ -21,9 +22,7 @@ import { MOCK_ALERTS_DATA, MOCK_SILENCES_DATA, } from '../../test'; -import { useListPluginMetadata } from '../plugin-registry'; import { DataQueriesProvider, useDataQueries } from './DataQueriesProvider'; -import { useQueryType } from './model'; jest.mock('../time-series-queries', () => ({ useTimeSeriesQueries: jest.fn().mockImplementation(() => [{ data: MOCK_TIME_SERIES_DATA }]), @@ -95,11 +94,16 @@ jest.mock('../plugin-registry', () => ({ describe('useDataQueries', (): void => { it('should return the correct data for TimeSeriesQuery', () => { - const definitions = [ + const definitions: QueryDefinition[] = [ { - kind: 'PrometheusTimeSeriesQuery', + kind: 'TimeSeriesQuery', spec: { - query: 'up', + plugin: { + kind: 'PrometheusTimeSeriesQuery', + spec: { + query: 'up', + }, + }, }, }, ]; @@ -115,11 +119,16 @@ describe('useDataQueries', (): void => { }); it('should return the correct data for TraceQuery', () => { - const definitions = [ + const definitions: QueryDefinition[] = [ { - kind: 'TempoTraceQuery', + kind: 'TraceQuery', spec: { - query: '{ duration > 1000ms }', + plugin: { + kind: 'TempoTraceQuery', + spec: { + query: '{ duration > 1000ms }', + }, + }, }, }, ]; @@ -135,10 +144,15 @@ describe('useDataQueries', (): void => { }); it('should return the correct data for AlertsQuery', () => { - const definitions = [ + const definitions: QueryDefinition[] = [ { - kind: 'AlertmanagerAlertsQuery', - spec: {}, + kind: 'AlertsQuery', + spec: { + plugin: { + kind: 'AlertmanagerAlertsQuery', + spec: {}, + }, + }, }, ]; @@ -153,10 +167,15 @@ describe('useDataQueries', (): void => { }); it('should return the correct data for SilencesQuery', () => { - const definitions = [ + const definitions: QueryDefinition[] = [ { - kind: 'AlertmanagerSilencesQuery', - spec: {}, + kind: 'SilencesQuery', + spec: { + plugin: { + kind: 'AlertmanagerSilencesQuery', + spec: {}, + }, + }, }, ]; @@ -170,32 +189,3 @@ describe('useDataQueries', (): void => { expect(result.current.queryResults[0]?.data).toEqual(MOCK_SILENCES_DATA); }); }); - -describe('useQueryType', () => { - it('should return the correct query type for a given plugin kind', () => { - const { result } = renderHook(() => useQueryType()); - - const getQueryType = result.current; - expect(getQueryType('PrometheusTimeSeriesQuery')).toBe('TimeSeriesQuery'); - expect(getQueryType('TempoTraceQuery')).toBe('TraceQuery'); - expect(getQueryType('AlertmanagerAlertsQuery')).toBe('AlertsQuery'); - expect(getQueryType('AlertmanagerSilencesQuery')).toBe('SilencesQuery'); - }); - - it('should throw an error if query type is not found ', () => { - const { result } = renderHook(() => useQueryType()); - - const getQueryType = result.current; - expect(() => getQueryType('UnknownQuery')).toThrow(`Unable to determine the query type: UnknownQuery`); - }); - - it('should return undefined if useListPluginMetadata is still loading', () => { - (useListPluginMetadata as jest.Mock).mockReturnValue({ isLoading: true }); - const { result } = renderHook(() => useQueryType()); - - const getQueryType = result.current; - expect(getQueryType('PrometheusTimeSeriesQuery')).toBeUndefined(); - expect(getQueryType('TempoTraceQuery')).toBeUndefined(); - expect(getQueryType('UnknownQuery')).toBeUndefined(); - }); -}); diff --git a/plugin-system/src/runtime/DataQueriesProvider/DataQueriesProvider.tsx b/plugin-system/src/runtime/DataQueriesProvider/DataQueriesProvider.tsx index 5a05a3d0..1557b687 100644 --- a/plugin-system/src/runtime/DataQueriesProvider/DataQueriesProvider.tsx +++ b/plugin-system/src/runtime/DataQueriesProvider/DataQueriesProvider.tsx @@ -27,7 +27,6 @@ import { transformQueryResults, DataQueriesContextType, QueryData, - useQueryType, } from './model'; export const DataQueriesContext = createContext(undefined); @@ -43,6 +42,9 @@ export function useDataQueriesContext(): DataQueriesContextType { export function useDataQueries(queryType: T): UseDataQueryResults { const ctx = useDataQueriesContext(); + // Filter query definitions based on the specified query type + const filteredQueryDefinitions = ctx.queryDefinitions.filter((definition) => definition.kind === queryType); + // Filter the query results based on the specified query type const filteredQueryResults = ctx.queryResults.filter( (queryResult) => queryResult?.definition?.kind === queryType @@ -52,60 +54,44 @@ export function useDataQueries(queryType: T): UseData const filteredErrors = ctx.errors.filter((errors, index) => ctx.queryResults[index]?.definition?.kind === queryType); // Create a new context object with the filtered results and errors - const filteredCtx = { + return { + queryDefinitions: filteredQueryDefinitions, queryResults: filteredQueryResults, isFetching: filteredQueryResults.some((result) => result.isFetching), isLoading: filteredQueryResults.some((result) => result.isLoading), refetchAll: ctx.refetchAll, errors: filteredErrors, }; - - return filteredCtx; } export function DataQueriesProvider(props: DataQueriesProviderProps): ReactElement { const { definitions, options, children, queryOptions } = props; - // Returns a query kind, for example "TimeSeriesQuery" = getQueryType("PrometheusTimeSeriesQuery") - const getQueryType = useQueryType(); - - const queryDefinitions = definitions.map((definition) => { - const type = getQueryType(definition.kind); - return { - kind: type, - spec: { - plugin: definition, - }, - }; - }); - const usageMetrics = useUsageMetrics(); // Filter definitions for time series query and other future query plugins - const timeSeriesQueries = queryDefinitions.filter( + const timeSeriesQueries = definitions.filter( (definition) => definition.kind === 'TimeSeriesQuery' ) as TimeSeriesQueryDefinition[]; const timeSeriesResults = useTimeSeriesQueries(timeSeriesQueries, options, queryOptions); - const traceQueries = queryDefinitions.filter( - (definition) => definition.kind === 'TraceQuery' - ) as TraceQueryDefinition[]; + const traceQueries = definitions.filter((definition) => definition.kind === 'TraceQuery') as TraceQueryDefinition[]; const traceResults = useTraceQueries(traceQueries); - const profileQueries = queryDefinitions.filter( + const profileQueries = definitions.filter( (definition) => definition.kind === 'ProfileQuery' ) as ProfileQueryDefinition[]; const profileResults = useProfileQueries(profileQueries); - const logQueries = queryDefinitions.filter((definition) => definition.kind === 'LogQuery') as LogQueryDefinition[]; + const logQueries = definitions.filter((definition) => definition.kind === 'LogQuery') as LogQueryDefinition[]; const logResults = useLogQueries(logQueries); - const alertsQueries = queryDefinitions.filter( + const alertsQueries = definitions.filter( (definition) => definition.kind === 'AlertsQuery' ) as AlertsQueryDefinition[]; const alertsResults = useAlertsQueries(alertsQueries); - const silencesQueries = queryDefinitions.filter( + const silencesQueries = definitions.filter( (definition) => definition.kind === 'SilencesQuery' ) as SilencesQueryDefinition[]; const silencesResults = useSilencesQueries(silencesQueries); @@ -142,6 +128,7 @@ export function DataQueriesProvider(props: DataQueriesProviderProps): ReactEleme } return { + queryDefinitions: definitions, queryResults: mergedQueryResults, isFetching: mergedQueryResults.some((result) => result.isFetching), isLoading: mergedQueryResults.some((result) => result.isLoading), @@ -149,20 +136,21 @@ export function DataQueriesProvider(props: DataQueriesProviderProps): ReactEleme errors: mergedQueryResults.map((result) => result.error), }; }, [ - timeSeriesQueries, - timeSeriesResults, - traceQueries, - traceResults, - profileQueries, - profileResults, - logQueries, - logResults, alertsQueries, alertsResults, + logQueries, + logResults, + profileQueries, + profileResults, silencesQueries, silencesResults, - refetchAll, + timeSeriesQueries, + timeSeriesResults, + traceQueries, + traceResults, + definitions, queryOptions?.enabled, + refetchAll, usageMetrics, ]); diff --git a/plugin-system/src/runtime/DataQueriesProvider/model.ts b/plugin-system/src/runtime/DataQueriesProvider/model.ts index 32136e6d..57f3ff6e 100644 --- a/plugin-system/src/runtime/DataQueriesProvider/model.ts +++ b/plugin-system/src/runtime/DataQueriesProvider/model.ts @@ -11,20 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Definition, QueryDefinition, UnknownSpec, QueryDataType } from '@perses-dev/spec'; +import { QueryDefinition, UnknownSpec, QueryDataType } from '@perses-dev/spec'; import { QueryObserverOptions, UseQueryResult } from '@tanstack/react-query'; -import { ReactNode, useCallback, useMemo } from 'react'; -import { useListPluginMetadata } from '../plugin-registry'; +import { ReactNode } from 'react'; export type QueryOptions = Record; -export interface DataQueriesProviderProps { - definitions: Array>; +export interface DataQueriesProviderProps { + definitions: Array>; children?: ReactNode; options?: QueryOptions; queryOptions?: Omit; } export interface DataQueriesContextType { + queryDefinitions: QueryDefinition[]; queryResults: QueryData[]; refetchAll: () => void; isFetching: boolean; @@ -57,126 +57,3 @@ export function transformQueryResults(results: UseQueryResult[], definitions: Qu } as QueryData; }); } - -export function useQueryType(): (pluginKind: string) => string | undefined { - const { data: timeSeriesQueryPlugins, isLoading: isTimeSeriesQueryLoading } = useListPluginMetadata([ - 'TimeSeriesQuery', - ]); - const { data: traceQueryPlugins, isLoading: isTraceQueryPluginLoading } = useListPluginMetadata(['TraceQuery']); - const { data: profileQueryPlugins, isLoading: isProfileQueryPluginLoading } = useListPluginMetadata(['ProfileQuery']); - const { data: logQueries, isLoading: isLogQueryPluginLoading } = useListPluginMetadata(['LogQuery']); - const { data: alertsQueryPlugins, isLoading: isAlertsQueryPluginLoading } = useListPluginMetadata(['AlertsQuery']); - const { data: silencesQueryPlugins, isLoading: isSilencesQueryPluginLoading } = useListPluginMetadata([ - 'SilencesQuery', - ]); - - // For example, `map: {"TimeSeriesQuery":["PrometheusTimeSeriesQuery"],"TraceQuery":["TempoTraceQuery"]}` - const queryTypeMap = useMemo(() => { - const map: Record = { - TimeSeriesQuery: [], - TraceQuery: [], - ProfileQuery: [], - LogQuery: [], - AlertsQuery: [], - SilencesQuery: [], - }; - - if (timeSeriesQueryPlugins) { - timeSeriesQueryPlugins.forEach((plugin) => { - map[plugin.kind]?.push(plugin.spec.name); - }); - } - - if (traceQueryPlugins) { - traceQueryPlugins.forEach((plugin) => { - map[plugin.kind]?.push(plugin.spec.name); - }); - } - - if (profileQueryPlugins) { - profileQueryPlugins.forEach((plugin) => { - map[plugin.kind]?.push(plugin.spec.name); - }); - } - - if (logQueries) { - logQueries.forEach((plugin) => { - map[plugin.kind]?.push(plugin.spec.name); - }); - } - - if (alertsQueryPlugins) { - alertsQueryPlugins.forEach((plugin) => { - map[plugin.kind]?.push(plugin.spec.name); - }); - } - - if (silencesQueryPlugins) { - silencesQueryPlugins.forEach((plugin) => { - map[plugin.kind]?.push(plugin.spec.name); - }); - } - - return map; - }, [ - timeSeriesQueryPlugins, - traceQueryPlugins, - profileQueryPlugins, - logQueries, - alertsQueryPlugins, - silencesQueryPlugins, - ]); - - const getQueryType = useCallback( - (pluginKind: string) => { - const isLoading = (pluginKind: string): boolean => { - switch (pluginKind) { - case 'PrometheusTimeSeriesQuery': - return isTimeSeriesQueryLoading; - case 'TempoTraceQuery': - return isTraceQueryPluginLoading; - case 'PyroscopeProfileQuery': - return isProfileQueryPluginLoading; - case 'LokiLogQuery': - return isLogQueryPluginLoading; - case 'AlertmanagerAlertsQuery': - return isAlertsQueryPluginLoading; - case 'AlertmanagerSilencesQuery': - return isSilencesQueryPluginLoading; - } - - return ( - isTraceQueryPluginLoading || - isTimeSeriesQueryLoading || - isProfileQueryPluginLoading || - isLogQueryPluginLoading || - isAlertsQueryPluginLoading || - isSilencesQueryPluginLoading - ); - }; - - if (isLoading(pluginKind)) { - return undefined; - } - - for (const queryType in queryTypeMap) { - if (queryTypeMap[queryType]?.includes(pluginKind)) { - return queryType; - } - } - - throw new Error(`Unable to determine the query type: ${pluginKind}`); - }, - [ - queryTypeMap, - isTimeSeriesQueryLoading, - isTraceQueryPluginLoading, - isProfileQueryPluginLoading, - isLogQueryPluginLoading, - isAlertsQueryPluginLoading, - isSilencesQueryPluginLoading, - ] - ); - - return getQueryType; -} diff --git a/plugin-system/src/runtime/QueryCountProvider.tsx b/plugin-system/src/runtime/QueryCountProvider.tsx deleted file mode 100644 index 77691521..00000000 --- a/plugin-system/src/runtime/QueryCountProvider.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React, { createContext, useContext, ReactNode } from 'react'; - -interface QueryCountProviderProps { - queryCount: number; - children: ReactNode; -} - -const QueryCountContext = createContext(0); - -export const QueryCountProvider: React.FC = ({ queryCount, children }) => { - return {children}; -}; - -export const useQueryCountContext = (): number => { - return useContext(QueryCountContext); -}; diff --git a/plugin-system/src/runtime/index.ts b/plugin-system/src/runtime/index.ts index 36d71278..9e1dda5e 100644 --- a/plugin-system/src/runtime/index.ts +++ b/plugin-system/src/runtime/index.ts @@ -24,6 +24,5 @@ export * from './alerts-queries'; export * from './silences-queries'; export * from './item-actions'; export * from './DataQueriesProvider'; -export * from './QueryCountProvider'; export * from './RouterProvider'; export * from './UsageMetricsProvider';