diff --git a/ui/src/components/page/Page.tsx b/ui/src/components/page/Page.tsx index cd51470d48..eae2eafb70 100644 --- a/ui/src/components/page/Page.tsx +++ b/ui/src/components/page/Page.tsx @@ -18,10 +18,12 @@ export const Page: FC = ({ title, footer, children }) => { return ( ({ + [theme.breakpoints.up('lg')]: { + width: 1000, + }, width: { - sm: 1000, - xs: 'auto', + md: 'auto', }, p: { xs: 2, @@ -29,7 +31,7 @@ export const Page: FC = ({ title, footer, children }) => { mx: 'auto', gap: 3, mt: 1, - }} + })} > {!!title && {title}} {user?.isAuthorized ? ( diff --git a/ui/src/icons/pmm/index.ts b/ui/src/icons/pmm/index.ts index 0c76be7214..4e30be4945 100644 --- a/ui/src/icons/pmm/index.ts +++ b/ui/src/icons/pmm/index.ts @@ -3,3 +3,4 @@ export * from './check'; export * from './dashboards'; export * from './node'; export * from './percona'; +export * from './knowledgeBase'; diff --git a/ui/src/icons/pmm/knowledgeBase.tsx b/ui/src/icons/pmm/knowledgeBase.tsx new file mode 100644 index 0000000000..0988159d38 --- /dev/null +++ b/ui/src/icons/pmm/knowledgeBase.tsx @@ -0,0 +1,17 @@ +import { SvgIcon, SvgIconProps } from '@mui/material'; + +export const KnowledgeBaseIcon = (props: SvgIconProps) => ( + + + +); diff --git a/ui/src/pages/help-center/HelpCenter.constants.ts b/ui/src/pages/help-center/HelpCenter.constants.ts new file mode 100644 index 0000000000..1bd3391423 --- /dev/null +++ b/ui/src/pages/help-center/HelpCenter.constants.ts @@ -0,0 +1,109 @@ +import { HelpCard } from './help-center-card/HelpCenterCard.types'; + +export const CARD_IDS = { + pmmDocs: 'pmm-docs', + support: 'support', + forum: 'forum', + pmmDump: 'pmm-dump', + pmmLogs: 'pmm-logs', + tips: 'tips', +}; + +export const START_ICON = { + download: 'download', + map: 'map', +}; + +export const CARDS_DATA: HelpCard[] = [ + { + id: CARD_IDS.pmmDocs, + title: 'PMM Documentation', + description: + 'From setup to troubleshooting, you’ll find step-by-step instructions, tips, and best practices to get the most out of PMM.', + buttons: [ + { + text: 'View docs', + target: '_blank', + url: 'https://per.co.na/pmm_documentation', + }, + ], + adminOnly: false, + borderColor: '#1486FF', + }, + { + id: CARD_IDS.support, + title: 'Get Percona Support', + description: + 'From 24/7 technical support to fully managed services, Percona’s trusted experts are ready to help you optimize, troubleshoot, and scale.', + buttons: [ + { + text: 'Contact Support', + target: '_blank', + url: 'https://per.co.na/pmm_support', + }, + ], + adminOnly: false, + borderColor: '#F24500', + }, + { + id: CARD_IDS.forum, + title: 'Percona Forum', + description: + 'A friendly space to connect with other users, share insights, and get answers from the community and from the Percona experts.', + buttons: [ + { + text: 'View forum', + target: '_blank', + url: 'https://per.co.na/PMM3_forum', + }, + ], + adminOnly: false, + borderColor: '#30D1B2', + }, + { + id: CARD_IDS.pmmDump, + title: 'PMM Dump', + description: + 'Generate datasets to securely share your data with Percona Support. This helps our experts quickly diagnose and replicate issues.', + buttons: [ + { + text: 'Manage datasets', + url: '/graph/pmm-dump', + }, + ], + adminOnly: true, + borderColor: '#F0B336', + }, + { + id: CARD_IDS.pmmLogs, + title: 'PMM Logs', + description: + 'Download your PMM logs as a ZIP file for easy sharing and faster issue diagnosis.', + buttons: [ + { + text: 'Export logs', + target: '_blank', + url: '/logs.zip', + startIconName: START_ICON.download, + }, + ], + adminOnly: true, + }, + { + id: CARD_IDS.tips, + title: 'Useful Tips', + description: + 'Need a refresher? Access the onboarding tour tips or the keyboard shortcuts.', + adminOnly: false, + buttons: [ + { + text: 'Start PMM tour', + startIconName: START_ICON.map, + }, + { + text: 'Shortcuts', + url: 'https://per.co.na/pmm_documentation', + }, + ], + }, +]; diff --git a/ui/src/pages/help-center/HelpCenter.messages.ts b/ui/src/pages/help-center/HelpCenter.messages.ts new file mode 100644 index 0000000000..379b016690 --- /dev/null +++ b/ui/src/pages/help-center/HelpCenter.messages.ts @@ -0,0 +1,3 @@ +export const Messages = { + pageTitle: 'Help Center', +}; diff --git a/ui/src/pages/help-center/HelpCenter.test.tsx b/ui/src/pages/help-center/HelpCenter.test.tsx new file mode 100644 index 0000000000..80ca7c746e --- /dev/null +++ b/ui/src/pages/help-center/HelpCenter.test.tsx @@ -0,0 +1,95 @@ +import { render, screen } from '@testing-library/react'; +import { HelpCenter } from './HelpCenter'; +import { CARD_IDS } from './HelpCenter.constants'; +import * as useUserModule from 'contexts/user'; +import { OrgRole } from 'types/user.types'; + +describe('HelpCenter', () => { + it('should show pmm dump and pmm logs if user is admin', () => { + vi.spyOn(useUserModule, 'useUser').mockReturnValue({ + isLoading: false, + user: { + id: 1, + isPMMAdmin: true, + orgRole: OrgRole.Admin, + isAuthorized: true, + }, + }); + + render(); + + expect( + screen.queryByTestId(`help-card-${CARD_IDS.pmmDump}`) + ).toBeInTheDocument(); + expect( + screen.queryByTestId(`help-card-${CARD_IDS.pmmLogs}`) + ).toBeInTheDocument(); + expect(screen.queryAllByTestId(/^help-card-/).length).toEqual(6); + }); + + it('should not show pmm dump and pmm logs if user has no org role', () => { + vi.spyOn(useUserModule, 'useUser').mockReturnValue({ + isLoading: false, + user: { + id: 1, + isPMMAdmin: false, + orgRole: OrgRole.None, + isAuthorized: true, + }, + }); + + render(); + + expect( + screen.queryByTestId(`help-card-${CARD_IDS.pmmDump}`) + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId(`help-card-${CARD_IDS.pmmLogs}`) + ).not.toBeInTheDocument(); + expect(screen.queryAllByTestId(/^help-card-/).length).toEqual(4); + }); + + it('should not show pmm dump and pmm logs if user is viewer', () => { + vi.spyOn(useUserModule, 'useUser').mockReturnValue({ + isLoading: false, + user: { + id: 1, + isPMMAdmin: false, + orgRole: OrgRole.Viewer, + isAuthorized: true, + }, + }); + + render(); + + expect( + screen.queryByTestId(`help-card-${CARD_IDS.pmmDump}`) + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId(`help-card-${CARD_IDS.pmmLogs}`) + ).not.toBeInTheDocument(); + expect(screen.queryAllByTestId(/^help-card-/).length).toEqual(4); + }); + + it('should not show pmm dump and pmm logs if user is editor', () => { + vi.spyOn(useUserModule, 'useUser').mockReturnValue({ + isLoading: false, + user: { + id: 1, + isPMMAdmin: false, + orgRole: OrgRole.Editor, + isAuthorized: true, + }, + }); + + render(); + + expect( + screen.queryByTestId(`help-card-${CARD_IDS.pmmDump}`) + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId(`help-card-${CARD_IDS.pmmLogs}`) + ).not.toBeInTheDocument(); + expect(screen.queryAllByTestId(/^help-card-/).length).toEqual(4); + }); +}); diff --git a/ui/src/pages/help-center/HelpCenter.tsx b/ui/src/pages/help-center/HelpCenter.tsx new file mode 100644 index 0000000000..db1b8a9cae --- /dev/null +++ b/ui/src/pages/help-center/HelpCenter.tsx @@ -0,0 +1,34 @@ +import { Box } from '@mui/material'; +import { Page } from 'components/page'; +import { FC } from 'react'; +import { Messages } from './HelpCenter.messages'; +import { CARDS_DATA } from './HelpCenter.constants'; +import { useUser } from 'contexts/user'; +import { HelpCenterCard } from './help-center-card/HelpCenterCard'; + +export const HelpCenter: FC = () => { + const { user } = useUser(); + const cards = CARDS_DATA.filter( + (card) => user?.isPMMAdmin || !card.adminOnly + ); + + return ( + + + {cards.map((item) => ( + + ))} + + + ); +}; diff --git a/ui/src/pages/help-center/help-center-card/HelpCenterCard.tsx b/ui/src/pages/help-center/help-center-card/HelpCenterCard.tsx new file mode 100644 index 0000000000..fcb90495ff --- /dev/null +++ b/ui/src/pages/help-center/help-center-card/HelpCenterCard.tsx @@ -0,0 +1,94 @@ +import { Button, CardContent, Typography, Card, Stack } from '@mui/material'; +import { + Support, + ForumOutlined, + DatasetOutlined, + NorthEast, + SaveAlt, + MapOutlined, +} from '@mui/icons-material'; +import { KnowledgeBaseIcon } from 'icons'; +import { FC, ReactNode, useCallback } from 'react'; +import { CARD_IDS, START_ICON } from '../HelpCenter.constants'; +import { HelpCenterCardProps, HelpCardButton } from './HelpCenterCard.types'; + +export const HelpCenterCard: FC = ({ card }) => { + const { id, title, borderColor, description, buttons } = card; + + const getIcon = useCallback((cardId: string): ReactNode => { + switch (cardId) { + case CARD_IDS.pmmDocs: + return ; + case CARD_IDS.support: + return ; + case CARD_IDS.forum: + return ; + case CARD_IDS.pmmDump: + return ; + default: + return null; + } + }, []); + + const getButtonStartIcon = useCallback((iconName?: string): ReactNode => { + switch (iconName) { + case START_ICON.download: + return ; + case START_ICON.map: + return ; + default: + return null; + } + }, []); + + const onButtonClick = useCallback((button: HelpCardButton) => { + if (button.target) { + window.open(button.url, button.target, 'noopener,noreferrer'); + } else { + // TODO: use react router once the grafana iframe is in place + button.url && window.location.assign(button.url); + } + }, []); + + return ( + + + + {getIcon(id)} + + {title} + + + + {description} + + {buttons.map((button) => ( + + ))} + + + + ); +}; diff --git a/ui/src/pages/help-center/help-center-card/HelpCenterCard.types.ts b/ui/src/pages/help-center/help-center-card/HelpCenterCard.types.ts new file mode 100644 index 0000000000..a767713267 --- /dev/null +++ b/ui/src/pages/help-center/help-center-card/HelpCenterCard.types.ts @@ -0,0 +1,19 @@ +export interface HelpCardButton { + text: string; + target?: string; + url?: string; + startIconName?: string; +} + +export interface HelpCard { + id: string; + title: string; + description: string; + buttons: HelpCardButton[]; + adminOnly: boolean; + borderColor?: string; +} + +export interface HelpCenterCardProps { + card: HelpCard; +} diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 995deaf35f..e6e6458aea 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -2,6 +2,7 @@ import { Navigate, createBrowserRouter } from 'react-router-dom'; import { Main } from 'components/main/Main'; import { Updates } from 'pages/updates'; import { UpdateClients } from 'pages/update-clients/UpdateClients'; +import { HelpCenter } from 'pages/help-center/HelpCenter'; const router = createBrowserRouter( [ @@ -21,6 +22,10 @@ const router = createBrowserRouter( path: 'updates/clients', element: , }, + { + path: 'help', + element: , + }, ], }, {