Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save brandonbryant12/ad3898e2f68c050eea8e3ace28c63a84 to your computer and use it in GitHub Desktop.
Save brandonbryant12/ad3898e2f68c050eea8e3ace28c63a84 to your computer and use it in GitHub Desktop.
import React, { useState, useEffect, useCallback } from 'react';
import {
Alert,
Box,
Switch,
FormControlLabel,
IconButton,
Typography,
} from '@mui/material';
import { Flag as FlagIcon, Close as CloseIcon } from '@mui/icons-material';
import {
useApi,
featureFlagsApiRef,
FeatureFlagState,
} from '@backstage/core-plugin-api';
export interface FeatureFlagBannerProps {
flagName: string;
displayName: string;
description?: string;
onToggle?: (enabled: boolean) => void;
}
export const FeatureFlagBanner: React.FC<FeatureFlagBannerProps> = ({
flagName,
displayName,
description,
onToggle,
}) => {
const featureFlagsApi = useApi(featureFlagsApiRef);
const [isEnabled, setIsEnabled] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [showBanner, setShowBanner] = useState(true);
const loadState = useCallback(async () => {
const registered = await featureFlagsApi.getRegisteredFlags();
if (!registered.some(f => f.name === flagName)) {
console.error(`Feature flag "${flagName}" is not registered`);
setIsLoading(false);
return;
}
setIsEnabled(featureFlagsApi.isActive(flagName));
setIsLoading(false);
}, [featureFlagsApi, flagName]);
useEffect(() => {
loadState();
}, [loadState]);
const handleToggle = async (_: any, checked: boolean) => {
await featureFlagsApi.save({
states: { [flagName]: checked ? FeatureFlagState.Active : FeatureFlagState.None },
});
setIsEnabled(checked);
onToggle?.(checked);
};
if (isLoading || !showBanner) return null;
return (
<Alert
severity="info"
icon={<FlagIcon />}
sx={{
mb: 2,
alignItems: 'center',
backgroundColor: '#e3f2fd',
color: '#1976d2',
border: '1px solid #1976d2',
'& .MuiAlert-icon': { color: '#1976d2' },
}}
action={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FormControlLabel
sx={{ m: 0 }}
label={
<Typography variant="body2" fontWeight="medium">
{isEnabled ? 'ON' : 'OFF'}
</Typography>
}
labelPlacement="start"
control={
<Switch
checked={isEnabled}
onChange={handleToggle}
size="small"
sx={{
'& .MuiSwitch-switchBase': {
color: '#1976d2',
'&.Mui-checked': {
color: '#1976d2',
'& + .MuiSwitch-track': {
backgroundColor: '#1976d2',
opacity: 0.5,
},
},
},
'& .MuiSwitch-track': {
backgroundColor: '#1976d2',
opacity: 0.3,
},
}}
inputProps={{ 'aria-label': `Toggle ${displayName} feature` }}
/>
}
/>
<IconButton
size="small"
aria-label="close"
onClick={() => setShowBanner(false)}
sx={{ color: '#1976d2' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
}
>
{/* LEFT SIDE: icon is rendered by Alert; this box holds name + description inline */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexGrow: 1,
minWidth: 0, // allows ellipsis
}}
>
<Typography variant="body1" fontWeight="medium" noWrap>
{displayName}
</Typography>
{description && (
<Typography
variant="body2"
color="text.secondary"
noWrap
sx={{ flexShrink: 1 }}
>
{description}
</Typography>
)}
</Box>
</Alert>
);
};
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { FeatureFlagBanner } from './FeatureFlagBanner';
import { featureFlagsApiRef, FeatureFlagState } from '@backstage/core-plugin-api';
// Mock the feature flags API
const mockFeatureFlagsApi = {
getRegisteredFlags: jest.fn(),
isActive: jest.fn(),
save: jest.fn(),
};
// Mock the API hook
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
useApi: (apiRef: any) => {
if (apiRef === featureFlagsApiRef) {
return mockFeatureFlagsApi;
}
return undefined;
},
}));
describe('FeatureFlagBanner', () => {
beforeEach(() => {
jest.clearAllMocks();
// Set up default mock implementations
mockFeatureFlagsApi.getRegisteredFlags.mockResolvedValue([
{ name: 'test-feature', pluginId: 'test-plugin' },
]);
mockFeatureFlagsApi.isActive.mockReturnValue(false);
mockFeatureFlagsApi.save.mockResolvedValue(undefined);
});
it('should not render while loading', async () => {
// Make getRegisteredFlags hang to keep loading state
mockFeatureFlagsApi.getRegisteredFlags.mockImplementation(
() => new Promise(() => {})
);
const { container } = render(
<FeatureFlagBanner flagName="test-feature" />
);
expect(container.firstChild).toBeNull();
});
it('should display feature flag information when loaded', async () => {
render(
<FeatureFlagBanner
flagName="test-feature"
displayName="Test Feature"
description="This is a test feature flag"
/>
);
await waitFor(() => {
expect(screen.getByText('Test Feature')).toBeInTheDocument();
expect(screen.getByText('This is a test feature flag')).toBeInTheDocument();
expect(screen.getByText('OFF')).toBeInTheDocument();
});
});
it('should show enabled state when feature is enabled', async () => {
mockFeatureFlagsApi.isActive.mockReturnValue(true);
render(
<FeatureFlagBanner
flagName="test-feature"
displayName="Test Feature"
/>
);
await waitFor(() => {
expect(screen.getByText('ON')).toBeInTheDocument();
});
});
it('should truncate long descriptions', async () => {
const longDescription = 'This is a very long description that should be truncated after fifty characters to keep the banner clean';
render(
<FeatureFlagBanner
flagName="test-feature"
displayName="Test Feature"
description={longDescription}
/>
);
await waitFor(() => {
expect(screen.getByText('This is a very long description that should be tru...')).toBeInTheDocument();
});
});
it('should handle toggle using save API', async () => {
const onToggle = jest.fn();
render(
<FeatureFlagBanner
flagName="test-feature"
onToggle={onToggle}
/>
);
await waitFor(() => {
expect(screen.getByRole('checkbox')).toBeInTheDocument();
});
const toggle = screen.getByRole('checkbox');
fireEvent.click(toggle);
await waitFor(() => {
expect(mockFeatureFlagsApi.save).toHaveBeenCalledWith({
states: {
'test-feature': FeatureFlagState.Active
}
});
expect(onToggle).toHaveBeenCalledWith(true);
});
});
it('should not render if feature flag is not registered', async () => {
mockFeatureFlagsApi.getRegisteredFlags.mockResolvedValue([]);
const { container } = render(
<FeatureFlagBanner
flagName="non-existent-feature"
displayName="Non Existent"
/>
);
await waitFor(() => {
expect(mockFeatureFlagsApi.getRegisteredFlags).toHaveBeenCalled();
});
expect(container.firstChild).toBeNull();
});
it('should dismiss banner when close button clicked', async () => {
render(
<FeatureFlagBanner
flagName="test-feature"
displayName="Test Feature"
/>
);
await waitFor(() => {
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
});
const closeButton = screen.getByRole('button', { name: /close/i });
fireEvent.click(closeButton);
await waitFor(() => {
expect(screen.queryByText('Test Feature')).not.toBeInTheDocument();
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment