♻️(frontend) replace env NEXT_PUBLIC_FEATURE_TEAM
NEXT_PUBLIC_FEATURE_TEAM is a buid-time env variable, it is not easy to overload it per environment. We will use the config endpoint to get the feature flag at runtime. To do so, we are using the ConfigStore.
This commit is contained in:
@@ -8,6 +8,10 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
🔧Runtime config for the frontend #345
|
||||
|
||||
## [1.0.1] - 2024-08-19
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
|
||||
NEXT_PUBLIC_FEATURE_TEAM=true
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://test.jest
|
||||
NEXT_PUBLIC_FEATURE_TEAM=true
|
||||
|
||||
@@ -1,23 +1,41 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { useConfigStore } from '@/core';
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import Page from '../pages';
|
||||
|
||||
const mockedPush = jest.fn();
|
||||
const mockedUseRouter = jest.fn().mockReturnValue({
|
||||
push: mockedPush,
|
||||
});
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
...jest.requireActual('next/navigation'),
|
||||
useRouter: () => ({}),
|
||||
useRouter: () => mockedUseRouter(),
|
||||
}));
|
||||
|
||||
describe('Page', () => {
|
||||
it('checks Page rendering', () => {
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('checks Page rendering with team feature', () => {
|
||||
useConfigStore.setState({
|
||||
config: { FEATURES: { TEAMS: true }, LANGUAGES: [] },
|
||||
});
|
||||
|
||||
render(<Page />, { wrapper: AppWrapper });
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: /Create a new team/i,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(mockedPush).toHaveBeenCalledWith('/teams/');
|
||||
});
|
||||
|
||||
it('checks Page rendering without team feature', () => {
|
||||
useConfigStore.setState({
|
||||
config: { FEATURES: { TEAMS: false }, LANGUAGES: [] },
|
||||
});
|
||||
|
||||
render(<Page />, { wrapper: AppWrapper });
|
||||
|
||||
expect(mockedPush).toHaveBeenCalledWith('/mail-domains/');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,13 +5,17 @@ import { Footer } from '@/features/footer/Footer';
|
||||
import { HEADER_HEIGHT, Header } from '@/features/header';
|
||||
import { Menu } from '@/features/menu';
|
||||
|
||||
import { useConfigStore } from './config';
|
||||
|
||||
export function MainLayout({ children }: PropsWithChildren) {
|
||||
const { config } = useConfigStore();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box $height="100vh">
|
||||
<Header />
|
||||
<Box $css="flex: 1;" $direction="row">
|
||||
{process.env.NEXT_PUBLIC_FEATURE_TEAM === 'true' && <Menu />}
|
||||
{config?.FEATURES.TEAMS && <Menu />}
|
||||
<Box
|
||||
as="main"
|
||||
$height={`calc(100vh - ${HEADER_HEIGHT})`}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { render, screen } from '@testing-library/react';
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import { MainLayout } from '../MainLayout';
|
||||
import { useConfigStore } from '../config';
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
...jest.requireActual('next/navigation'),
|
||||
@@ -11,7 +12,11 @@ jest.mock('next/navigation', () => ({
|
||||
}));
|
||||
|
||||
describe('MainLayout', () => {
|
||||
it('checks menu rendering', () => {
|
||||
it('checks menu rendering with team feature', () => {
|
||||
useConfigStore.setState({
|
||||
config: { FEATURES: { TEAMS: true }, LANGUAGES: [] },
|
||||
});
|
||||
|
||||
render(<MainLayout />, { wrapper: AppWrapper });
|
||||
|
||||
expect(
|
||||
@@ -28,8 +33,6 @@ describe('MainLayout', () => {
|
||||
});
|
||||
|
||||
it('checks menu rendering without team feature', () => {
|
||||
process.env.NEXT_PUBLIC_FEATURE_TEAM = 'false';
|
||||
|
||||
render(<MainLayout />, { wrapper: AppWrapper });
|
||||
|
||||
expect(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import IconOpenClose from '@/assets/icons/icon-open-close.svg';
|
||||
import { Box, BoxButton, Text } from '@/components';
|
||||
import { useConfigStore } from '@/core/';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { ItemList } from './ItemList';
|
||||
@@ -11,6 +12,7 @@ import { PanelActions } from './PanelActions';
|
||||
export const Panel = () => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { config } = useConfigStore();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
@@ -20,7 +22,7 @@ export const Panel = () => {
|
||||
$minWidth: '0',
|
||||
};
|
||||
|
||||
const styleNoTeam = process.env.NEXT_PUBLIC_FEATURE_TEAM !== 'true' && {
|
||||
const styleNoTeam = !config?.FEATURES.TEAMS && {
|
||||
$display: 'none',
|
||||
tabIndex: -1,
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export const Menu = () => {
|
||||
<MenuItem
|
||||
Icon={IconGroup}
|
||||
label={t('Teams')}
|
||||
href="/"
|
||||
href="/teams"
|
||||
alias={['/teams']}
|
||||
/>
|
||||
<MenuItem
|
||||
|
||||
@@ -11,7 +11,7 @@ const Page: NextPageWithLayout = () => {
|
||||
useEffect(() => {
|
||||
config?.FEATURES.TEAMS
|
||||
? router.push('/teams/')
|
||||
: router.push('mail-domains/');
|
||||
: router.push('/mail-domains/');
|
||||
}, [config?.FEATURES.TEAMS, router]);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -24,6 +24,6 @@ test.describe('404', () => {
|
||||
page,
|
||||
}) => {
|
||||
await page.getByText('Back to home page').click();
|
||||
await expect(page).toHaveURL('/');
|
||||
await expect(page).toHaveURL('/teams/');
|
||||
});
|
||||
});
|
||||
|
||||
63
src/frontend/apps/e2e/__tests__/app-desk/config.spec.ts
Normal file
63
src/frontend/apps/e2e/__tests__/app-desk/config.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { keyCloakSignIn } from './common';
|
||||
|
||||
test.describe('Config', () => {
|
||||
test.beforeEach(async ({ page, browserName }) => {
|
||||
await page.goto('/');
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
|
||||
test('it checks the config api is called', async ({ page }) => {
|
||||
const responsePromise = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/config/') && response.status() === 200,
|
||||
);
|
||||
|
||||
const response = await responsePromise;
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
expect(await response.json()).toStrictEqual({
|
||||
LANGUAGES: [
|
||||
['en-us', 'English'],
|
||||
['fr-fr', 'French'],
|
||||
],
|
||||
FEATURES: { TEAMS: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('it checks that the config can deactivate the feature "teams"', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route('**/api/v1.0/config/', async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method().includes('GET')) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
LANGUAGES: [
|
||||
['en-us', 'English'],
|
||||
['fr-fr', 'French'],
|
||||
],
|
||||
FEATURES: { TEAMS: false },
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await expect(page.locator('menu')).toBeHidden();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Create a new team',
|
||||
}),
|
||||
).toBeHidden();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: 'Add your mail domain',
|
||||
}),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ test.describe('Menu', () => {
|
||||
{
|
||||
name: 'Teams',
|
||||
isDefault: true,
|
||||
expectedUrl: '',
|
||||
expectedUrl: '/teams/',
|
||||
expectedText: 'Create a new team',
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user