Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/Unleash/unleash
Browse files Browse the repository at this point in the history
  • Loading branch information
pransh15 committed Oct 19, 2023
2 parents b5dd8f1 + b5d9bba commit d212917
Show file tree
Hide file tree
Showing 50 changed files with 1,417 additions and 346 deletions.
8 changes: 4 additions & 4 deletions frontend/src/component/App.tsx
Expand Up @@ -20,8 +20,8 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import MaintenanceBanner from './maintenance/MaintenanceBanner';
import { styled } from '@mui/material';
import { InitialRedirect } from './InitialRedirect';
import { InternalMessageBanners } from './messageBanners/internalMessageBanners/InternalMessageBanners';
import { ExternalMessageBanners } from './messageBanners/externalMessageBanners/ExternalMessageBanners';
import { InternalBanners } from './banners/internalBanners/InternalBanners';
import { ExternalBanners } from './banners/externalBanners/ExternalBanners';

const StyledContainer = styled('div')(() => ({
'& ul': {
Expand Down Expand Up @@ -65,8 +65,8 @@ export const App = () => {
)}
show={<MaintenanceBanner />}
/>
<ExternalMessageBanners />
<InternalMessageBanners />
<ExternalBanners />
<InternalBanners />
<StyledContainer>
<ToastRenderer />
<Routes>
Expand Down
Expand Up @@ -7,33 +7,26 @@ import {
import { styled, Icon, Link } from '@mui/material';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { useNavigate } from 'react-router-dom';
import { MessageBannerDialog } from './MessageBannerDialog/MessageBannerDialog';
import { BannerDialog } from './BannerDialog/BannerDialog';
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { BannerVariant, IMessageBanner } from 'interfaces/messageBanner';
import { BannerVariant, IBanner } from 'interfaces/banner';
import { Sticky } from 'component/common/Sticky/Sticky';

const StyledBar = styled('aside', {
shouldForwardProp: (prop) => prop !== 'variant' && prop !== 'sticky',
})<{ variant: BannerVariant; sticky?: boolean }>(
({ theme, variant, sticky }) => ({
position: sticky ? 'sticky' : 'relative',
zIndex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: theme.spacing(1),
gap: theme.spacing(1),
borderBottom: '1px solid',
borderColor: theme.palette[variant].border,
background: theme.palette[variant].light,
color: theme.palette[variant].dark,
fontSize: theme.fontSizes.smallBody,
...(sticky && {
top: 0,
zIndex: theme.zIndex.sticky - 100,
}),
}),
);
shouldForwardProp: (prop) => prop !== 'variant',
})<{ variant: BannerVariant }>(({ theme, variant }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: theme.spacing(1),
gap: theme.spacing(1),
borderBottom: '1px solid',
borderColor: theme.palette[variant].border,
background: theme.palette[variant].light,
color: theme.palette[variant].dark,
fontSize: theme.fontSizes.smallBody,
}));

const StyledIcon = styled('div', {
shouldForwardProp: (prop) => prop !== 'variant',
Expand All @@ -43,11 +36,11 @@ const StyledIcon = styled('div', {
color: theme.palette[variant].main,
}));

interface IMessageBannerProps {
messageBanner: IMessageBanner;
interface IBannerProps {
banner: IBanner;
}

export const MessageBanner = ({ messageBanner }: IMessageBannerProps) => {
export const Banner = ({ banner }: IBannerProps) => {
const [open, setOpen] = useState(false);

const {
Expand All @@ -60,10 +53,10 @@ export const MessageBanner = ({ messageBanner }: IMessageBannerProps) => {
plausibleEvent,
dialogTitle,
dialog,
} = messageBanner;
} = banner;

return (
<StyledBar variant={variant} sticky={sticky}>
const bannerBar = (
<StyledBar variant={variant}>
<StyledIcon variant={variant}>
<BannerIcon icon={icon} variant={variant} />
</StyledIcon>
Expand All @@ -75,15 +68,21 @@ export const MessageBanner = ({ messageBanner }: IMessageBannerProps) => {
>
{linkText}
</BannerButton>
<MessageBannerDialog
<BannerDialog
open={open}
setOpen={setOpen}
title={dialogTitle || linkText}
>
{dialog!}
</MessageBannerDialog>
</BannerDialog>
</StyledBar>
);

if (sticky) {
return <Sticky>{bannerBar}</Sticky>;
}

return bannerBar;
};

const VariantIcons = {
Expand Down Expand Up @@ -127,7 +126,7 @@ const BannerButton = ({

const trackEvent = () => {
if (!plausibleEvent) return;
tracker.trackEvent('message_banner', {
tracker.trackEvent('banner', {
props: { event: plausibleEvent },
});
};
Expand Down
Expand Up @@ -8,19 +8,19 @@ const StyledReactMarkdown = styled(ReactMarkdown)(({ theme }) => ({
},
}));

interface IMessageBannerDialogProps {
interface IBannerDialogProps {
title: string;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
children: string;
}

export const MessageBannerDialog = ({
export const BannerDialog = ({
open,
setOpen,
title,
children,
}: IMessageBannerDialogProps) => {
}: IBannerDialogProps) => {
return (
<Dialogue
title={title}
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/component/banners/externalBanners/ExternalBanners.tsx
@@ -0,0 +1,30 @@
import { Banner } from 'component/banners/Banner/Banner';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useVariant } from 'hooks/useVariant';
import { IBanner } from 'interfaces/banner';

export const ExternalBanners = () => {
const { uiConfig } = useUiConfig();

const bannerVariantFromMessageBannerFlag = useVariant<IBanner | IBanner[]>(
uiConfig.flags.messageBanner,
);
const bannerVariantFromBannerFlag = useVariant<IBanner | IBanner[]>(
uiConfig.flags.banner,
);

const bannerVariant =
bannerVariantFromMessageBannerFlag || bannerVariantFromBannerFlag || [];

const banners: IBanner[] = Array.isArray(bannerVariant)
? bannerVariant
: [bannerVariant];

return (
<>
{banners.map((banner) => (
<Banner key={banner.message} banner={banner} />
))}
</>
);
};
14 changes: 14 additions & 0 deletions frontend/src/component/banners/internalBanners/InternalBanners.tsx
@@ -0,0 +1,14 @@
import { Banner } from 'component/banners/Banner/Banner';
import { useBanners } from 'hooks/api/getters/useBanners/useBanners';

export const InternalBanners = () => {
const { banners } = useBanners();

return (
<>
{banners.map((banner) => (
<Banner key={banner.id} banner={banner} />
))}
</>
);
};
17 changes: 11 additions & 6 deletions frontend/src/component/changeRequest/ChangeRequest.test.tsx
Expand Up @@ -8,6 +8,7 @@ import { AccessProvider } from '../providers/AccessProvider/AccessProvider';
import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/AnnouncerProvider';
import { testServerRoute, testServerSetup } from '../../utils/testServer';
import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer';
import { StickyProvider } from 'component/common/Sticky/StickyProvider';

const server = testServerSetup();

Expand Down Expand Up @@ -227,12 +228,16 @@ const UnleashUiSetup: FC<{ path: string; pathTemplate: string }> = ({
<MemoryRouter initialEntries={[path]}>
<ThemeProvider>
<AnnouncerProvider>
<Routes>
<Route
path={pathTemplate}
element={<MainLayout>{children}</MainLayout>}
/>
</Routes>
<StickyProvider>
<Routes>
<Route
path={pathTemplate}
element={
<MainLayout>{children}</MainLayout>
}
/>
</Routes>
</StickyProvider>
</AnnouncerProvider>
</ThemeProvider>
</MemoryRouter>
Expand Down
Expand Up @@ -10,6 +10,7 @@ import { FC } from 'react';
import { IPermission } from '../../interfaces/user';
import { SWRConfig } from 'swr';
import { ProjectMode } from '../project/Project/hooks/useProjectEnterpriseSettingsForm';
import { StickyProvider } from 'component/common/Sticky/StickyProvider';

const server = testServerSetup();

Expand Down Expand Up @@ -186,9 +187,14 @@ const UnleashUiSetup: FC<{ path: string; pathTemplate: string }> = ({
<MemoryRouter initialEntries={[path]}>
<ThemeProvider>
<AnnouncerProvider>
<Routes>
<Route path={pathTemplate} element={children} />
</Routes>
<StickyProvider>
<Routes>
<Route
path={pathTemplate}
element={children}
/>
</Routes>
</StickyProvider>
</AnnouncerProvider>
</ThemeProvider>
</MemoryRouter>
Expand Down
112 changes: 112 additions & 0 deletions frontend/src/component/common/Sticky/Sticky.test.tsx
@@ -0,0 +1,112 @@
import { render, screen, cleanup } from '@testing-library/react';
import { Sticky } from './Sticky';
import { IStickyContext, StickyContext } from './StickyContext';
import { vi, expect } from 'vitest';

describe('Sticky component', () => {
let originalConsoleError: () => void;
let mockRegisterStickyItem: () => void;
let mockUnregisterStickyItem: () => void;
let mockGetTopOffset: () => number;
let mockContextValue: IStickyContext;

beforeEach(() => {
originalConsoleError = console.error;
console.error = vi.fn();

mockRegisterStickyItem = vi.fn();
mockUnregisterStickyItem = vi.fn();
mockGetTopOffset = vi.fn(() => 10);

mockContextValue = {
registerStickyItem: mockRegisterStickyItem,
unregisterStickyItem: mockUnregisterStickyItem,
getTopOffset: mockGetTopOffset,
stickyItems: [],
};
});

afterEach(() => {
cleanup();
console.error = originalConsoleError;
});

it('renders correctly within StickyContext', () => {
render(
<StickyContext.Provider value={mockContextValue}>
<Sticky>Content</Sticky>
</StickyContext.Provider>,
);

expect(screen.getByText('Content')).toBeInTheDocument();
});

it('throws error when not wrapped in StickyContext', () => {
console.error = vi.fn();

expect(() => render(<Sticky>Content</Sticky>)).toThrow(
'Sticky component must be used within a StickyProvider',
);
});

it('applies sticky positioning', () => {
render(
<StickyContext.Provider value={mockContextValue}>
<Sticky>Content</Sticky>
</StickyContext.Provider>,
);

const stickyElement = screen.getByText('Content');
expect(stickyElement).toHaveStyle({ position: 'sticky' });
});

it('registers and unregisters sticky item on mount/unmount', () => {
const { unmount } = render(
<StickyContext.Provider value={mockContextValue}>
<Sticky>Content</Sticky>
</StickyContext.Provider>,
);

expect(mockRegisterStickyItem).toHaveBeenCalledTimes(1);

unmount();

expect(mockUnregisterStickyItem).toHaveBeenCalledTimes(1);
});

it('correctly sets the top value when mounted', async () => {
render(
<StickyContext.Provider value={mockContextValue}>
<Sticky>Content</Sticky>
</StickyContext.Provider>,
);

const stickyElement = await screen.findByText('Content');
expect(stickyElement).toHaveStyle({ top: '10px' });
});

it('updates top offset when stickyItems changes', async () => {
const { rerender } = render(
<StickyContext.Provider value={mockContextValue}>
<Sticky>Content</Sticky>
</StickyContext.Provider>,
);

let stickyElement = await screen.findByText('Content');
expect(stickyElement).toHaveStyle({ top: '10px' });

const updatedMockContextValue = {
...mockContextValue,
getTopOffset: vi.fn(() => 20),
};

rerender(
<StickyContext.Provider value={updatedMockContextValue}>
<Sticky>Content</Sticky>
</StickyContext.Provider>,
);

stickyElement = await screen.findByText('Content');
expect(stickyElement).toHaveStyle({ top: '20px' });
});
});

0 comments on commit d212917

Please sign in to comment.