diff --git a/packages/primitives/src/CustomNavBar/UserProfile.tsx b/packages/primitives/src/CustomNavBar/UserProfile.tsx index c6276df82..85a43dba5 100644 --- a/packages/primitives/src/CustomNavBar/UserProfile.tsx +++ b/packages/primitives/src/CustomNavBar/UserProfile.tsx @@ -9,8 +9,8 @@ import Divider from '@mui/material/Divider'; import Button from '@mui/material/Button'; import get from 'lodash/get'; import LogoutLogo from '@clients/ui-atoms/LogoutLogo'; -import { useFlyteApi } from '@clients/flyte-api/ApiProvider'; import { Flyte } from '../types/flyteTypes'; +import { SignOutPanel } from '../SessionManagent/SignOutPanel'; const StyledAvatar = styled(Avatar)(({ theme }) => ({ background: 'transparent', @@ -67,7 +67,7 @@ export interface UserProfileProps { /** Displays User name when user is logged in - would be used as User settings entry in future */ export const UserProfile = ({ profile }: UserProfileProps) => { const [anchorEl, setAnchorEl] = useState(null); - const apiContext = useFlyteApi(); + const [signOutOpen, setSignOutOpen] = useState(false); const handlePopoverOpen: MouseEventHandler = (event) => { setAnchorEl(event.currentTarget); @@ -139,7 +139,10 @@ export const UserProfile = ({ profile }: UserProfileProps) => { variant="text" color="inherit" className="actionButton" - href={apiContext.getLogoutUrl()} + onClick={() => { + handlePopoverClose(); + setSignOutOpen(true); + }} data-cy="logout-button" startIcon={} > @@ -149,6 +152,9 @@ export const UserProfile = ({ profile }: UserProfileProps) => { + + {/* Outside the Popover so closing the menu doesn't unmount the dialog. */} + setSignOutOpen(false)} /> ); }; diff --git a/packages/primitives/src/SessionManagent/LoginPanel.tsx b/packages/primitives/src/SessionManagent/LoginPanel.tsx index 25cdf8746..82034a663 100644 --- a/packages/primitives/src/SessionManagent/LoginPanel.tsx +++ b/packages/primitives/src/SessionManagent/LoginPanel.tsx @@ -1,47 +1,11 @@ import React, { useEffect } from 'react'; import { useFlyteApi } from '@clients/flyte-api/ApiProvider'; import Button from '@mui/material/Button'; -import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogTitle from '@mui/material/DialogTitle'; import Box from '@mui/material/Box'; -import styled from '@mui/system/styled'; import { FlyteLogo } from '../assets/icons/FlyteLogo'; - -const StyledDialog = styled(Dialog)(() => ({ - '& .MuiDialog-paper': { - width: '100%', - maxWidth: '448px', - padding: '40px 64px', - gap: '24px', - }, - - h2: { - fontSize: '22px', - lineHeight: '28px', - }, - '& .MuiDialogTitle-root': { - padding: 0, - }, - - '& .MuiDialogActions-root': { - padding: 0, - - button: { - width: '100%', - }, - }, - - '& .displayColumn': { - display: 'flex', - flexDirection: 'column', - gap: '8px', - }, - '& .centerAlign': { - alignItems: 'center', - justifyContent: 'center', - }, -})); +import { SessionDialog } from './SessionDialog'; /** A shared panel rendered along the right side of the UI. Content can be * rendered into it using `LoginPanelContent` @@ -56,7 +20,7 @@ export const LoginPanel: React.FC = () => { }, [expired]); return ( - { setIsLoginModalOpen(false); @@ -86,6 +50,6 @@ export const LoginPanel: React.FC = () => { - + ); }; diff --git a/packages/primitives/src/SessionManagent/SessionDialog.tsx b/packages/primitives/src/SessionManagent/SessionDialog.tsx new file mode 100644 index 000000000..ea291fbce --- /dev/null +++ b/packages/primitives/src/SessionManagent/SessionDialog.tsx @@ -0,0 +1,41 @@ +import Dialog from '@mui/material/Dialog'; +import styled from '@mui/system/styled'; + +/** Shared shell for the session dialogs (login expired, sign out): centered Flyte + * logo, a title, and full-width stacked actions. */ +export const SessionDialog = styled(Dialog)(() => ({ + '& .MuiDialog-paper': { + width: '100%', + maxWidth: '448px', + padding: '40px 64px', + gap: '24px', + }, + + h2: { + fontSize: '22px', + lineHeight: '28px', + }, + '& .MuiDialogTitle-root': { + padding: 0, + }, + + '& .MuiDialogActions-root': { + padding: 0, + + button: { + width: '100%', + }, + }, + + '& .displayColumn': { + display: 'flex', + flexDirection: 'column', + gap: '8px', + }, + '& .centerAlign': { + alignItems: 'center', + justifyContent: 'center', + }, +})); + +export default SessionDialog; diff --git a/packages/primitives/src/SessionManagent/SignOutPanel.tsx b/packages/primitives/src/SessionManagent/SignOutPanel.tsx new file mode 100644 index 000000000..db0066416 --- /dev/null +++ b/packages/primitives/src/SessionManagent/SignOutPanel.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { useFlyteApi } from '@clients/flyte-api/ApiProvider'; +import Button from '@mui/material/Button'; +import DialogActions from '@mui/material/DialogActions'; +import DialogTitle from '@mui/material/DialogTitle'; +import Box from '@mui/material/Box'; +import { FlyteLogo } from '../assets/icons/FlyteLogo'; +import { SessionDialog } from './SessionDialog'; +import t from './strings'; + +export interface SignOutPanelProps { + open: boolean; + onCancel: () => void; +} + +/** Confirmation shown before signing out. Signing out ends the Admin session, so + * it is worth a deliberate click rather than firing on the menu item itself. */ +export const SignOutPanel: React.FC = ({ open, onCancel }) => { + const { getLogoutUrl } = useFlyteApi(); + + return ( + + + + {t('signOutTitle')} + + + + + + + + + ); +}; diff --git a/packages/primitives/src/SessionManagent/index.ts b/packages/primitives/src/SessionManagent/index.ts index eb5693bc4..e836a9b40 100644 --- a/packages/primitives/src/SessionManagent/index.ts +++ b/packages/primitives/src/SessionManagent/index.ts @@ -1 +1,3 @@ export * from './LoginPanel'; +export * from './SessionDialog'; +export * from './SignOutPanel'; diff --git a/packages/primitives/src/SessionManagent/signOutPanel.test.tsx b/packages/primitives/src/SessionManagent/signOutPanel.test.tsx new file mode 100644 index 000000000..4fb691981 --- /dev/null +++ b/packages/primitives/src/SessionManagent/signOutPanel.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { muiTheme } from '@clients/theme/Theme/muiTheme'; +import { ThemeProvider } from '@mui/material/styles'; +import { SignOutPanel } from './SignOutPanel'; + +jest.mock('@clients/flyte-api/ApiProvider', () => ({ + useFlyteApi: () => ({ getLogoutUrl: () => '/logout?redirect_url=/select-project' }), +})); + +const Wrapper = (props: { children: React.ReactNode }) => ( + {props.children} +); + +describe('SignOutPanel', () => { + it('renders nothing while closed', () => { + const { queryByText } = render( + + + , + ); + expect(queryByText('Sign out of Flyte?')).not.toBeInTheDocument(); + }); + + it('confirms to the logout url and cancels without leaving', () => { + const onCancel = jest.fn(); + const { getByText, getByRole } = render( + + + , + ); + + expect(getByText('Sign out of Flyte?')).toBeInTheDocument(); + expect(getByRole('link', { name: 'Sign Out' })).toHaveAttribute( + 'href', + '/logout?redirect_url=/select-project', + ); + + fireEvent.click(getByRole('button', { name: 'Cancel' })); + expect(onCancel).toHaveBeenCalled(); + }); +}); diff --git a/packages/primitives/src/SessionManagent/strings.ts b/packages/primitives/src/SessionManagent/strings.ts new file mode 100644 index 000000000..b7b092166 --- /dev/null +++ b/packages/primitives/src/SessionManagent/strings.ts @@ -0,0 +1,9 @@ +import { createLocalizedString } from '@clients/locale/locale'; + +const str = { + signOutTitle: 'Sign out of Flyte?', + signOutConfirm: 'Sign Out', + signOutCancel: 'Cancel', +}; + +export default createLocalizedString(str);