From 80f7384411082ad12110915968f6f593bc3689c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 5 Jun 2024 16:14:30 +0200 Subject: [PATCH] PlainReact: Expose scene features through contexts and hooks and normal react components (#734) Co-authored-by: Oscar Kilhed --- .github/workflows/node-ci.yml | 6 +- .gitignore | 3 + docusaurus/website/package.json | 2 + lerna.json | 2 +- package.json | 19 +- packages/scenes-app/package.json | 1 + .../src/components/Routes/Routes.tsx | 2 + packages/scenes-app/src/constants.ts | 1 + packages/scenes-app/src/demos/index.ts | 2 + packages/scenes-app/src/demos/interopDemo.tsx | 51 + packages/scenes-app/src/demos/utils.ts | 6 +- packages/scenes-app/src/plugin.json | 7 + .../src/react-demo/DrilldownDemoPage.tsx | 68 + .../src/react-demo/DynamicQueriesPage.tsx | 55 + .../src/react-demo/DynamicVariablesPage.tsx | 23 + .../react-demo/DynamicVisualizationPage.tsx | 49 + packages/scenes-app/src/react-demo/Home.tsx | 56 + .../src/react-demo/InterpolationHookPage.tsx | 53 + .../src/react-demo/NestedContextPage.tsx | 48 + .../scenes-app/src/react-demo/PageWrapper.tsx | 43 + .../react-demo/PlainGraphWithRandomWalk.tsx | 18 + .../src/react-demo/RepeatBySeriesPage.tsx | 70 + .../src/react-demo/RepeatByVariablePage.tsx | 37 + packages/scenes-app/src/react-demo/utils.tsx | 33 + .../src/react-demo/visualizations.tsx | 16 + packages/scenes-react/.eslintrc | 11 + packages/scenes-react/CHANGELOG.md | 1 + packages/scenes-react/README.md | 27 + packages/scenes-react/jest.config.ts | 29 + packages/scenes-react/package.json | 115 ++ packages/scenes-react/rollup.config.ts | 46 + packages/scenes-react/scripts/postpack.js | 10 + packages/scenes-react/scripts/prepack.js | 19 + .../scenes-react/src/DataProxyProvider.tsx | 47 + .../src/components/RefreshPicker.tsx | 47 + .../src/components/TimeRangePicker.tsx | 23 + .../src/components/VariableSelect.tsx | 18 + .../src/components/VizPanel.test.tsx | 49 + .../scenes-react/src/components/VizPanel.tsx | 102 ++ .../src/contexts/BreadcrumbContext.tsx | 113 ++ .../src/contexts/SceneContextObject.test.tsx | 16 + .../src/contexts/SceneContextObject.tsx | 82 ++ .../contexts/SceneContextProvider.test.tsx | 76 ++ .../src/contexts/SceneContextProvider.tsx | 111 ++ packages/scenes-react/src/hooks/hooks.ts | 105 ++ .../src/hooks/useQueryRunner.test.tsx | 102 ++ .../scenes-react/src/hooks/useQueryRunner.ts | 46 + .../src/hooks/useVariableValues.test.tsx | 40 + .../src/hooks/useVariableValues.ts | 27 + packages/scenes-react/src/index.ts | 10 + packages/scenes-react/src/types.ts | 3 + packages/scenes-react/src/utils.ts | 11 + packages/scenes-react/src/utils/testUtils.tsx | 67 + .../src/variables/CustomVariable.tsx | 47 + packages/scenes-react/tsconfig.build.json | 7 + packages/scenes-react/tsconfig.json | 12 + packages/scenes-react/utils/setupTests.ts | 28 + .../utils/test/__mocks__/pluginMocks.ts | 35 + packages/scenes/README.md | 2 + packages/scenes/package.json | 7 +- packages/scenes/rollup.config.ts | 5 +- .../src/components/VizPanel/VizPanel.tsx | 4 + .../core/PanelBuilders/VizConfigBuilder.ts | 144 +++ .../core/PanelBuilders/VizConfigBuilders.ts | 180 +++ .../scenes/src/core/PanelBuilders/types.ts | 9 +- packages/scenes/src/core/types.ts | 9 +- packages/scenes/src/index.ts | 7 +- .../src/querying/SceneQueryRunner.test.ts | 49 +- .../scenes/src/querying/SceneQueryRunner.ts | 9 +- .../src/services/UrlSyncManager.test.ts | 24 +- packages/scenes/src/utils/writeSceneLog.ts | 8 +- packages/scenes/src/variables/utils.ts | 6 +- .../variants/query/QueryVariable.tsx | 5 +- packages/scenes/tsconfig.json | 2 +- scripts/demo.sh | 12 +- turbo.json | 22 + yarn.lock | 1152 +++++------------ 77 files changed, 2876 insertions(+), 933 deletions(-) create mode 100644 packages/scenes-app/src/demos/interopDemo.tsx create mode 100644 packages/scenes-app/src/react-demo/DrilldownDemoPage.tsx create mode 100644 packages/scenes-app/src/react-demo/DynamicQueriesPage.tsx create mode 100644 packages/scenes-app/src/react-demo/DynamicVariablesPage.tsx create mode 100644 packages/scenes-app/src/react-demo/DynamicVisualizationPage.tsx create mode 100644 packages/scenes-app/src/react-demo/Home.tsx create mode 100644 packages/scenes-app/src/react-demo/InterpolationHookPage.tsx create mode 100644 packages/scenes-app/src/react-demo/NestedContextPage.tsx create mode 100644 packages/scenes-app/src/react-demo/PageWrapper.tsx create mode 100644 packages/scenes-app/src/react-demo/PlainGraphWithRandomWalk.tsx create mode 100644 packages/scenes-app/src/react-demo/RepeatBySeriesPage.tsx create mode 100644 packages/scenes-app/src/react-demo/RepeatByVariablePage.tsx create mode 100644 packages/scenes-app/src/react-demo/utils.tsx create mode 100644 packages/scenes-app/src/react-demo/visualizations.tsx create mode 100644 packages/scenes-react/.eslintrc create mode 100644 packages/scenes-react/CHANGELOG.md create mode 100644 packages/scenes-react/README.md create mode 100644 packages/scenes-react/jest.config.ts create mode 100644 packages/scenes-react/package.json create mode 100644 packages/scenes-react/rollup.config.ts create mode 100644 packages/scenes-react/scripts/postpack.js create mode 100644 packages/scenes-react/scripts/prepack.js create mode 100644 packages/scenes-react/src/DataProxyProvider.tsx create mode 100644 packages/scenes-react/src/components/RefreshPicker.tsx create mode 100644 packages/scenes-react/src/components/TimeRangePicker.tsx create mode 100644 packages/scenes-react/src/components/VariableSelect.tsx create mode 100644 packages/scenes-react/src/components/VizPanel.test.tsx create mode 100644 packages/scenes-react/src/components/VizPanel.tsx create mode 100644 packages/scenes-react/src/contexts/BreadcrumbContext.tsx create mode 100644 packages/scenes-react/src/contexts/SceneContextObject.test.tsx create mode 100644 packages/scenes-react/src/contexts/SceneContextObject.tsx create mode 100644 packages/scenes-react/src/contexts/SceneContextProvider.test.tsx create mode 100644 packages/scenes-react/src/contexts/SceneContextProvider.tsx create mode 100644 packages/scenes-react/src/hooks/hooks.ts create mode 100644 packages/scenes-react/src/hooks/useQueryRunner.test.tsx create mode 100644 packages/scenes-react/src/hooks/useQueryRunner.ts create mode 100644 packages/scenes-react/src/hooks/useVariableValues.test.tsx create mode 100644 packages/scenes-react/src/hooks/useVariableValues.ts create mode 100644 packages/scenes-react/src/index.ts create mode 100644 packages/scenes-react/src/types.ts create mode 100644 packages/scenes-react/src/utils.ts create mode 100644 packages/scenes-react/src/utils/testUtils.tsx create mode 100644 packages/scenes-react/src/variables/CustomVariable.tsx create mode 100644 packages/scenes-react/tsconfig.build.json create mode 100644 packages/scenes-react/tsconfig.json create mode 100644 packages/scenes-react/utils/setupTests.ts create mode 100644 packages/scenes-react/utils/test/__mocks__/pluginMocks.ts create mode 100644 packages/scenes/src/core/PanelBuilders/VizConfigBuilder.ts create mode 100644 packages/scenes/src/core/PanelBuilders/VizConfigBuilders.ts create mode 100644 turbo.json diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 00ea97631..6cc2981cc 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -23,13 +23,13 @@ jobs: uses: ./.github/actions/yarn-nm-install - name: Test library - run: yarn run lerna run test + run: yarn test - name: Typecheck library - run: yarn run lerna run typecheck + run: yarn typecheck - name: Build library - run: yarn run lerna run build --ignore website + run: yarn build release: name: Release diff --git a/.gitignore b/.gitignore index 8fde3a1c3..4d660d14b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ node_modules build .env + +# turbo +.turbo \ No newline at end of file diff --git a/docusaurus/website/package.json b/docusaurus/website/package.json index 9bd99e119..48260b7a5 100644 --- a/docusaurus/website/package.json +++ b/docusaurus/website/package.json @@ -17,6 +17,8 @@ "dependencies": { "@docusaurus/core": "2.4.0", "@docusaurus/preset-classic": "2.4.0", + "@grafana/scenes": "workspace:*", + "@grafana/scenes-react": "workspace:*", "@iconscout/unicons": "^4.0.8", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", diff --git a/lerna.json b/lerna.json index 763fac79d..e0c7283f7 100644 --- a/lerna.json +++ b/lerna.json @@ -5,4 +5,4 @@ "packages": [ "packages/*" ] -} +} \ No newline at end of file diff --git a/package.json b/package.json index dbaebe373..7f0ef8ad7 100644 --- a/package.json +++ b/package.json @@ -30,19 +30,19 @@ "prepare": "husky install", "packages:publish": "lerna exec --no-private -- npm publish", "docs": "yarn workspace website run start --port 8080", - "docs:build": "yarn workspace website run build", - "test:lib": "lerna run test --scope '@grafana/scenes' --", - "dev:lib": "lerna run dev --scope '@grafana/scenes' --", - "dev:app": "lerna run dev --scope 'scenes-app' --", - "test": "lerna run test --scope '@grafana/scenes' -- --watch", - "typecheck": "lerna run typecheck" + "docs:build": "yarn turbo build --filter=website", + "build": "yarn turbo build --filter=!website", + "test:scenes": "yarn turbo test --filter=@grafana/scenes -- --watch", + "test:scenes-react": "yarn turbo test --filter=@grafana/scenes-react -- --watch", + "dev": "yarn turbo dev --filter=!website", + "test": "yarn turbo test --filter=!website", + "typecheck": "yarn turbo typecheck --filter=!website" }, "resolutions": { "@types/react": "18.2.74" }, "packageManager": "yarn@4.1.1", "workspaces": [ - ".", "packages/*", "docusaurus/website" ], @@ -51,7 +51,10 @@ "@auto-it/released": "^11.0.7", "@testing-library/react": "^14.1.2", "auto": "^11.0.7", + "eslint": "^8.57.0", "lerna": "^6.5.1", - "lint-staged": "^13.2.0" + "lint-staged": "^13.2.0", + "prettier": "^3.2.5", + "turbo": "latest" } } diff --git a/packages/scenes-app/package.json b/packages/scenes-app/package.json index 08db8be47..d5c884688 100644 --- a/packages/scenes-app/package.json +++ b/packages/scenes-app/package.json @@ -63,6 +63,7 @@ "@grafana/data": "^10.4.1", "@grafana/runtime": "^10.4.1", "@grafana/scenes": "workspace:*", + "@grafana/scenes-react": "workspace:*", "@grafana/schema": "^10.4.1", "@grafana/ui": "^10.4.1", "@types/lodash": "latest", diff --git a/packages/scenes-app/src/components/Routes/Routes.tsx b/packages/scenes-app/src/components/Routes/Routes.tsx index 0380d8c85..f01432fd6 100644 --- a/packages/scenes-app/src/components/Routes/Routes.tsx +++ b/packages/scenes-app/src/components/Routes/Routes.tsx @@ -4,6 +4,7 @@ import { prefixRoute } from '../../utils/utils.routing'; import { ROUTES } from '../../constants'; import { DemoListPage } from '../../pages/DemoListPage'; import GrafanaMonitoringApp from '../../monitoring-app/GrafanaMonitoringApp'; +import { ReactDemoPage } from '../../react-demo/Home'; export const Routes = () => { return ( @@ -11,6 +12,7 @@ export const Routes = () => { {/* Default page */} + ); diff --git a/packages/scenes-app/src/constants.ts b/packages/scenes-app/src/constants.ts index 3f3b6ce6d..7842697df 100644 --- a/packages/scenes-app/src/constants.ts +++ b/packages/scenes-app/src/constants.ts @@ -6,6 +6,7 @@ export enum ROUTES { Home = '', Demos = 'demos', GrafanaMonitoring = 'grafana-monitoring', + ReactDemo = 'react-only', } export const DATASOURCE_REF = { diff --git a/packages/scenes-app/src/demos/index.ts b/packages/scenes-app/src/demos/index.ts index f67283c33..19cb39cc1 100644 --- a/packages/scenes-app/src/demos/index.ts +++ b/packages/scenes-app/src/demos/index.ts @@ -35,6 +35,7 @@ import { getInteractiveTableDemo } from './interactiveTableDemo'; import { getVariableRepeaterDemo } from './variableRepeater'; import { getQueryControllerDemo } from './queryController'; import { getDynamicDataLayersDemo } from './dynamicDataLayers'; +import { getInteropDemo } from './interopDemo'; export interface DemoDescriptor { title: string; @@ -79,5 +80,6 @@ export function getDemos(): DemoDescriptor[] { { title: 'Vertical controls layout', getPage: getVerticalControlsLayoutDemo }, { title: 'Interactive table with expandable rows', getPage: getInteractiveTableDemo }, { title: 'Query controller demo', getPage: getQueryControllerDemo }, + { title: 'Interop with hooks and context', getPage: getInteropDemo }, ].sort((a, b) => a.title.localeCompare(b.title)); } diff --git a/packages/scenes-app/src/demos/interopDemo.tsx b/packages/scenes-app/src/demos/interopDemo.tsx new file mode 100644 index 000000000..53a11fdaa --- /dev/null +++ b/packages/scenes-app/src/demos/interopDemo.tsx @@ -0,0 +1,51 @@ +import { + EmbeddedScene, + SceneAppPage, + SceneAppPageState, + SceneComponentProps, + SceneFlexItem, + SceneFlexLayout, + SceneObjectBase, + SceneObjectState, + VizPanel, +} from '@grafana/scenes'; +import { getEmbeddedSceneDefaults, getQueryRunnerWithRandomWalkQuery } from './utils'; +import React from 'react'; +import { useTimeRange } from '@grafana/scenes-react'; + +export function getInteropDemo(defaults: SceneAppPageState) { + return new SceneAppPage({ + ...defaults, + subTitle: 'Testing using the hooks and plain react components from normal scene', + getScene: () => { + return new EmbeddedScene({ + ...getEmbeddedSceneDefaults(), + // context: new SceneContextObject({}), + key: 'Flex layout embedded scene', + body: new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneFlexItem({ + body: new VizPanel({ + title: 'Graph', + pluginId: 'timeseries', + $data: getQueryRunnerWithRandomWalkQuery({}), + }), + }), + new SceneFlexItem({ + body: new CustomSceneObject({}), + }), + ], + }), + }); + }, + }); +} + +class CustomSceneObject extends SceneObjectBase { + static Component = ({ model }: SceneComponentProps) => { + const [timeRange, _] = useTimeRange(); + + return
Time hook: {timeRange.from.toString()}
; + }; +} diff --git a/packages/scenes-app/src/demos/utils.ts b/packages/scenes-app/src/demos/utils.ts index 8e06bcdb5..9be453547 100644 --- a/packages/scenes-app/src/demos/utils.ts +++ b/packages/scenes-app/src/demos/utils.ts @@ -8,9 +8,9 @@ import { SceneTimePicker, SceneTimeRange, VariableValueSelectors, + SceneDataQuery, } from '@grafana/scenes'; import { DATASOURCE_REF } from '../constants'; -import { DataQueryExtended } from '@grafana/scenes/src/querying/SceneQueryRunner'; export function getQueryRunnerWithRandomWalkQuery( overrides?: Partial, @@ -51,7 +51,7 @@ export function getRowWithText(text: string) { }); } -export function getPromQueryInstant(query: Partial): SceneQueryRunner { +export function getPromQueryInstant(query: Partial): SceneQueryRunner { return new SceneQueryRunner({ datasource: { uid: 'gdev-prometheus' }, queries: [ @@ -66,7 +66,7 @@ export function getPromQueryInstant(query: Partial): SceneQue }); } -export function getPromQueryTimeSeries(query: Partial): SceneQueryRunner { +export function getPromQueryTimeSeries(query: Partial): SceneQueryRunner { return new SceneQueryRunner({ datasource: { uid: 'gdev-prometheus' }, queries: [ diff --git a/packages/scenes-app/src/plugin.json b/packages/scenes-app/src/plugin.json index 22641ea1b..7f98422cd 100644 --- a/packages/scenes-app/src/plugin.json +++ b/packages/scenes-app/src/plugin.json @@ -39,6 +39,13 @@ "path": "/a/%PLUGIN_ID%/grafana-monitoring", "role": "Admin", "addToNav": true + }, + { + "type": "page", + "name": "React only demo", + "path": "/a/%PLUGIN_ID%/react-only", + "role": "Admin", + "addToNav": true } ], "dependencies": { diff --git a/packages/scenes-app/src/react-demo/DrilldownDemoPage.tsx b/packages/scenes-app/src/react-demo/DrilldownDemoPage.tsx new file mode 100644 index 000000000..e0a278aae --- /dev/null +++ b/packages/scenes-app/src/react-demo/DrilldownDemoPage.tsx @@ -0,0 +1,68 @@ +import { VizConfigBuilders } from '@grafana/scenes'; +import { Breadcrumb, BreadcrumbProvider, VizPanel, useQueryRunner } from '@grafana/scenes-react'; +import React from 'react'; +import { DATASOURCE_REF } from '../constants'; +import { PageWrapper } from './PageWrapper'; +import { DemoVizLayout, urlBase } from './utils'; +import { Route, Switch, useParams } from 'react-router-dom'; +import { PlainGraphWithRandomWalk } from './PlainGraphWithRandomWalk'; + +export function DrilldownDemoPage() { + /** + * The breadcrumb provider should really be wrapping all routes, but placing it here just to show the concept and so far this is the only pages that use it. + * It needs to be above the PageWrapper as it's the the component that uses the BreadcrumbContext + */ + return ( + + + + + + + + ); +} + +export function DrilldownHome() { + const dataProvider = useQueryRunner({ + queries: [ + { + scenarioId: 'csv_file', + refId: 'A', + csvFileName: 'js_libraries.csv', + }, + ], + datasource: DATASOURCE_REF, + }); + + return ( + + + + + + ); +} + +export function DrilldownLibraryPage() { + const libraryName = useParams<{ lib: string }>().lib; + + return ( + + + + + + ); +} + +export const tableWithDrilldown = VizConfigBuilders.table() + .setOverrides((b) => + b.matchFieldsWithName('Library').overrideLinks([ + { + title: 'Go to library details', + url: '${__url.path}/lib/${__value.text}', + }, + ]) + ) + .build(); diff --git a/packages/scenes-app/src/react-demo/DynamicQueriesPage.tsx b/packages/scenes-app/src/react-demo/DynamicQueriesPage.tsx new file mode 100644 index 000000000..65e890007 --- /dev/null +++ b/packages/scenes-app/src/react-demo/DynamicQueriesPage.tsx @@ -0,0 +1,55 @@ +import { useQueryRunner, VizPanel } from '@grafana/scenes-react'; +import { Field, Select, Stack } from '@grafana/ui'; +import React, { useMemo, useState } from 'react'; +import { DATASOURCE_REF } from '../constants'; +import { PageWrapper } from './PageWrapper'; +import { toOption } from '@grafana/data'; +import { SceneDataQuery } from '@grafana/scenes'; +import { plainGraph } from './visualizations'; +import { DemoVizLayout } from './utils'; + +export function DynamicQueriesPage() { + const scenarios = ['Slow query', 'Random walk'].map(toOption); + const [scenario, setScenario] = useState('Random walk'); + const queries = useMemo(() => buildQueriesForScenario(scenario), [scenario]); + + const dataProvider = useQueryRunner({ queries: queries, maxDataPoints: 100, datasource: DATASOURCE_REF }); + + return ( + + + + +