From 1cc2ee4ea5f50403e99953bd6c51a7d4cbec3b3f Mon Sep 17 00:00:00 2001 From: Mark Phelps <209477+markphelps@users.noreply.github.com> Date: Mon, 13 May 2024 16:21:09 -0400 Subject: [PATCH] feat: add banner about cloud offering (#3077) --- ui/src/app/Layout.tsx | 10 ++++++++ ui/src/app/Onboarding.tsx | 17 +++++++++---- ui/src/app/events/eventSlice.ts | 14 +++++++++-- ui/src/components/Banner.tsx | 44 +++++++++++++++++++++++++++++++++ ui/src/store.ts | 5 +++- ui/tests/onboarding.spec.ts | 21 ++++++++++------ 6 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 ui/src/components/Banner.tsx diff --git a/ui/src/app/Layout.tsx b/ui/src/app/Layout.tsx index f4331f1ef3..a70687ba9e 100644 --- a/ui/src/app/Layout.tsx +++ b/ui/src/app/Layout.tsx @@ -28,11 +28,14 @@ import { useListNamespacesQuery } from './namespaces/namespacesSlice'; import CommandDialog from '~/components/command/CommandDialog'; +import Banner from '~/components/Banner'; +import { selectDismissedBanner } from './events/eventSlice'; function InnerLayout() { const { session } = useSession(); const [sidebarOpen, setSidebarOpen] = useState(false); + const dismissedBanner = useSelector(selectDismissedBanner); const dispatch = useAppDispatch(); const { namespaceKey } = useParams(); @@ -75,6 +78,13 @@ function InnerLayout() {
+ {!dismissedBanner && ( + + )}
diff --git a/ui/src/app/Onboarding.tsx b/ui/src/app/Onboarding.tsx index 90115786ac..ef17158568 100644 --- a/ui/src/app/Onboarding.tsx +++ b/ui/src/app/Onboarding.tsx @@ -3,6 +3,7 @@ import { AcademicCapIcon, BookOpenIcon, ChatBubbleLeftIcon, + CloudIcon, CodeBracketIcon, CommandLineIcon, PuzzlePieceIcon, @@ -20,28 +21,34 @@ const gettingStartedTiles = [ icon: AcademicCapIcon, name: 'Get Started', description: 'Learn how to create your first feature flag', + href: 'https://docs.flipt.io/introduction' + }, + { + icon: CloudIcon, + name: 'Introducing Flipt Hybrid Cloud', className: 'sm:col-span-2', - href: 'https://www.flipt.io/docs/introduction' + description: + 'Learn about our managed offering with enhanced security and support', + href: 'https://docs.flipt.io/cloud/overview' }, { icon: CommandLineIcon, name: 'Try the CLI', description: 'Use the Flipt CLI to manage your feature flags and more', - href: 'https://www.flipt.io/docs/cli/overview' + href: 'https://docs.flipt.io/cli/overview' }, { icon: BookOpenIcon, name: 'Checkout a Guide', description: 'Use Flipt to its full potential. Read our guides including using Flipt with GitOps', - href: 'https://www.flipt.io/docs/guides' + href: 'https://docs.flipt.io/guides' }, { icon: PuzzlePieceIcon, name: 'Integrate Your Application', description: 'Use our SDKs to integrate your applications in your language', - className: 'sm:col-span-2', - href: 'https://www.flipt.io/docs/integration/overview' + href: 'https://docs.flipt.io/integration/overview' } ]; diff --git a/ui/src/app/events/eventSlice.ts b/ui/src/app/events/eventSlice.ts index c273209011..59703d4eec 100644 --- a/ui/src/app/events/eventSlice.ts +++ b/ui/src/app/events/eventSlice.ts @@ -4,10 +4,12 @@ import { RootState } from '~/store'; export const eventKey = 'event'; interface IEventState { completedOnboarding: boolean; + dismissedBanner: boolean; } const initialState: IEventState = { - completedOnboarding: false + completedOnboarding: false, + dismissedBanner: false }; export const eventSlice = createSlice({ @@ -16,15 +18,23 @@ export const eventSlice = createSlice({ reducers: { onboardingCompleted: (state) => { state.completedOnboarding = true; + }, + bannerDismissed: (state) => { + state.dismissedBanner = true; } } }); -export const { onboardingCompleted } = eventSlice.actions; +export const { onboardingCompleted, bannerDismissed } = eventSlice.actions; export const selectCompletedOnboarding = createSelector( [(state: RootState) => state.user], (user) => user.completedOnboarding ); +export const selectDismissedBanner = createSelector( + [(state: RootState) => state.user], + (user) => user.dismissedBanner +); + export default eventSlice.reducer; diff --git a/ui/src/components/Banner.tsx b/ui/src/components/Banner.tsx new file mode 100644 index 0000000000..3f0db5a8da --- /dev/null +++ b/ui/src/components/Banner.tsx @@ -0,0 +1,44 @@ +import { XMarkIcon } from '@heroicons/react/20/solid'; +import { useDispatch } from 'react-redux'; +import { bannerDismissed } from '~/app/events/eventSlice'; + +type BannerProps = { + title: string; + description: string; + href: string; +}; + +export default function Banner(props: BannerProps) { + const { title, description, href } = props; + + const dispatch = useDispatch(); + + return ( +
+

+ + {title} + + {description}  + + +

+
+ +
+
+ ); +} diff --git a/ui/src/store.ts b/ui/src/store.ts index 421d228779..e61f963734 100644 --- a/ui/src/store.ts +++ b/ui/src/store.ts @@ -28,7 +28,10 @@ import { eventSlice, eventKey } from './app/events/eventSlice'; const listenerMiddleware = createListenerMiddleware(); listenerMiddleware.startListening({ - matcher: isAnyOf(eventSlice.actions.onboardingCompleted), + matcher: isAnyOf( + eventSlice.actions.onboardingCompleted, + eventSlice.actions.bannerDismissed + ), effect: (_action, api) => { // save to local storage localStorage.setItem( diff --git a/ui/tests/onboarding.spec.ts b/ui/tests/onboarding.spec.ts index cdbf47edfb..b6c953d5fc 100644 --- a/ui/tests/onboarding.spec.ts +++ b/ui/tests/onboarding.spec.ts @@ -7,24 +7,29 @@ test.describe('Onboarding', () => { await expect(page.locator('h1')).toContainText('Onboarding'); await expect( - page.getByText('Get Started', { exact: true }) + page.getByRole('heading', { name: 'Get Started' }) ).toBeVisible(); await expect( - page.getByText('Try the CLI', { exact: true }) + page.getByRole('heading', { name: 'Introducing Flipt Hybrid Cloud' }) ).toBeVisible(); await expect( - page.getByText('Checkout a Guide', { exact: true }) + page.getByRole('heading', { name: 'Try the CLI' }) ).toBeVisible(); await expect( - page.getByText('Integrate Your Application', { exact: true }) + page.getByRole('heading', { name: 'Checkout a Guide' }) ).toBeVisible(); await expect( - page.getByText('Join the Community', { exact: true }) + page.getByRole('heading', { name: 'Integrate Your Application' }) ).toBeVisible(); await expect( - page.getByText('View API Reference', { exact: true }) + page.getByRole('heading', { name: 'Join the Community' }) + ).toBeVisible(); + await expect( + page.getByRole('heading', { name: 'View API Reference' }) + ).toBeVisible(); + await expect( + page.getByRole('heading', { name: 'Support Us' }) ).toBeVisible(); - await expect(page.getByText('Support Us', { exact: true })).toBeVisible(); await expect( page.getByRole('button', { name: 'Continue to Dashboard' }) ).toBeVisible(); @@ -41,7 +46,7 @@ test.describe('Onboarding', () => { test.describe('user navigates to the onboarding page', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); - await page.getByRole('link', { name: 'Support' }).click(); + await page.getByRole('link', { name: 'Support', exact: true }).click(); await page.getByRole('heading', { name: 'Onboarding' }).click(); await page.getByRole('link', { name: "Let's Go" }).click(); });