Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions workspaces/app-defaults/.changeset/giant-geese-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-app-react': patch
---

Replace context based state with global store.
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,41 @@
} from '@backstage/core-components';
import { NavContentBlueprint } from '@backstage/plugin-app-react';
import { SidebarLogo } from './SidebarLogo';
import { useAppDrawer } from '@red-hat-developer-hub/backstage-plugin-app-react';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
import ChatIcon from '@material-ui/icons/Chat';
import HelpIcon from '@material-ui/icons/Help';
import { SidebarSearchModal } from '@backstage/plugin-search';
import { UserSettingsSignInAvatar } from '@backstage/plugin-user-settings';
import { NotificationsSidebarItem } from '@backstage/plugin-notifications';

const DrawerDemoItems = () => {
const { toggleDrawer } = useAppDrawer();
return (
<>
<SidebarItem
icon={() => <ChatIcon />}

Check warning on line 40 in workspaces/app-defaults/packages/app/src/modules/nav/Sidebar.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this component definition out of the parent component and pass data as props.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ1OZrBrHQBP3bJ2h4Ag&open=AZ1OZrBrHQBP3bJ2h4Ag&pullRequest=2691
text="Chat"
onClick={() => toggleDrawer('demo-chat')}
/>
<SidebarItem
icon={() => <HelpIcon />}

Check warning on line 45 in workspaces/app-defaults/packages/app/src/modules/nav/Sidebar.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this component definition out of the parent component and pass data as props.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ1OZrBrHQBP3bJ2h4Ah&open=AZ1OZrBrHQBP3bJ2h4Ah&pullRequest=2691
text="Help"
onClick={() => toggleDrawer('demo-help')}
/>
</>
);
};

export const SidebarContent = NavContentBlueprint.make({
params: {
component: ({ navItems }) => {
const nav = navItems.withComponent(item => (
<SidebarItem icon={() => item.icon} to={item.href} text={item.title} />
));

// Skipped items
nav.take('page:search'); // Using search modal instead
nav.take('page:search');

return (
<Sidebar>
Expand All @@ -56,6 +76,7 @@
</SidebarGroup>
<SidebarSpace />
<SidebarDivider />
<DrawerDemoItems />
<NotificationsSidebarItem />
<SidebarDivider />
<SidebarGroup
Expand Down
14 changes: 7 additions & 7 deletions workspaces/app-defaults/plugins/app-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ export default createApp({
});
```

This registers two extensions:

- `app-root-wrapper:app/drawer-provider` -- wraps the app with `AppDrawerProvider` context
- `app-root-element:app/drawer` -- renders the `ApplicationDrawer` outside the app layout
This registers a single wrapper extension (`app-root-wrapper:app/drawer`) that
renders the `ApplicationDrawer` around the app content and accepts drawer
content contributions via inputs. Drawer state is managed by a global singleton
store, so `useAppDrawer()` works from anywhere in the React tree without a
wrapping provider.

## Plugin Author Guide

Expand Down Expand Up @@ -138,13 +139,12 @@ function MyDrawerContent() {
### Main entry (`@red-hat-developer-hub/backstage-plugin-app-react`)

- `useAppDrawer` -- hook to control drawers
- `AppDrawerProvider` -- context provider (used by the module internally)
- `ApplicationDrawer` -- drawer renderer component
- `DrawerPanel` -- low-level MUI drawer wrapper
- `AppDrawerContent` / `AppDrawerApi` types
- `AppDrawerContent` / `AppDrawerApi` / `ApplicationDrawerProps` / `DrawerPanelProps` types

### Alpha entry (`@red-hat-developer-hub/backstage-plugin-app-react/alpha`)

- `appDrawerContentDataRef` -- extension data ref
- `AppDrawerContentBlueprint` -- blueprint for contributing drawers
- `appDrawerModule` -- frontend module (registers provider + renderer)
- `appDrawerModule` -- frontend module (registers the drawer wrapper extension)
1 change: 1 addition & 0 deletions workspaces/app-defaults/plugins/app-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@backstage/core-plugin-api": "^1.12.4",
"@backstage/frontend-plugin-api": "^0.15.1",
"@backstage/plugin-app-react": "^0.2.1",
"@backstage/version-bridge": "^1.0.12",
"@mui/material": "5.18.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ export const appDrawerContentDataRef: ConfigurableExtensionDataRef<
// @alpha
export const appDrawerModule: FrontendModule;

// @public
export const AppDrawerProvider: (input: {
children: React.ReactNode;
}) => JSX_2.Element;

// @public
export const ApplicationDrawer: (
input: ApplicationDrawerProps,
Expand Down
5 changes: 0 additions & 5 deletions workspaces/app-defaults/plugins/app-react/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ export interface AppDrawerContent {
resizable?: boolean;
}

// @public
export const AppDrawerProvider: (input: {
children: React.ReactNode;
}) => JSX_2.Element;

// @public
export const ApplicationDrawer: (
input: ApplicationDrawerProps,
Expand Down
5 changes: 2 additions & 3 deletions workspaces/app-defaults/plugins/app-react/src/alpha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
*/

/**
* New Frontend System extension APIs for the RHDH app drawer.
* New Frontend System extension APIs for the RHDH app shell.
*
* @packageDocumentation
*/

export * from './extensions';
export type * from './drawer';
export * from './drawer';

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,32 @@

import { render, screen, act } from '@testing-library/react';

import { AppDrawerProvider, useAppDrawer } from './AppDrawerContext';
import { useAppDrawer } from '../hooks/useAppDrawer';
import { drawerStore } from '../utils/drawerStore';
import { ApplicationDrawer } from './ApplicationDrawer';
import type { AppDrawerContent } from './types';
import type { AppDrawerContent } from '../types';

function OpenButton({ id }: { id: string }) {

Check warning on line 24 in workspaces/app-defaults/plugins/app-react/src/drawer/components/ApplicationDrawer.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ1OZrMVHQBP3bJ2h4Ai&open=AZ1OZrMVHQBP3bJ2h4Ai&pullRequest=2691
const { openDrawer } = useAppDrawer();
return <button onClick={() => openDrawer(id)}>Open {id}</button>;
}

function CloseButton({ id }: { id: string }) {

Check warning on line 29 in workspaces/app-defaults/plugins/app-react/src/drawer/components/ApplicationDrawer.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZ1OZrMVHQBP3bJ2h4Aj&open=AZ1OZrMVHQBP3bJ2h4Aj&pullRequest=2691
const { closeDrawer } = useAppDrawer();
return <button onClick={() => closeDrawer(id)}>Close {id}</button>;
}

function renderWithProvider(contents: AppDrawerContent[]) {
return render(
<AppDrawerProvider>
<ApplicationDrawer contents={contents}>
<OpenButton id="test-drawer" />
<CloseButton id="test-drawer" />
</ApplicationDrawer>
</AppDrawerProvider>,
<ApplicationDrawer contents={contents}>
<OpenButton id="test-drawer" />
<CloseButton id="test-drawer" />
</ApplicationDrawer>,
);
}

describe('ApplicationDrawer', () => {
beforeEach(() => drawerStore.reset());
afterEach(() => {
document.body.classList.remove('docked-drawer-open');
document.body.style.removeProperty('--docked-drawer-width');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

import { useEffect, useRef } from 'react';

import { useAppDrawer } from './AppDrawerContext';
import { useAppDrawer } from '../hooks/useAppDrawer';
import { DrawerPanel } from './DrawerPanel';
import type { AppDrawerContent } from './types';
import type { AppDrawerContent } from '../types';

const DEFAULT_WIDTH = 500;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { createExtensionDataRef } from '@backstage/frontend-plugin-api';

import type { AppDrawerContent } from '../drawer/types';
import type { AppDrawerContent } from '../types';

/**
* Extension data ref carrying drawer content from a plugin to the host.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,16 @@ import {
} from '@backstage/frontend-plugin-api';
import { AppRootWrapperBlueprint } from '@backstage/plugin-app-react';

import { AppDrawerProvider } from '../drawer/AppDrawerContext';
import { ApplicationDrawer } from '../drawer/ApplicationDrawer';
import { ApplicationDrawer } from '../components/ApplicationDrawer';
import { appDrawerContentDataRef } from './appDrawerContentDataRef';

/**
* Wrapper extension that provides the AppDrawerContext **and** renders the
* ApplicationDrawer inside it.
* Wrapper extension that renders the ApplicationDrawer around the app content.
*
* Uses AppRootWrapperBlueprint.makeWithOverrides to stay aligned with the
* blueprint API while adding a custom `drawers` input for content extensions.
*
* Because Backstage NFS renders app-root-element extensions as siblings
* (outside) of app-root-wrapper providers, we cannot use a separate
* app-root-element for the drawer. Combining both into one wrapper
* guarantees the drawer lives inside the context provider.
* Drawer state is managed by a global singleton store (see drawerStore.ts)
* rather than a React context provider.
*/
const appDrawerExtension = AppRootWrapperBlueprint.makeWithOverrides({
name: 'drawer',
Expand All @@ -45,18 +40,16 @@ const appDrawerExtension = AppRootWrapperBlueprint.makeWithOverrides({
const contents = inputs.drawers.map(d => d.get(appDrawerContentDataRef));
return originalFactory({
component: ({ children }) => (
<AppDrawerProvider>
<ApplicationDrawer contents={contents}>{children}</ApplicationDrawer>
</AppDrawerProvider>
<ApplicationDrawer contents={contents}>{children}</ApplicationDrawer>
),
});
},
});

/**
* Frontend module that provides the app drawer system.
* Registers a single wrapper extension that provides context and renders
* the drawer, plus accepts drawer content contributions via inputs.
* Registers a wrapper extension that renders the drawer and accepts
* drawer content contributions via inputs.
*
* @alpha
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,26 @@

import { renderHook, act } from '@testing-library/react';

import { AppDrawerProvider, useAppDrawer } from './AppDrawerContext';
import { useAppDrawer } from './useAppDrawer';
import { drawerStore } from '../utils/drawerStore';

function renderDrawerHook() {
return renderHook(() => useAppDrawer(), {
wrapper: ({ children }) => (
<AppDrawerProvider>{children}</AppDrawerProvider>
),
});
return renderHook(() => useAppDrawer());
}

describe('AppDrawerContext', () => {
describe('useAppDrawer outside provider', () => {
it('returns a no-op fallback', () => {
describe('useAppDrawer', () => {
beforeEach(() => drawerStore.reset());

describe('without provider', () => {
it('works without a wrapping provider (global store)', () => {
const { result } = renderHook(() => useAppDrawer());

expect(result.current.activeDrawerId).toBeNull();
expect(result.current.isOpen('any')).toBe(false);
expect(result.current.getWidth('any')).toBe(500);

act(() => result.current.openDrawer('test'));
expect(result.current.activeDrawerId).toBe('test');
});
});

Expand Down
Loading
Loading