From f07cd415ded434f92ce76882113239182ba8519e Mon Sep 17 00:00:00 2001 From: Debsmita Santra Date: Wed, 17 Dec 2025 15:47:47 +0530 Subject: [PATCH 1/2] [spike]: Resizable docked mode exploration --- .../quickstart/packages/app/package.json | 2 + .../src/components/Root/ApplicationDrawer.tsx | 62 +++++++ .../packages/app/src/components/Root/Root.tsx | 116 ++++++++----- .../components/Root/TestDrawerSidebarItem.tsx | 26 +++ .../plugins/application-drawer/package.json | 58 +++++++ .../components/ApplicationDrawerContext.tsx | 73 ++++++++ .../components/ApplicationDrawerProvider.tsx | 81 +++++++++ .../src/components/ResizableDrawer.tsx | 157 ++++++++++++++++++ .../src/components/index.ts | 24 +++ .../plugins/application-drawer/src/index.ts | 19 +++ .../plugins/application-drawer/src/plugin.ts | 27 +++ .../plugins/quickstart/dev/index.tsx | 14 +- .../plugins/quickstart/package.json | 1 + ...ickstartDrawer.tsx => DrawerComponent.tsx} | 18 +- .../components/QuickstartDrawerContent.tsx | 69 ++++++++ .../components/QuickstartDrawerContext.tsx | 1 + .../components/QuickstartDrawerProvider.tsx | 13 +- .../plugins/quickstart/src/plugin.ts | 17 ++ .../quickstart/plugins/test-drawer/README.md | 79 +++++++++ .../plugins/test-drawer/dev/index.tsx | 87 ++++++++++ .../plugins/test-drawer/package.json | 59 +++++++ .../src/components/DrawerComponent.tsx | 34 ++++ .../src/components/TestDrawerButton.tsx | 44 +++++ .../src/components/TestDrawerContent.tsx | 121 ++++++++++++++ .../src/components/TestDrawerContext.tsx | 67 ++++++++ .../src/components/TestDrawerProvider.tsx | 88 ++++++++++ .../test-drawer/src/components/index.ts | 26 +++ .../plugins/test-drawer/src/index.ts | 29 ++++ .../plugins/test-drawer/src/plugin.ts | 77 +++++++++ workspaces/quickstart/yarn.lock | 46 +++++ 30 files changed, 1475 insertions(+), 60 deletions(-) create mode 100644 workspaces/quickstart/packages/app/src/components/Root/ApplicationDrawer.tsx create mode 100644 workspaces/quickstart/packages/app/src/components/Root/TestDrawerSidebarItem.tsx create mode 100644 workspaces/quickstart/plugins/application-drawer/package.json create mode 100644 workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerContext.tsx create mode 100644 workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerProvider.tsx create mode 100644 workspaces/quickstart/plugins/application-drawer/src/components/ResizableDrawer.tsx create mode 100644 workspaces/quickstart/plugins/application-drawer/src/components/index.ts create mode 100644 workspaces/quickstart/plugins/application-drawer/src/index.ts create mode 100644 workspaces/quickstart/plugins/application-drawer/src/plugin.ts rename workspaces/quickstart/plugins/quickstart/src/components/{QuickstartDrawer.tsx => DrawerComponent.tsx} (83%) create mode 100644 workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerContent.tsx create mode 100644 workspaces/quickstart/plugins/test-drawer/README.md create mode 100644 workspaces/quickstart/plugins/test-drawer/dev/index.tsx create mode 100644 workspaces/quickstart/plugins/test-drawer/package.json create mode 100644 workspaces/quickstart/plugins/test-drawer/src/components/DrawerComponent.tsx create mode 100644 workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerButton.tsx create mode 100644 workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerContent.tsx create mode 100644 workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerContext.tsx create mode 100644 workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerProvider.tsx create mode 100644 workspaces/quickstart/plugins/test-drawer/src/components/index.ts create mode 100644 workspaces/quickstart/plugins/test-drawer/src/index.ts create mode 100644 workspaces/quickstart/plugins/test-drawer/src/plugin.ts diff --git a/workspaces/quickstart/packages/app/package.json b/workspaces/quickstart/packages/app/package.json index f3584c70e6..0f1d93bdc4 100644 --- a/workspaces/quickstart/packages/app/package.json +++ b/workspaces/quickstart/packages/app/package.json @@ -49,8 +49,10 @@ "@material-ui/icons": "^4.9.1", "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", + "@red-hat-developer-hub/backstage-plugin-application-drawer": "workspace:^", "@red-hat-developer-hub/backstage-plugin-global-header": "^1.17.1", "@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^", + "@red-hat-developer-hub/backstage-plugin-test-drawer": "workspace:^", "@red-hat-developer-hub/backstage-plugin-theme": "^0.11.0", "react": "^18.0.2", "react-dom": "^18.0.2", diff --git a/workspaces/quickstart/packages/app/src/components/Root/ApplicationDrawer.tsx b/workspaces/quickstart/packages/app/src/components/Root/ApplicationDrawer.tsx new file mode 100644 index 0000000000..8543907bbe --- /dev/null +++ b/workspaces/quickstart/packages/app/src/components/Root/ApplicationDrawer.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { ComponentType } from 'react'; +import { + useApplicationDrawerContext, + ResizableDrawer, +} from '@red-hat-developer-hub/backstage-plugin-application-drawer'; + +type DrawerContentType = { + id: string; + Component: ComponentType; + priority?: number; + resizable?: boolean; +}; + +export const ApplicationDrawer = ({ + drawerContents, +}: { + drawerContents: DrawerContentType[]; +}) => { + const { getDrawers } = useApplicationDrawerContext(); + + // Get active drawer - compute fresh each render since we use refs + const drawers = getDrawers(); + const activeDrawer = drawers + .filter(p => p.isDrawerOpen) + .map(p => { + const content = drawerContents.find(c => c.id === p.id); + if (!content) return null; + return { ...p, ...content }; + }) + .filter(Boolean) + .sort((a, b) => (b?.priority ?? -1) - (a?.priority ?? -1))[0]; + + if (!activeDrawer) return null; + + const { Component, resizable, drawerWidth, setDrawerWidth } = activeDrawer; + return ( + + + + ); +}; diff --git a/workspaces/quickstart/packages/app/src/components/Root/Root.tsx b/workspaces/quickstart/packages/app/src/components/Root/Root.tsx index 9731ec6cc9..19e3919679 100644 --- a/workspaces/quickstart/packages/app/src/components/Root/Root.tsx +++ b/workspaces/quickstart/packages/app/src/components/Root/Root.tsx @@ -50,6 +50,14 @@ import Box from '@mui/material/Box'; import { QuickstartDrawerProvider } from '@red-hat-developer-hub/backstage-plugin-quickstart'; import { QuickstartSidebarItem } from './QuickstartSidebarItem'; import { Administration } from '@backstage-community/plugin-rbac'; +import { QuickstartDrawerContent } from '@red-hat-developer-hub/backstage-plugin-quickstart'; +import { + TestDrawerContent, + TestDrawerProvider, +} from '@red-hat-developer-hub/backstage-plugin-test-drawer'; +import { ApplicationDrawerProvider } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; +import { ApplicationDrawer } from './ApplicationDrawer'; +import { TestDrawerSidebarItem } from './TestDrawerSidebarItem'; const useSidebarLogoStyles = makeStyles({ root: { @@ -94,54 +102,80 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { transition: 'margin-right 0.3s ease', }, }, + 'body.test-drawer-open #sidebar&': { + "> div > main[class*='BackstagePage-root']": { + marginRight: 'calc(var(--test-drawer-width, 500px) + 1.5em)', + transition: 'margin-right 0.3s ease', + }, + }, }} > - - - - } to="/search"> - - - - }> - {/* Global nav, not org-specific */} - - - - - + + + + + } to="/search"> + + + + }> + {/* Global nav, not org-specific */} + + + + + + {/* End global nav */} + + + + {/* Items in this group will be scrollable if they run out of space */} + + + + + + + } + to="/settings" + > + + + + {children} + - {/* End global nav */} - - - - {/* Items in this group will be scrollable if they run out of space */} - - - - - - } - to="/settings" - > - - - - {children} - + + + ); diff --git a/workspaces/quickstart/packages/app/src/components/Root/TestDrawerSidebarItem.tsx b/workspaces/quickstart/packages/app/src/components/Root/TestDrawerSidebarItem.tsx new file mode 100644 index 0000000000..0b47afcd90 --- /dev/null +++ b/workspaces/quickstart/packages/app/src/components/Root/TestDrawerSidebarItem.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { SidebarItem, StarIcon } from '@backstage/core-components'; +import { useTestDrawerContext } from '@red-hat-developer-hub/backstage-plugin-test-drawer'; + +export const TestDrawerSidebarItem = () => { + const { toggleDrawer } = useTestDrawerContext(); + + return ( + + ); +}; diff --git a/workspaces/quickstart/plugins/application-drawer/package.json b/workspaces/quickstart/plugins/application-drawer/package.json new file mode 100644 index 0000000000..9b504bb75f --- /dev/null +++ b/workspaces/quickstart/plugins/application-drawer/package.json @@ -0,0 +1,58 @@ +{ + "name": "@red-hat-developer-hub/backstage-plugin-application-drawer", + "version": "0.1.0", + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/redhat-developer/rhdh-plugins", + "directory": "workspaces/quickstart/plugins/application-drawer" + }, + "backstage": { + "role": "frontend-plugin", + "pluginId": "application-drawer", + "pluginPackages": [ + "@red-hat-developer-hub/backstage-plugin-application-drawer" + ] + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.18.3", + "@backstage/core-plugin-api": "^1.12.0", + "@backstage/theme": "^0.7.0", + "@mui/icons-material": "5.18.0", + "@mui/material": "5.18.0", + "@red-hat-developer-hub/backstage-plugin-theme": "^0.11.0" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.34.5", + "@backstage/dev-utils": "^1.1.17", + "@backstage/test-utils": "^1.7.13", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^14.0.0", + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "files": [ + "dist" + ] +} diff --git a/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerContext.tsx b/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerContext.tsx new file mode 100644 index 0000000000..6acd24fc6d --- /dev/null +++ b/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerContext.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { createContext, useContext } from 'react'; + +/** + * Type for a drawer context + * + * @public + */ +export type DrawerContext = { + id: string; + isDrawerOpen: boolean; + drawerWidth?: number; + setDrawerWidth?: React.Dispatch>; +}; + +/** + * Type for ApplicationDrawerContext + * + * @public + */ +export type ApplicationDrawerContextType = { + addDrawerContext: ( + id: string, + context: { + isDrawerOpen: boolean; + drawerWidth?: number; + setDrawerWidth?: React.Dispatch>; + }, + ) => void; + getDrawers: () => DrawerContext[]; +}; + +/** + * Context for the Application Drawer + * + * Allows drawer providers (Quickstart, Lightspeed, etc.) to register + * themselves so ApplicationDrawer can render the active one. + * + * @public + */ +export const ApplicationDrawerContext = createContext< + ApplicationDrawerContextType | undefined +>(undefined); + +/** + * Hook to access the ApplicationDrawerContext + * + * @public + */ +export const useApplicationDrawerContext = (): ApplicationDrawerContextType => { + const context = useContext(ApplicationDrawerContext); + if (!context) { + throw new Error( + 'useApplicationDrawerContext must be used within an ApplicationDrawerProvider', + ); + } + return context; +}; diff --git a/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerProvider.tsx b/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerProvider.tsx new file mode 100644 index 0000000000..2c07e42602 --- /dev/null +++ b/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerProvider.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { PropsWithChildren, useCallback, useState, useMemo } from 'react'; +import { + ApplicationDrawerContext, + DrawerContext, +} from './ApplicationDrawerContext'; + +/** + * Provider component for the ApplicationDrawer functionality + * + * This provider should wrap all drawer providers (QuickstartDrawerProvider, + * TestDrawerProvider, etc.) to allow them to register themselves. + * + * @public + */ +export const ApplicationDrawerProvider = ({ children }: PropsWithChildren) => { + const [drawers, setDrawers] = useState([]); + + const addDrawerContext = useCallback( + ( + id: string, + context: { + isDrawerOpen: boolean; + drawerWidth?: number; + setDrawerWidth?: React.Dispatch>; + }, + ) => { + setDrawers(prev => { + const existingIndex = prev.findIndex(d => d.id === id); + const newContext: DrawerContext = { id, ...context }; + + if (existingIndex !== -1) { + // Check if anything actually changed + const existing = prev[existingIndex]; + if ( + existing.isDrawerOpen === newContext.isDrawerOpen && + existing.drawerWidth === newContext.drawerWidth + ) { + return prev; + } + const updated = [...prev]; + updated[existingIndex] = newContext; + return updated; + } + return [...prev, newContext]; + }); + }, + [], + ); + + const getDrawers = useCallback(() => drawers, [drawers]); + + const contextValue = useMemo( + () => ({ + addDrawerContext, + getDrawers, + }), + [addDrawerContext, getDrawers], + ); + + return ( + + {children} + + ); +}; diff --git a/workspaces/quickstart/plugins/application-drawer/src/components/ResizableDrawer.tsx b/workspaces/quickstart/plugins/application-drawer/src/components/ResizableDrawer.tsx new file mode 100644 index 0000000000..3b17a3b015 --- /dev/null +++ b/workspaces/quickstart/plugins/application-drawer/src/components/ResizableDrawer.tsx @@ -0,0 +1,157 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { useCallback, useEffect, useRef, useState } from 'react'; + +import Box from '@mui/material/Box'; +import Drawer from '@mui/material/Drawer'; +import { styled } from '@mui/material/styles'; + +import { ThemeConfig } from '@red-hat-developer-hub/backstage-plugin-theme'; + +const Handle = styled('div')(({ theme }) => ({ + width: 6, + cursor: 'col-resize', + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + zIndex: 1201, + backgroundColor: theme.palette.divider, +})); + +export type ResizableDrawerProps = { + children: React.ReactNode; + minWidth?: number; + maxWidth?: number; + initialWidth?: number; + isDrawerOpen: boolean; + drawerWidth?: number; + onWidthChange?: (width: number) => void; + isResizable?: boolean; + [key: string]: any; +}; + +export const ResizableDrawer = (props: ResizableDrawerProps) => { + const { + children, + minWidth = 400, + maxWidth = 800, + initialWidth = 400, + isDrawerOpen, + isResizable = false, + drawerWidth: externalDrawerWidth, + onWidthChange, + ...drawerProps + } = props; + + // Ensure width is never below minWidth + const clampedInitialWidth = Math.max( + externalDrawerWidth || initialWidth, + minWidth, + ); + + const [width, setWidth] = useState(clampedInitialWidth); + const resizingRef = useRef(false); + + // Sync with external drawerWidth when it changes + useEffect(() => { + if (externalDrawerWidth !== undefined) { + const clampedWidth = Math.max(externalDrawerWidth, minWidth); + if (clampedWidth !== width) { + setWidth(clampedWidth); + // If the external width was below min, update the parent + if (externalDrawerWidth < minWidth && onWidthChange && isResizable) { + onWidthChange(clampedWidth); + } + } + } + }, [externalDrawerWidth, width, minWidth, onWidthChange, isResizable]); + + const onMouseDown = () => { + resizingRef.current = true; + }; + + const onMouseMove = useCallback( + (e: MouseEvent) => { + if (!resizingRef.current) return; + // For right-anchored drawer, calculate width from the right edge + const newWidth = window.innerWidth - e.clientX; + + if (newWidth >= minWidth && newWidth <= maxWidth) { + setWidth(newWidth); + if (onWidthChange) { + onWidthChange(newWidth); + } + } + }, + [maxWidth, minWidth, onWidthChange], + ); + + const onMouseUp = () => { + resizingRef.current = false; + }; + + useEffect(() => { + if (isResizable) { + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + return () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + }; + } + return () => {}; + }, [onMouseMove, isResizable]); + + // Ensure anchor is always 'right' and not overridden by drawerProps + const { anchor: _, ...restDrawerProps } = drawerProps; + + return ( + { + const themeConfig = theme as ThemeConfig; + return ( + themeConfig.palette?.rhdh?.general?.sidebarBackgroundColor || + theme.palette.background.paper + ); + }, + justifyContent: 'space-between', + }, + // Only apply header offset when global header exists + 'body:has(#global-header) &': { + '& .v5-MuiDrawer-paper': { + top: '64px !important', + height: 'calc(100vh - 64px) !important', + }, + }, + }} + variant="persistent" + open={isDrawerOpen} + > + + {children} + {isResizable && } + + + ); +}; diff --git a/workspaces/quickstart/plugins/application-drawer/src/components/index.ts b/workspaces/quickstart/plugins/application-drawer/src/components/index.ts new file mode 100644 index 0000000000..6adffc407c --- /dev/null +++ b/workspaces/quickstart/plugins/application-drawer/src/components/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Red Hat, Inc. + * + * 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. + */ + +export { useApplicationDrawerContext } from './ApplicationDrawerContext'; +export type { + ApplicationDrawerContextType, + DrawerContext, +} from './ApplicationDrawerContext'; +export { ApplicationDrawerProvider } from './ApplicationDrawerProvider'; +export { ResizableDrawer } from './ResizableDrawer'; +export type { ResizableDrawerProps } from './ResizableDrawer'; diff --git a/workspaces/quickstart/plugins/application-drawer/src/index.ts b/workspaces/quickstart/plugins/application-drawer/src/index.ts new file mode 100644 index 0000000000..1970cc98bc --- /dev/null +++ b/workspaces/quickstart/plugins/application-drawer/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Red Hat, Inc. + * + * 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. + */ + +export * from './plugin'; +export * from './components'; + diff --git a/workspaces/quickstart/plugins/application-drawer/src/plugin.ts b/workspaces/quickstart/plugins/application-drawer/src/plugin.ts new file mode 100644 index 0000000000..4f9d39abe2 --- /dev/null +++ b/workspaces/quickstart/plugins/application-drawer/src/plugin.ts @@ -0,0 +1,27 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { createPlugin } from '@backstage/core-plugin-api'; + +/** + * The application-drawer plugin + * + * @public + */ +export const applicationDrawerPlugin = createPlugin({ + id: 'application-drawer', +}); + diff --git a/workspaces/quickstart/plugins/quickstart/dev/index.tsx b/workspaces/quickstart/plugins/quickstart/dev/index.tsx index 1b36d1335f..515a2a51d2 100644 --- a/workspaces/quickstart/plugins/quickstart/dev/index.tsx +++ b/workspaces/quickstart/plugins/quickstart/dev/index.tsx @@ -25,6 +25,9 @@ import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Button from '@mui/material/Button'; import { getAllThemes } from '@red-hat-developer-hub/backstage-plugin-theme'; +import { QuickstartDrawerContent } from '../src/components/QuickstartDrawerContent'; +import { DrawerComponent } from '../src/components/DrawerComponent'; +import { ApplicationDrawerProvider } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; const QuickstartTestPageContent = () => { const { openDrawer, closeDrawer, isDrawerOpen } = @@ -97,9 +100,14 @@ const QuickstartTestPageContent = () => { }; const QuickstartTestPage = () => ( - - - + + + + + + + + ); createDevApp() diff --git a/workspaces/quickstart/plugins/quickstart/package.json b/workspaces/quickstart/plugins/quickstart/package.json index aef5ae8a05..fbc3c13b55 100644 --- a/workspaces/quickstart/plugins/quickstart/package.json +++ b/workspaces/quickstart/plugins/quickstart/package.json @@ -37,6 +37,7 @@ "@backstage/theme": "^0.7.0", "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", + "@red-hat-developer-hub/backstage-plugin-application-drawer": "^0.1.0", "@red-hat-developer-hub/backstage-plugin-theme": "^0.11.0", "react-use": "^17.6.0" }, diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawer.tsx b/workspaces/quickstart/plugins/quickstart/src/components/DrawerComponent.tsx similarity index 83% rename from workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawer.tsx rename to workspaces/quickstart/plugins/quickstart/src/components/DrawerComponent.tsx index 76856b34ee..c14c9bcf47 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawer.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/DrawerComponent.tsx @@ -14,18 +14,17 @@ * limitations under the License. */ +import { PropsWithChildren, useMemo } from 'react'; import Drawer from '@mui/material/Drawer'; import { ThemeConfig } from '@red-hat-developer-hub/backstage-plugin-theme'; import { configApiRef, useApiHolder } from '@backstage/core-plugin-api'; -import { Quickstart } from './Quickstart'; import { useQuickstartDrawerContext } from '../hooks/useQuickstartDrawerContext'; import { QuickstartItemData } from '../types'; import { filterQuickstartItemsByRole } from '../utils'; // Role is now provided through context to avoid re-fetching on drawer open/close -import { useMemo } from 'react'; -export const QuickstartDrawer = () => { - const { isDrawerOpen, closeDrawer, drawerWidth, userRole, roleLoading } = +export const DrawerComponent = ({ children }: PropsWithChildren) => { + const { isDrawerOpen, drawerWidth, userRole, roleLoading } = useQuickstartDrawerContext(); const apiHolder = useApiHolder(); @@ -43,11 +42,6 @@ export const QuickstartDrawer = () => { : []; }, [roleLoading, userRole, quickstartItems]); - // Only expose items to the body when drawer is open to avoid re-renders during close - const filteredItems = useMemo(() => { - return isDrawerOpen ? eligibleItems : []; - }, [isDrawerOpen, eligibleItems]); - // No auto-open logic here; the provider initializes per user (visited/open) // If no quickstart items are configured at all, don't render the drawer to avoid reserving space @@ -87,11 +81,7 @@ export const QuickstartDrawer = () => { anchor="right" open={isDrawerOpen} > - + {children} ); }; diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerContent.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerContent.tsx new file mode 100644 index 0000000000..c5268acec0 --- /dev/null +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerContent.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { useMemo } from 'react'; +import { configApiRef, useApiHolder } from '@backstage/core-plugin-api'; +import { Quickstart } from './Quickstart'; +import { useQuickstartDrawerContext } from '../hooks/useQuickstartDrawerContext'; +import { QuickstartItemData } from '../types'; +import { filterQuickstartItemsByRole } from '../utils'; + +export const QuickstartDrawerContent = () => { + const { isDrawerOpen, closeDrawer, userRole, roleLoading } = + useQuickstartDrawerContext(); + + const apiHolder = useApiHolder(); + const config = apiHolder.get(configApiRef); + const quickstartItems: QuickstartItemData[] = useMemo(() => { + return config?.has('app.quickstart') + ? (config.get('app.quickstart') as QuickstartItemData[]) + : []; + }, [config]); + + // Items available to the user based on role from context + const eligibleItems = useMemo(() => { + return !roleLoading && userRole + ? filterQuickstartItemsByRole(quickstartItems, userRole) + : []; + }, [roleLoading, userRole, quickstartItems]); + + // Only expose items to the body when drawer is open to avoid re-renders during close + const filteredItems = useMemo(() => { + return isDrawerOpen ? eligibleItems : []; + }, [isDrawerOpen, eligibleItems]); + + // No auto-open logic here; the provider initializes per user (visited/open) + + // If no quickstart items are configured at all, don't render the drawer to avoid reserving space + if (quickstartItems.length === 0) { + return null; + } + + // If there are no items for the user, hide the drawer entirely + if (!roleLoading && eligibleItems.length === 0) { + return null; + } + + // No role-fetching or filtering here when the drawer is closed + + return ( + + ); +}; diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerContext.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerContext.tsx index c89741296b..2f9a360c4f 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerContext.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerContext.tsx @@ -23,6 +23,7 @@ import { UserRole } from '../types'; * @public */ export interface QuickstartDrawerContextType { + id: string; /** * The prop to check if the drawer is open */ diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerProvider.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerProvider.tsx index 1db449e068..a6cdf96ced 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerProvider.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerProvider.tsx @@ -23,8 +23,8 @@ import { import Snackbar from '@mui/material/Snackbar'; import CloseIcon from '@mui/icons-material/Close'; import IconButton from '@mui/material/IconButton'; +import { useApplicationDrawerContext } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; import { QuickstartDrawerContext } from './QuickstartDrawerContext'; -import { QuickstartDrawer } from './QuickstartDrawer'; import { QuickstartItemData } from '../types'; import { filterQuickstartItemsByRole } from '../utils'; import { useQuickstartRole } from '../hooks/useQuickstartRole'; @@ -41,10 +41,19 @@ export const QuickstartDrawerProvider = ({ children }: PropsWithChildren) => { const [userKey, setUserKey] = useState('guest'); const identityApi = useApi(identityApiRef); const configApi = useApi(configApiRef); + const { addDrawerContext } = useApplicationDrawerContext(); // Determine role once at provider level to avoid re-fetching on drawer open/close const { isLoading: roleLoading, userRole } = useQuickstartRole(); + useEffect(() => { + addDrawerContext('quickstart', { + isDrawerOpen, + drawerWidth, + setDrawerWidth, + }); + }, [addDrawerContext, drawerWidth, isDrawerOpen]); + // Single useEffect - sets class on document.body useEffect(() => { if (isDrawerOpen) { @@ -190,6 +199,7 @@ export const QuickstartDrawerProvider = ({ children }: PropsWithChildren) => { return ( { }} > {children} - = }), ); +/** + * Quickstart Drawer Content component extension + * + * @public + */ +export const QuickstartDrawerContent = quickstartPlugin.provide( + createComponentExtension({ + name: 'QuickstartDrawerContent', + component: { + lazy: () => + import('./components/QuickstartDrawerContent').then( + m => m.QuickstartDrawerContent, + ), + }, + }), +); + /** * Quick start button for global header help dropdown * diff --git a/workspaces/quickstart/plugins/test-drawer/README.md b/workspaces/quickstart/plugins/test-drawer/README.md new file mode 100644 index 0000000000..9d5d314af9 --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/README.md @@ -0,0 +1,79 @@ +# Test Drawer Plugin + +A test drawer plugin for Backstage that demonstrates how to create drawer components with context-based state management. + +## Getting Started + +This plugin can be accessed by running `yarn start` from this directory, and then navigating to [/test-drawer](http://localhost:3000/test-drawer). + +## Components + +### TestDrawerProvider + +Provider component that wraps your application and provides drawer context and the MUI Drawer component. + +```tsx +import { TestDrawerProvider } from '@red-hat-developer-hub/backstage-plugin-test-drawer'; + +export const App = () => ( + + {/* Your app content */} + +); +``` + +### TestDrawerContent + +The content component that renders inside the MUI Drawer. It includes a header with close button, main content area, and footer. + +### TestDrawerButton + +A button component that can be placed anywhere to toggle the drawer. + +```tsx +import { TestDrawerButton } from '@red-hat-developer-hub/backstage-plugin-test-drawer'; + +// Use in header or toolbar + +``` + +### useTestDrawerContext + +Hook to access the drawer context from any component within the provider. + +```tsx +import { useTestDrawerContext } from '@red-hat-developer-hub/backstage-plugin-test-drawer'; + +const MyComponent = () => { + const { isDrawerOpen, openDrawer, closeDrawer, toggleDrawer, drawerWidth, setDrawerWidth } = useTestDrawerContext(); + + return ( + + ); +}; +``` + +## Context API + +The `TestDrawerContextType` provides: + +| Property | Type | Description | +|----------|------|-------------| +| `isDrawerOpen` | `boolean` | Whether the drawer is currently open | +| `openDrawer` | `() => void` | Function to open the drawer | +| `closeDrawer` | `() => void` | Function to close the drawer | +| `toggleDrawer` | `() => void` | Function to toggle the drawer state | +| `drawerWidth` | `number` | Current drawer width in pixels | +| `setDrawerWidth` | `Dispatch>` | Function to set the drawer width | + +## CSS Variables + +When the drawer is open, the following CSS class and variable are set on `document.body`: + +- Class: `test-drawer-open` +- Variable: `--test-drawer-width` (e.g., `400px`) + +This allows you to adjust other UI elements when the drawer is open. + diff --git a/workspaces/quickstart/plugins/test-drawer/dev/index.tsx b/workspaces/quickstart/plugins/test-drawer/dev/index.tsx new file mode 100644 index 0000000000..10213d6912 --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/dev/index.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { createDevApp } from '@backstage/dev-utils'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import { + TestDrawerContent, + testDrawerPlugin, + TestDrawerProvider, + useTestDrawerContext, +} from '../src'; +import { DrawerComponent } from '../src/components'; +import { ApplicationDrawerProvider } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; + +const TestPage = () => { + const { toggleDrawer, isDrawerOpen, drawerWidth } = useTestDrawerContext(); + + return ( + + + Test Drawer Plugin + + + + This page demonstrates the Test Drawer plugin functionality. + + + + + + + + Drawer State: + + Is Open: {isDrawerOpen ? 'Yes' : 'No'} + + + Width: {drawerWidth}px + + + + ); +}; + +const DevPage = () => ( + + + + + + + + +); + +createDevApp() + .registerPlugin(testDrawerPlugin) + .addPage({ + element: , + title: 'Test Drawer', + path: '/test-drawer', + }) + .render(); diff --git a/workspaces/quickstart/plugins/test-drawer/package.json b/workspaces/quickstart/plugins/test-drawer/package.json new file mode 100644 index 0000000000..caefcf7edc --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/package.json @@ -0,0 +1,59 @@ +{ + "name": "@red-hat-developer-hub/backstage-plugin-test-drawer", + "version": "0.1.0", + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/redhat-developer/rhdh-plugins", + "directory": "workspaces/quickstart/plugins/test-drawer" + }, + "backstage": { + "role": "frontend-plugin", + "pluginId": "test-drawer", + "pluginPackages": [ + "@red-hat-developer-hub/backstage-plugin-test-drawer" + ] + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.18.3", + "@backstage/core-plugin-api": "^1.12.0", + "@backstage/theme": "^0.7.0", + "@mui/icons-material": "5.18.0", + "@mui/material": "5.18.0", + "@red-hat-developer-hub/backstage-plugin-application-drawer": "^0.1.0", + "@red-hat-developer-hub/backstage-plugin-theme": "^0.11.0" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.34.5", + "@backstage/dev-utils": "^1.1.17", + "@backstage/test-utils": "^1.7.13", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^14.0.0", + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "files": [ + "dist" + ] +} diff --git a/workspaces/quickstart/plugins/test-drawer/src/components/DrawerComponent.tsx b/workspaces/quickstart/plugins/test-drawer/src/components/DrawerComponent.tsx new file mode 100644 index 0000000000..0dd552eb16 --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/src/components/DrawerComponent.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { PropsWithChildren } from 'react'; +import { ResizableDrawer } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; +import { useTestDrawerContext } from './TestDrawerContext'; + +export const DrawerComponent = ({ children }: PropsWithChildren) => { + const { isDrawerOpen, drawerWidth, setDrawerWidth } = useTestDrawerContext(); + + return ( + + {children} + + ); +}; diff --git a/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerButton.tsx b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerButton.tsx new file mode 100644 index 0000000000..f279c3bc6a --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerButton.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import ViewSidebarIcon from '@mui/icons-material/ViewSidebar'; +import { useTestDrawerContext } from './TestDrawerContext'; + +/** + * Button component to toggle the Test Drawer + * + * Can be used in the header or any other location to trigger the drawer. + * + * @public + */ +export const TestDrawerButton = () => { + const { toggleDrawer, isDrawerOpen } = useTestDrawerContext(); + + return ( + + + + + + ); +}; + diff --git a/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerContent.tsx b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerContent.tsx new file mode 100644 index 0000000000..f0a8fabacb --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerContent.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import CloseIcon from '@mui/icons-material/Close'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import { useTestDrawerContext } from './TestDrawerContext'; + +/** + * Content to be rendered inside the Test Drawer + * + * @public + */ +export const TestDrawerContent = () => { + const { toggleDrawer } = useTestDrawerContext(); + + return ( + + + + Test Drawer + + + + + + + {/* Content */} + + + This is a test drawer component that demonstrates how drawer content + can be structured and rendered inside an MUI Drawer. + + + + Features: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Drawer Plugin v0.1.0 + + + + ); +}; + diff --git a/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerContext.tsx b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerContext.tsx new file mode 100644 index 0000000000..46e8406500 --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerContext.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { createContext, useContext } from 'react'; + +/** + * Type for TestDrawerContext + * + * @public + */ +export interface TestDrawerContextType { + id: string; + /** + * Whether the drawer is open + */ + isDrawerOpen: boolean; + /** + * Function to toggle the drawer state + */ + toggleDrawer: () => void; + /** + * Current drawer width in pixels + */ + drawerWidth: number; + /** + * Function to set the drawer width + */ + setDrawerWidth: React.Dispatch>; +} + +/** + * Context for the Test Drawer + * + * @public + */ +export const TestDrawerContext = createContext< + TestDrawerContextType | undefined +>(undefined); + +/** + * Hook to access the TestDrawerContext + * + * @public + */ +export const useTestDrawerContext = (): TestDrawerContextType => { + const context = useContext(TestDrawerContext); + if (!context) { + throw new Error( + 'useTestDrawerContext must be used within a TestDrawerProvider', + ); + } + return context; +}; + diff --git a/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerProvider.tsx b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerProvider.tsx new file mode 100644 index 0000000000..8b23d59c04 --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerProvider.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { PropsWithChildren, useState, useCallback, useEffect } from 'react'; +import { useApplicationDrawerContext } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; +import { TestDrawerContext } from './TestDrawerContext'; + +const DEFAULT_DRAWER_WIDTH = 400; +const MIN_DRAWER_WIDTH = 300; +const MAX_DRAWER_WIDTH = 800; + +/** + * Provider component for the Test Drawer functionality + * + * @public + */ +export const TestDrawerProvider = ({ children }: PropsWithChildren) => { + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [drawerWidth, setDrawerWidth] = useState(DEFAULT_DRAWER_WIDTH); + + const { addDrawerContext } = useApplicationDrawerContext(); + + useEffect(() => { + addDrawerContext('test-drawer', { + isDrawerOpen, + drawerWidth, + setDrawerWidth, + }); + }, [addDrawerContext, isDrawerOpen, drawerWidth]); + + useEffect(() => { + if (isDrawerOpen) { + document.body.classList.add('test-drawer-open'); + document.body.style.setProperty( + '--test-drawer-width', + `${drawerWidth}px`, + ); + } else { + document.body.classList.remove('test-drawer-open'); + document.body.style.removeProperty('--test-drawer-width'); + } + + return () => { + document.body.classList.remove('test-drawer-open'); + document.body.style.removeProperty('--test-drawer-width'); + }; + }, [isDrawerOpen, drawerWidth]); + + const toggleDrawer = useCallback(() => { + setIsDrawerOpen(prev => !prev); + }, []); + + // Constrain drawer width to min/max bounds + const handleSetDrawerWidth: React.Dispatch> = + useCallback(value => { + setDrawerWidth(prev => { + const newWidth = typeof value === 'function' ? value(prev) : value; + return Math.min(MAX_DRAWER_WIDTH, Math.max(MIN_DRAWER_WIDTH, newWidth)); + }); + }, []); + + return ( + + {children} + + ); +}; diff --git a/workspaces/quickstart/plugins/test-drawer/src/components/index.ts b/workspaces/quickstart/plugins/test-drawer/src/components/index.ts new file mode 100644 index 0000000000..422cffccf6 --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/src/components/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Red Hat, Inc. + * + * 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. + */ + +export { + TestDrawerContext, + useTestDrawerContext, +} from './TestDrawerContext'; +export type { TestDrawerContextType } from './TestDrawerContext'; +export { TestDrawerContent } from './TestDrawerContent'; +export { TestDrawerProvider } from './TestDrawerProvider'; +export { TestDrawerButton } from './TestDrawerButton'; +export { DrawerComponent } from './DrawerComponent'; + diff --git a/workspaces/quickstart/plugins/test-drawer/src/index.ts b/workspaces/quickstart/plugins/test-drawer/src/index.ts new file mode 100644 index 0000000000..e0f2fd8a04 --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/src/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Red Hat, Inc. + * + * 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. + */ + +export { + testDrawerPlugin, + TestDrawerProvider, + TestDrawerContent, + TestDrawerButton, +} from './plugin'; + +export { + TestDrawerContext, + useTestDrawerContext, +} from './components'; +export type { TestDrawerContextType } from './components'; + diff --git a/workspaces/quickstart/plugins/test-drawer/src/plugin.ts b/workspaces/quickstart/plugins/test-drawer/src/plugin.ts new file mode 100644 index 0000000000..84ac8dc5e3 --- /dev/null +++ b/workspaces/quickstart/plugins/test-drawer/src/plugin.ts @@ -0,0 +1,77 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { + createPlugin, + createComponentExtension, +} from '@backstage/core-plugin-api'; + +/** + * Test Drawer Plugin + * + * @public + */ +export const testDrawerPlugin = createPlugin({ + id: 'test-drawer', +}); + +/** + * Test Drawer Provider component extension + * + * @public + */ +export const TestDrawerProvider = testDrawerPlugin.provide( + createComponentExtension({ + name: 'TestDrawerProvider', + component: { + lazy: () => + import('./components/TestDrawerProvider').then( + m => m.TestDrawerProvider, + ), + }, + }), +); + +/** + * Test Drawer Content component extension + * + * @public + */ +export const TestDrawerContent = testDrawerPlugin.provide( + createComponentExtension({ + name: 'TestDrawerContent', + component: { + lazy: () => + import('./components/TestDrawerContent').then(m => m.TestDrawerContent), + }, + }), +); + +/** + * Test Drawer Button component extension + * + * @public + */ +export const TestDrawerButton = testDrawerPlugin.provide( + createComponentExtension({ + name: 'TestDrawerButton', + component: { + lazy: () => + import('./components/TestDrawerButton').then(m => m.TestDrawerButton), + }, + }), +); + diff --git a/workspaces/quickstart/yarn.lock b/workspaces/quickstart/yarn.lock index 7cc04947e2..bc220e164f 100644 --- a/workspaces/quickstart/yarn.lock +++ b/workspaces/quickstart/yarn.lock @@ -10996,6 +10996,27 @@ __metadata: languageName: node linkType: hard +"@red-hat-developer-hub/backstage-plugin-application-drawer@^0.1.0, @red-hat-developer-hub/backstage-plugin-application-drawer@workspace:^, @red-hat-developer-hub/backstage-plugin-application-drawer@workspace:plugins/application-drawer": + version: 0.0.0-use.local + resolution: "@red-hat-developer-hub/backstage-plugin-application-drawer@workspace:plugins/application-drawer" + dependencies: + "@backstage/cli": ^0.34.5 + "@backstage/core-components": ^0.18.3 + "@backstage/core-plugin-api": ^1.12.0 + "@backstage/dev-utils": ^1.1.17 + "@backstage/test-utils": ^1.7.13 + "@backstage/theme": ^0.7.0 + "@mui/icons-material": 5.18.0 + "@mui/material": 5.18.0 + "@red-hat-developer-hub/backstage-plugin-theme": ^0.11.0 + "@testing-library/jest-dom": ^6.0.0 + "@testing-library/react": ^14.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + languageName: unknown + linkType: soft + "@red-hat-developer-hub/backstage-plugin-global-header@npm:^1.17.1": version: 1.17.1 resolution: "@red-hat-developer-hub/backstage-plugin-global-header@npm:1.17.1" @@ -11044,6 +11065,7 @@ __metadata: "@backstage/theme": ^0.7.0 "@mui/icons-material": 5.18.0 "@mui/material": 5.18.0 + "@red-hat-developer-hub/backstage-plugin-application-drawer": ^0.1.0 "@red-hat-developer-hub/backstage-plugin-theme": ^0.11.0 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 @@ -11056,6 +11078,28 @@ __metadata: languageName: unknown linkType: soft +"@red-hat-developer-hub/backstage-plugin-test-drawer@workspace:^, @red-hat-developer-hub/backstage-plugin-test-drawer@workspace:plugins/test-drawer": + version: 0.0.0-use.local + resolution: "@red-hat-developer-hub/backstage-plugin-test-drawer@workspace:plugins/test-drawer" + dependencies: + "@backstage/cli": ^0.34.5 + "@backstage/core-components": ^0.18.3 + "@backstage/core-plugin-api": ^1.12.0 + "@backstage/dev-utils": ^1.1.17 + "@backstage/test-utils": ^1.7.13 + "@backstage/theme": ^0.7.0 + "@mui/icons-material": 5.18.0 + "@mui/material": 5.18.0 + "@red-hat-developer-hub/backstage-plugin-application-drawer": ^0.1.0 + "@red-hat-developer-hub/backstage-plugin-theme": ^0.11.0 + "@testing-library/jest-dom": ^6.0.0 + "@testing-library/react": ^14.0.0 + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + languageName: unknown + linkType: soft + "@red-hat-developer-hub/backstage-plugin-theme@npm:^0.11.0": version: 0.11.0 resolution: "@red-hat-developer-hub/backstage-plugin-theme@npm:0.11.0" @@ -15318,8 +15362,10 @@ __metadata: "@mui/icons-material": 5.18.0 "@mui/material": 5.18.0 "@playwright/test": 1.57.0 + "@red-hat-developer-hub/backstage-plugin-application-drawer": "workspace:^" "@red-hat-developer-hub/backstage-plugin-global-header": ^1.17.1 "@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^" + "@red-hat-developer-hub/backstage-plugin-test-drawer": "workspace:^" "@red-hat-developer-hub/backstage-plugin-theme": ^0.11.0 "@testing-library/dom": ^9.0.0 "@testing-library/jest-dom": ^6.0.0 From ed346015339835e52451b5a6e7cc3e2b704bfe1c Mon Sep 17 00:00:00 2001 From: Debsmita Santra Date: Mon, 22 Dec 2025 22:59:36 +0530 Subject: [PATCH 2/2] expose partial drawer context for ApplicationDrawer --- .../src/components/FloatingButton.tsx | 6 +- .../src/components/Root/ApplicationDrawer.tsx | 178 ++++++++++++++---- .../src/components/Root}/ResizableDrawer.tsx | 2 +- .../packages/app/src/components/Root/Root.tsx | 146 +++++++------- .../plugins/application-drawer/package.json | 58 ------ .../components/ApplicationDrawerContext.tsx | 73 ------- .../components/ApplicationDrawerProvider.tsx | 81 -------- .../src/components/index.ts | 24 --- .../plugins/application-drawer/src/index.ts | 19 -- .../plugins/application-drawer/src/plugin.ts | 27 --- .../plugins/quickstart/dev/index.tsx | 15 +- .../components/QuickstartDrawerProvider.tsx | 33 +--- .../QuickstartDrawerStateExposer.tsx | 68 +++++++ .../plugins/quickstart/src/index.ts | 1 + .../plugins/quickstart/src/plugin.ts | 17 ++ .../plugins/test-drawer/dev/index.tsx | 15 +- .../src/components/DrawerComponent.tsx | 2 +- .../src/components/TestDrawerButton.tsx | 66 +++++-- .../src/components/TestDrawerProvider.tsx | 34 +--- .../src/components/TestDrawerStateExposer.tsx | 68 +++++++ .../test-drawer/src/components/index.ts | 7 +- .../plugins/test-drawer/src/index.ts | 2 +- .../plugins/test-drawer/src/plugin.ts | 16 ++ 23 files changed, 466 insertions(+), 492 deletions(-) rename workspaces/quickstart/{plugins/application-drawer/src/components => packages/app/src/components/Root}/ResizableDrawer.tsx (99%) delete mode 100644 workspaces/quickstart/plugins/application-drawer/package.json delete mode 100644 workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerContext.tsx delete mode 100644 workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerProvider.tsx delete mode 100644 workspaces/quickstart/plugins/application-drawer/src/components/index.ts delete mode 100644 workspaces/quickstart/plugins/application-drawer/src/index.ts delete mode 100644 workspaces/quickstart/plugins/application-drawer/src/plugin.ts create mode 100644 workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerStateExposer.tsx create mode 100644 workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerStateExposer.tsx diff --git a/workspaces/global-floating-action-button/plugins/global-floating-action-button/src/components/FloatingButton.tsx b/workspaces/global-floating-action-button/plugins/global-floating-action-button/src/components/FloatingButton.tsx index 0a85787cd1..443a481802 100644 --- a/workspaces/global-floating-action-button/plugins/global-floating-action-button/src/components/FloatingButton.tsx +++ b/workspaces/global-floating-action-button/plugins/global-floating-action-button/src/components/FloatingButton.tsx @@ -31,10 +31,10 @@ const useStyles = makeStyles(theme => ({ right: `calc(${theme?.spacing?.(2) ?? '16px'} + 1.5em)`, alignItems: 'end', - // When quickstart drawer is open, adjust margin - '.quickstart-drawer-open &': { + // When drawer is docked, adjust margin + '.docked-drawer-open &': { transition: 'margin-right 0.3s ease', - marginRight: 'var(--quickstart-drawer-width, 500px) ', + marginRight: 'var(--docked-drawer-width, 500px) ', }, }, 'bottom-left': { diff --git a/workspaces/quickstart/packages/app/src/components/Root/ApplicationDrawer.tsx b/workspaces/quickstart/packages/app/src/components/Root/ApplicationDrawer.tsx index 8543907bbe..91ef3aa54f 100644 --- a/workspaces/quickstart/packages/app/src/components/Root/ApplicationDrawer.tsx +++ b/workspaces/quickstart/packages/app/src/components/Root/ApplicationDrawer.tsx @@ -14,12 +14,40 @@ * limitations under the License. */ -import { ComponentType } from 'react'; import { - useApplicationDrawerContext, - ResizableDrawer, -} from '@red-hat-developer-hub/backstage-plugin-application-drawer'; + ComponentType, + useState, + useCallback, + useMemo, + useEffect, +} from 'react'; +import { ResizableDrawer } from './ResizableDrawer'; +/** + * Partial drawer state exposed by drawer plugins + * + * @public + */ +export interface DrawerPartialState { + id: string; + isDrawerOpen: boolean; + drawerWidth: number; + setDrawerWidth: (width: number) => void; +} + +/** + * Props for drawer state exposer components + * + * @public + */ +export interface DrawerStateExposerProps { + onStateChange: (state: DrawerPartialState) => void; + onUnmount?: (id: string) => void; +} + +/** + * Drawer content configuration + */ type DrawerContentType = { id: string; Component: ComponentType; @@ -27,36 +55,122 @@ type DrawerContentType = { resizable?: boolean; }; +/** + * State exposer component type + */ +type StateExposerType = { + Component: ComponentType; +}; + +export interface ApplicationDrawerProps { + /** + * Array of drawer content configurations + * Maps drawer IDs to their content components + */ + drawerContents: DrawerContentType[]; + /** + * Array of state exposer components from drawer plugins + * These are typically mounted via `application/drawer-state` mount point + * + * In RHDH dynamic plugins, this would come from: + * ```yaml + * mountPoints: + * - mountPoint: application/drawer-state + * importName: TestDrawerStateExposer + * ``` + */ + stateExposers?: StateExposerType[]; +} + export const ApplicationDrawer = ({ drawerContents, -}: { - drawerContents: DrawerContentType[]; -}) => { - const { getDrawers } = useApplicationDrawerContext(); - - // Get active drawer - compute fresh each render since we use refs - const drawers = getDrawers(); - const activeDrawer = drawers - .filter(p => p.isDrawerOpen) - .map(p => { - const content = drawerContents.find(c => c.id === p.id); - if (!content) return null; - return { ...p, ...content }; - }) - .filter(Boolean) - .sort((a, b) => (b?.priority ?? -1) - (a?.priority ?? -1))[0]; - - if (!activeDrawer) return null; - - const { Component, resizable, drawerWidth, setDrawerWidth } = activeDrawer; + stateExposers = [], +}: ApplicationDrawerProps) => { + // Collect drawer states from all state exposers + const [drawerStates, setDrawerStates] = useState< + Record + >({}); + + // Callback for state exposers to report their state + const handleStateChange = useCallback((state: DrawerPartialState) => { + setDrawerStates(prev => { + // Only update if something actually changed + const existing = prev[state.id]; + if ( + existing && + existing.isDrawerOpen === state.isDrawerOpen && + existing.drawerWidth === state.drawerWidth && + existing.setDrawerWidth === state.setDrawerWidth + ) { + return prev; + } + return { ...prev, [state.id]: state }; + }); + }, []); + + // Convert states record to array + const statesArray = useMemo( + () => Object.values(drawerStates), + [drawerStates], + ); + + // Get active drawer - find the open drawer with highest priority + const activeDrawer = useMemo(() => { + return statesArray + .filter(state => state.isDrawerOpen) + .map(state => { + const content = drawerContents.find(c => c.id === state.id); + if (!content) return null; + return { ...state, ...content }; + }) + .filter(Boolean) + .sort((a, b) => (b?.priority ?? -1) - (a?.priority ?? -1))[0]; + }, [statesArray, drawerContents]); + + // Manage CSS classes and variables for layout adjustments + useEffect(() => { + if (activeDrawer) { + const className = `docked-drawer-open`; + const cssVar = `--docked-drawer-width`; + + document.body.classList.add(className); + document.body.style.setProperty(cssVar, `${activeDrawer.drawerWidth}px`); + + return () => { + document.body.classList.remove(className); + document.body.style.removeProperty(cssVar); + }; + } + return undefined; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeDrawer?.id, activeDrawer?.drawerWidth]); + + // Wrapper to handle the width change callback type + const handleWidthChange = useCallback( + (width: number) => { + activeDrawer?.setDrawerWidth(width); + }, + [activeDrawer], + ); + return ( - - - + <> + {/* Render all state exposers - they return null but report their state */} + {stateExposers.map(({ Component }, index) => ( + + ))} + + {/* Render the active drawer */} + {activeDrawer && ( + + + + )} + ); }; diff --git a/workspaces/quickstart/plugins/application-drawer/src/components/ResizableDrawer.tsx b/workspaces/quickstart/packages/app/src/components/Root/ResizableDrawer.tsx similarity index 99% rename from workspaces/quickstart/plugins/application-drawer/src/components/ResizableDrawer.tsx rename to workspaces/quickstart/packages/app/src/components/Root/ResizableDrawer.tsx index 3b17a3b015..e67e837a44 100644 --- a/workspaces/quickstart/plugins/application-drawer/src/components/ResizableDrawer.tsx +++ b/workspaces/quickstart/packages/app/src/components/Root/ResizableDrawer.tsx @@ -154,4 +154,4 @@ export const ResizableDrawer = (props: ResizableDrawerProps) => { ); -}; +}; \ No newline at end of file diff --git a/workspaces/quickstart/packages/app/src/components/Root/Root.tsx b/workspaces/quickstart/packages/app/src/components/Root/Root.tsx index 19e3919679..6161d0d420 100644 --- a/workspaces/quickstart/packages/app/src/components/Root/Root.tsx +++ b/workspaces/quickstart/packages/app/src/components/Root/Root.tsx @@ -47,15 +47,18 @@ import { GlobalHeaderComponent, } from '@red-hat-developer-hub/backstage-plugin-global-header'; import Box from '@mui/material/Box'; -import { QuickstartDrawerProvider } from '@red-hat-developer-hub/backstage-plugin-quickstart'; +import { + QuickstartDrawerProvider, + QuickstartDrawerContent, + QuickstartDrawerStateExposer, +} from '@red-hat-developer-hub/backstage-plugin-quickstart'; import { QuickstartSidebarItem } from './QuickstartSidebarItem'; import { Administration } from '@backstage-community/plugin-rbac'; -import { QuickstartDrawerContent } from '@red-hat-developer-hub/backstage-plugin-quickstart'; import { TestDrawerContent, TestDrawerProvider, + TestDrawerStateExposer, } from '@red-hat-developer-hub/backstage-plugin-test-drawer'; -import { ApplicationDrawerProvider } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; import { ApplicationDrawer } from './ApplicationDrawer'; import { TestDrawerSidebarItem } from './TestDrawerSidebarItem'; @@ -96,15 +99,9 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { // This code exists similarly in RHDH: // https://github.com/redhat-developer/rhdh/blob/main/packages/app/src/components/Root/Root.tsx#L159-L165 // https://github.com/redhat-developer/rhdh/blob/main/packages/app/src/components/ErrorPages/ErrorPage.tsx#L54-L59 - 'body.quickstart-drawer-open #sidebar&': { - "> div > main[class*='BackstagePage-root']": { - marginRight: 'calc(var(--quickstart-drawer-width, 500px) + 1.5em)', - transition: 'margin-right 0.3s ease', - }, - }, - 'body.test-drawer-open #sidebar&': { + 'body.docked-drawer-open #sidebar&': { "> div > main[class*='BackstagePage-root']": { - marginRight: 'calc(var(--test-drawer-width, 500px) + 1.5em)', + marginRight: 'calc(var(--docked-drawer-width, 500px) + 1.5em)', transition: 'margin-right 0.3s ease', }, }, @@ -114,68 +111,73 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { - - - - - - } to="/search"> - - - - }> - {/* Global nav, not org-specific */} - - - - - - {/* End global nav */} - - - - {/* Items in this group will be scrollable if they run out of space */} - - - - - + + + + + } to="/search"> + + + + }> + {/* Global nav, not org-specific */} + + + + + + {/* End global nav */} - } - to="/settings" - > - - - - {children} - - - - + + + {/* Items in this group will be scrollable if they run out of space */} + + + + + + + } + to="/settings" + > + + + + {children} + + + ); diff --git a/workspaces/quickstart/plugins/application-drawer/package.json b/workspaces/quickstart/plugins/application-drawer/package.json deleted file mode 100644 index 9b504bb75f..0000000000 --- a/workspaces/quickstart/plugins/application-drawer/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@red-hat-developer-hub/backstage-plugin-application-drawer", - "version": "0.1.0", - "license": "Apache-2.0", - "main": "src/index.ts", - "types": "src/index.ts", - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "https://github.com/redhat-developer/rhdh-plugins", - "directory": "workspaces/quickstart/plugins/application-drawer" - }, - "backstage": { - "role": "frontend-plugin", - "pluginId": "application-drawer", - "pluginPackages": [ - "@red-hat-developer-hub/backstage-plugin-application-drawer" - ] - }, - "sideEffects": false, - "scripts": { - "start": "backstage-cli package start", - "build": "backstage-cli package build", - "lint": "backstage-cli package lint", - "test": "backstage-cli package test", - "clean": "backstage-cli package clean", - "prepack": "backstage-cli package prepack", - "postpack": "backstage-cli package postpack" - }, - "dependencies": { - "@backstage/core-components": "^0.18.3", - "@backstage/core-plugin-api": "^1.12.0", - "@backstage/theme": "^0.7.0", - "@mui/icons-material": "5.18.0", - "@mui/material": "5.18.0", - "@red-hat-developer-hub/backstage-plugin-theme": "^0.11.0" - }, - "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" - }, - "devDependencies": { - "@backstage/cli": "^0.34.5", - "@backstage/dev-utils": "^1.1.17", - "@backstage/test-utils": "^1.7.13", - "@testing-library/jest-dom": "^6.0.0", - "@testing-library/react": "^14.0.0", - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" - }, - "exports": { - ".": "./src/index.ts", - "./package.json": "./package.json" - }, - "files": [ - "dist" - ] -} diff --git a/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerContext.tsx b/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerContext.tsx deleted file mode 100644 index 6acd24fc6d..0000000000 --- a/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerContext.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * 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 { createContext, useContext } from 'react'; - -/** - * Type for a drawer context - * - * @public - */ -export type DrawerContext = { - id: string; - isDrawerOpen: boolean; - drawerWidth?: number; - setDrawerWidth?: React.Dispatch>; -}; - -/** - * Type for ApplicationDrawerContext - * - * @public - */ -export type ApplicationDrawerContextType = { - addDrawerContext: ( - id: string, - context: { - isDrawerOpen: boolean; - drawerWidth?: number; - setDrawerWidth?: React.Dispatch>; - }, - ) => void; - getDrawers: () => DrawerContext[]; -}; - -/** - * Context for the Application Drawer - * - * Allows drawer providers (Quickstart, Lightspeed, etc.) to register - * themselves so ApplicationDrawer can render the active one. - * - * @public - */ -export const ApplicationDrawerContext = createContext< - ApplicationDrawerContextType | undefined ->(undefined); - -/** - * Hook to access the ApplicationDrawerContext - * - * @public - */ -export const useApplicationDrawerContext = (): ApplicationDrawerContextType => { - const context = useContext(ApplicationDrawerContext); - if (!context) { - throw new Error( - 'useApplicationDrawerContext must be used within an ApplicationDrawerProvider', - ); - } - return context; -}; diff --git a/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerProvider.tsx b/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerProvider.tsx deleted file mode 100644 index 2c07e42602..0000000000 --- a/workspaces/quickstart/plugins/application-drawer/src/components/ApplicationDrawerProvider.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * 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 { PropsWithChildren, useCallback, useState, useMemo } from 'react'; -import { - ApplicationDrawerContext, - DrawerContext, -} from './ApplicationDrawerContext'; - -/** - * Provider component for the ApplicationDrawer functionality - * - * This provider should wrap all drawer providers (QuickstartDrawerProvider, - * TestDrawerProvider, etc.) to allow them to register themselves. - * - * @public - */ -export const ApplicationDrawerProvider = ({ children }: PropsWithChildren) => { - const [drawers, setDrawers] = useState([]); - - const addDrawerContext = useCallback( - ( - id: string, - context: { - isDrawerOpen: boolean; - drawerWidth?: number; - setDrawerWidth?: React.Dispatch>; - }, - ) => { - setDrawers(prev => { - const existingIndex = prev.findIndex(d => d.id === id); - const newContext: DrawerContext = { id, ...context }; - - if (existingIndex !== -1) { - // Check if anything actually changed - const existing = prev[existingIndex]; - if ( - existing.isDrawerOpen === newContext.isDrawerOpen && - existing.drawerWidth === newContext.drawerWidth - ) { - return prev; - } - const updated = [...prev]; - updated[existingIndex] = newContext; - return updated; - } - return [...prev, newContext]; - }); - }, - [], - ); - - const getDrawers = useCallback(() => drawers, [drawers]); - - const contextValue = useMemo( - () => ({ - addDrawerContext, - getDrawers, - }), - [addDrawerContext, getDrawers], - ); - - return ( - - {children} - - ); -}; diff --git a/workspaces/quickstart/plugins/application-drawer/src/components/index.ts b/workspaces/quickstart/plugins/application-drawer/src/components/index.ts deleted file mode 100644 index 6adffc407c..0000000000 --- a/workspaces/quickstart/plugins/application-drawer/src/components/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * 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. - */ - -export { useApplicationDrawerContext } from './ApplicationDrawerContext'; -export type { - ApplicationDrawerContextType, - DrawerContext, -} from './ApplicationDrawerContext'; -export { ApplicationDrawerProvider } from './ApplicationDrawerProvider'; -export { ResizableDrawer } from './ResizableDrawer'; -export type { ResizableDrawerProps } from './ResizableDrawer'; diff --git a/workspaces/quickstart/plugins/application-drawer/src/index.ts b/workspaces/quickstart/plugins/application-drawer/src/index.ts deleted file mode 100644 index 1970cc98bc..0000000000 --- a/workspaces/quickstart/plugins/application-drawer/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * 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. - */ - -export * from './plugin'; -export * from './components'; - diff --git a/workspaces/quickstart/plugins/application-drawer/src/plugin.ts b/workspaces/quickstart/plugins/application-drawer/src/plugin.ts deleted file mode 100644 index 4f9d39abe2..0000000000 --- a/workspaces/quickstart/plugins/application-drawer/src/plugin.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * 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 { createPlugin } from '@backstage/core-plugin-api'; - -/** - * The application-drawer plugin - * - * @public - */ -export const applicationDrawerPlugin = createPlugin({ - id: 'application-drawer', -}); - diff --git a/workspaces/quickstart/plugins/quickstart/dev/index.tsx b/workspaces/quickstart/plugins/quickstart/dev/index.tsx index 515a2a51d2..6e4f575190 100644 --- a/workspaces/quickstart/plugins/quickstart/dev/index.tsx +++ b/workspaces/quickstart/plugins/quickstart/dev/index.tsx @@ -27,7 +27,6 @@ import Button from '@mui/material/Button'; import { getAllThemes } from '@red-hat-developer-hub/backstage-plugin-theme'; import { QuickstartDrawerContent } from '../src/components/QuickstartDrawerContent'; import { DrawerComponent } from '../src/components/DrawerComponent'; -import { ApplicationDrawerProvider } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; const QuickstartTestPageContent = () => { const { openDrawer, closeDrawer, isDrawerOpen } = @@ -100,14 +99,12 @@ const QuickstartTestPageContent = () => { }; const QuickstartTestPage = () => ( - - - - - - - - + + + + + + ); createDevApp() diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerProvider.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerProvider.tsx index a6cdf96ced..825a023bf0 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerProvider.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartDrawerProvider.tsx @@ -23,12 +23,13 @@ import { import Snackbar from '@mui/material/Snackbar'; import CloseIcon from '@mui/icons-material/Close'; import IconButton from '@mui/material/IconButton'; -import { useApplicationDrawerContext } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; import { QuickstartDrawerContext } from './QuickstartDrawerContext'; import { QuickstartItemData } from '../types'; import { filterQuickstartItemsByRole } from '../utils'; import { useQuickstartRole } from '../hooks/useQuickstartRole'; +const DRAWER_ID = 'quickstart'; + /** * Provider component for the Quickstart Drawer functionality * @public @@ -41,38 +42,10 @@ export const QuickstartDrawerProvider = ({ children }: PropsWithChildren) => { const [userKey, setUserKey] = useState('guest'); const identityApi = useApi(identityApiRef); const configApi = useApi(configApiRef); - const { addDrawerContext } = useApplicationDrawerContext(); // Determine role once at provider level to avoid re-fetching on drawer open/close const { isLoading: roleLoading, userRole } = useQuickstartRole(); - useEffect(() => { - addDrawerContext('quickstart', { - isDrawerOpen, - drawerWidth, - setDrawerWidth, - }); - }, [addDrawerContext, drawerWidth, isDrawerOpen]); - - // Single useEffect - sets class on document.body - useEffect(() => { - if (isDrawerOpen) { - document.body.classList.add('quickstart-drawer-open'); - document.body.style.setProperty( - '--quickstart-drawer-width', - `${drawerWidth}px`, - ); - } else { - document.body.classList.remove('quickstart-drawer-open'); - document.body.style.removeProperty('--quickstart-drawer-width'); - } - - return () => { - document.body.classList.remove('quickstart-drawer-open'); - document.body.style.removeProperty('--quickstart-drawer-width'); - }; - }, [isDrawerOpen, drawerWidth]); - // Resolve the current user's identity to scope localStorage keys per user useEffect(() => { let cancelled = false; @@ -199,7 +172,7 @@ export const QuickstartDrawerProvider = ({ children }: PropsWithChildren) => { return ( void; +}; + +/** + * Props for drawer state exposer components + * + * @public + */ +export type DrawerStateExposerProps = { + /** + * Callback called whenever the drawer state changes + */ + onStateChange: (state: DrawerPartialState) => void; +}; + +/** + * This exposes Quickstart Drawer's partial context to the ApplicationDrawer + * + * It reads the QuickstartDrawerContext and calls the onStateChange callback with the + * partial state (id, isDrawerOpen, drawerWidth, setDrawerWidth). + * + * @public + */ +export const QuickstartDrawerStateExposer = ({ + onStateChange, +}: DrawerStateExposerProps) => { + const { id, isDrawerOpen, drawerWidth, setDrawerWidth } = + useQuickstartDrawerContext(); + + useEffect(() => { + onStateChange({ + id, + isDrawerOpen, + drawerWidth, + setDrawerWidth, + }); + }, [id, isDrawerOpen, drawerWidth, onStateChange, setDrawerWidth]); + + return null; +}; diff --git a/workspaces/quickstart/plugins/quickstart/src/index.ts b/workspaces/quickstart/plugins/quickstart/src/index.ts index a9e3cfb234..55413c0272 100644 --- a/workspaces/quickstart/plugins/quickstart/src/index.ts +++ b/workspaces/quickstart/plugins/quickstart/src/index.ts @@ -26,6 +26,7 @@ export * from './plugin'; export { useQuickstartDrawerContext } from './hooks/useQuickstartDrawerContext'; export type { QuickstartDrawerContextType } from './components/QuickstartDrawerContext'; +export { QuickstartDrawerStateExposer } from './components/QuickstartDrawerStateExposer'; /** * @public */ diff --git a/workspaces/quickstart/plugins/quickstart/src/plugin.ts b/workspaces/quickstart/plugins/quickstart/src/plugin.ts index b797b9ecb6..2459bf7b2d 100644 --- a/workspaces/quickstart/plugins/quickstart/src/plugin.ts +++ b/workspaces/quickstart/plugins/quickstart/src/plugin.ts @@ -89,3 +89,20 @@ export const QuickstartButton: React.ComponentType = }, }), ); + +/** + * Quickstart Drawer State Exposer exposes its drawer state + * + * @public + */ +export const QuickstartDrawerStateExposer = quickstartPlugin.provide( + createComponentExtension({ + name: 'QuickstartDrawerStateExposer', + component: { + lazy: () => + import('./components/QuickstartDrawerStateExposer').then( + m => m.QuickstartDrawerStateExposer, + ), + }, + }), +); diff --git a/workspaces/quickstart/plugins/test-drawer/dev/index.tsx b/workspaces/quickstart/plugins/test-drawer/dev/index.tsx index 10213d6912..2bcf8010de 100644 --- a/workspaces/quickstart/plugins/test-drawer/dev/index.tsx +++ b/workspaces/quickstart/plugins/test-drawer/dev/index.tsx @@ -25,7 +25,6 @@ import { useTestDrawerContext, } from '../src'; import { DrawerComponent } from '../src/components'; -import { ApplicationDrawerProvider } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; const TestPage = () => { const { toggleDrawer, isDrawerOpen, drawerWidth } = useTestDrawerContext(); @@ -67,14 +66,12 @@ const TestPage = () => { }; const DevPage = () => ( - - - - - - - - + + + + + + ); createDevApp() diff --git a/workspaces/quickstart/plugins/test-drawer/src/components/DrawerComponent.tsx b/workspaces/quickstart/plugins/test-drawer/src/components/DrawerComponent.tsx index 0dd552eb16..43620f28a0 100644 --- a/workspaces/quickstart/plugins/test-drawer/src/components/DrawerComponent.tsx +++ b/workspaces/quickstart/plugins/test-drawer/src/components/DrawerComponent.tsx @@ -15,8 +15,8 @@ */ import { PropsWithChildren } from 'react'; -import { ResizableDrawer } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; import { useTestDrawerContext } from './TestDrawerContext'; +import { ResizableDrawer } from '../../../../packages/app/src/components/Root/ResizableDrawer'; export const DrawerComponent = ({ children }: PropsWithChildren) => { const { isDrawerOpen, drawerWidth, setDrawerWidth } = useTestDrawerContext(); diff --git a/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerButton.tsx b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerButton.tsx index f279c3bc6a..2fd840402b 100644 --- a/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerButton.tsx +++ b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerButton.tsx @@ -13,32 +13,66 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import IconButton from '@mui/material/IconButton'; -import Tooltip from '@mui/material/Tooltip'; -import ViewSidebarIcon from '@mui/icons-material/ViewSidebar'; +import MenuItem from '@mui/material/MenuItem'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { useTheme } from '@mui/material/styles'; import { useTestDrawerContext } from './TestDrawerContext'; +import { useCallback } from 'react'; /** * Button component to toggle the Test Drawer * - * Can be used in the header or any other location to trigger the drawer. + * Can be used in the global header help dropdown * * @public */ -export const TestDrawerButton = () => { - const { toggleDrawer, isDrawerOpen } = useTestDrawerContext(); +export const TestDrawerButton = ({ + onClick = () => {}, +}: { + onClick: () => void; +}) => { + const { toggleDrawer } = useTestDrawerContext(); + const theme = useTheme(); + + const handleClick = useCallback(() => { + toggleDrawer(); + onClick(); + }, [toggleDrawer, onClick]); return ( - - + - - - + + + + Test Drawer + + + + + ); }; - diff --git a/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerProvider.tsx b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerProvider.tsx index 8b23d59c04..2b8352c66a 100644 --- a/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerProvider.tsx +++ b/workspaces/quickstart/plugins/test-drawer/src/components/TestDrawerProvider.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { PropsWithChildren, useState, useCallback, useEffect } from 'react'; -import { useApplicationDrawerContext } from '@red-hat-developer-hub/backstage-plugin-application-drawer'; +import { PropsWithChildren, useState, useCallback } from 'react'; import { TestDrawerContext } from './TestDrawerContext'; +const DRAWER_ID = 'test-drawer'; const DEFAULT_DRAWER_WIDTH = 400; const MIN_DRAWER_WIDTH = 300; const MAX_DRAWER_WIDTH = 800; @@ -31,34 +31,6 @@ export const TestDrawerProvider = ({ children }: PropsWithChildren) => { const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [drawerWidth, setDrawerWidth] = useState(DEFAULT_DRAWER_WIDTH); - const { addDrawerContext } = useApplicationDrawerContext(); - - useEffect(() => { - addDrawerContext('test-drawer', { - isDrawerOpen, - drawerWidth, - setDrawerWidth, - }); - }, [addDrawerContext, isDrawerOpen, drawerWidth]); - - useEffect(() => { - if (isDrawerOpen) { - document.body.classList.add('test-drawer-open'); - document.body.style.setProperty( - '--test-drawer-width', - `${drawerWidth}px`, - ); - } else { - document.body.classList.remove('test-drawer-open'); - document.body.style.removeProperty('--test-drawer-width'); - } - - return () => { - document.body.classList.remove('test-drawer-open'); - document.body.style.removeProperty('--test-drawer-width'); - }; - }, [isDrawerOpen, drawerWidth]); - const toggleDrawer = useCallback(() => { setIsDrawerOpen(prev => !prev); }, []); @@ -75,7 +47,7 @@ export const TestDrawerProvider = ({ children }: PropsWithChildren) => { return ( void; +}; + +/** + * Props for drawer state exposer components + * + * @public + */ +export type DrawerStateExposerProps = { + /** + * Callback called whenever the drawer state changes + */ + onStateChange: (state: DrawerState) => void; +}; + +/** + * This exposes TestDrawer's partial context to the ApplicationDrawer + * + * It reads the TestDrawerContext and calls the onStateChange callback with the + * partial state (id, isDrawerOpen, drawerWidth, setDrawerWidth). + * + * @public + */ +export const TestDrawerStateExposer = ({ + onStateChange, +}: DrawerStateExposerProps) => { + const { id, isDrawerOpen, drawerWidth, setDrawerWidth } = + useTestDrawerContext(); + + useEffect(() => { + onStateChange({ + id, + isDrawerOpen, + drawerWidth, + setDrawerWidth, + }); + }, [id, isDrawerOpen, drawerWidth, onStateChange, setDrawerWidth]); + + return null; +}; diff --git a/workspaces/quickstart/plugins/test-drawer/src/components/index.ts b/workspaces/quickstart/plugins/test-drawer/src/components/index.ts index 422cffccf6..bfda8c19fd 100644 --- a/workspaces/quickstart/plugins/test-drawer/src/components/index.ts +++ b/workspaces/quickstart/plugins/test-drawer/src/components/index.ts @@ -14,13 +14,10 @@ * limitations under the License. */ -export { - TestDrawerContext, - useTestDrawerContext, -} from './TestDrawerContext'; +export { TestDrawerContext, useTestDrawerContext } from './TestDrawerContext'; export type { TestDrawerContextType } from './TestDrawerContext'; export { TestDrawerContent } from './TestDrawerContent'; export { TestDrawerProvider } from './TestDrawerProvider'; export { TestDrawerButton } from './TestDrawerButton'; export { DrawerComponent } from './DrawerComponent'; - +export { TestDrawerStateExposer } from './TestDrawerStateExposer'; diff --git a/workspaces/quickstart/plugins/test-drawer/src/index.ts b/workspaces/quickstart/plugins/test-drawer/src/index.ts index e0f2fd8a04..eec03a32e4 100644 --- a/workspaces/quickstart/plugins/test-drawer/src/index.ts +++ b/workspaces/quickstart/plugins/test-drawer/src/index.ts @@ -24,6 +24,6 @@ export { export { TestDrawerContext, useTestDrawerContext, + TestDrawerStateExposer, } from './components'; export type { TestDrawerContextType } from './components'; - diff --git a/workspaces/quickstart/plugins/test-drawer/src/plugin.ts b/workspaces/quickstart/plugins/test-drawer/src/plugin.ts index 84ac8dc5e3..6dd233a959 100644 --- a/workspaces/quickstart/plugins/test-drawer/src/plugin.ts +++ b/workspaces/quickstart/plugins/test-drawer/src/plugin.ts @@ -75,3 +75,19 @@ export const TestDrawerButton = testDrawerPlugin.provide( }), ); +/** + * Test Drawer State Exposer exposes its drawer state + * + * @public + */ +export const TestDrawerStateExposer = testDrawerPlugin.provide( + createComponentExtension({ + name: 'TestDrawerStateExposer', + component: { + lazy: () => + import('./components/TestDrawerStateExposer').then( + m => m.TestDrawerStateExposer, + ), + }, + }), +);