diff --git a/.gitignore b/.gitignore index 503c3b8e270..c2682b44790 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ RTA-Jira-Tasks-Summary.md # PMM Demo backups (it would increase repository size significantly) dev/clickhouse-backups/ +node_modules/@percona/percona-ui diff --git a/ui/.cursor/rules/percona_ui-styling.mdc b/ui/.cursor/rules/percona_ui-styling.mdc new file mode 100644 index 00000000000..37313d4dbc0 --- /dev/null +++ b/ui/.cursor/rules/percona_ui-styling.mdc @@ -0,0 +1,50 @@ +--- +description: "Percona UI + PMM UI styling — MUI + @percona/percona-ui, theme tokens, layout conventions, what not to do" +alwaysApply: true +--- + +## Stack order + +1. **MUI** (mostly `@mui/material`, `@mui/icons-material`, `@mui/x-date-pickers` — though other MUI dependencies may also be in use) is the base component and styling API. Use MUI primitives for layout and composition. +2. **`@percona/percona-ui`** is the Percona design layer on top of MUI (theme, branded components, tables, dialogs). Prefer exports from `@percona/percona-ui` when they wrap or standardize behavior (e.g. `Table`, `Dialog`, `ThemeContextProvider`, `pmmThemeOptions`, `NotistackMuiSnackbar`, `primitives`). + +## Do not introduce + +- Do not introduce other CSS/UI frameworks (Tailwind, Bootstrap, styled-components, etc.) or ad-hoc global CSS that fights the theme. +- Do not introduce hard-coded colors, font families, or spacing that bypass the theme. Prefer `sx` with `theme.palette`, `theme.spacing`, breakpoints, and MUI `Typography` variants. +- Do not introduce custom component libraries outside MUI + `@percona/percona-ui` unless explicitly requested. + +## Theming and color mode + +- The app root uses `ThemeContextProvider` with `pmmThemeOptions` from `@percona/percona-ui` — do not replace with a separate `ThemeProvider` or duplicate theme objects in feature code. +- Use `ColorModeContext` / existing hooks (e.g. `useColorMode` in `hooks/theme.ts`) for light/dark; avoid direct `document` or `localStorage` theme hacks. + +## Layout and PMM consistency + +- Follow established layout patterns: MUI `Stack`, `Box`, `Grid`, page shells like `components/page/Page.tsx` (max width, padding, `gap` aligned with existing `sx`). +- Avoid one-off page structures that break alignment with the rest of PMM (arbitrary full-viewport hacks, inconsistent gutters). + +## Styling mechanics + +- Prefer **`sx`** and **`useTheme()`** from `@mui/material/styles` for component-level styling. +- For icons, use **`@mui/icons-material`** (Outlined variants where the codebase already does). + +## When something is missing + +- If a token or component behavior should be shared across products, explain it to them, plan a change, present it as a proposal to extend **`@percona/percona-ui`** (theme / components) rather than embedding one-off design in PMM only. +- If unsure whether a primitive exists in `percona-ui`, check that package or Storybook before inventing a parallel implementation in PMM. + +## Examples + +```tsx +// Good — MUI + theme tokens + + +// Bad — unrelated styling stack +
+ +// Good — branded table from design system +import { Table } from '@percona/percona-ui'; + +// Bad — pulling in another data-table library for the same job +``` diff --git a/ui/apps/pmm-compat/src/compat.ts b/ui/apps/pmm-compat/src/compat.ts index d6631738647..1975d7094b7 100644 --- a/ui/apps/pmm-compat/src/compat.ts +++ b/ui/apps/pmm-compat/src/compat.ts @@ -158,6 +158,11 @@ export const initialize = () => { }, }); + messenger.addListener({ + type: 'SETTINGS_CHANGED', + onMessage: () => getAppEvents().publish(new SettingsUpdatedEvent()), + }); + getAppEvents().subscribe(SettingsUpdatedEvent, () => { messenger.sendMessage({ type: 'SETTINGS_CHANGED', diff --git a/ui/apps/pmm/README.md b/ui/apps/pmm/README.md index 1912c40897c..15be98de381 100644 --- a/ui/apps/pmm/README.md +++ b/ui/apps/pmm/README.md @@ -12,6 +12,7 @@ See the [PMM Documentation](https://www.percona.com/doc/percona-monitoring-and-m See detailed information about prerequisites and setup [here](../../README.md) # Locally testing @percona/percona-ui + - Checkout code from https://github.com/percona/percona-ui - From the lib folder, run `pnpm build:watch` and `yarn link` - On this repo's `ui/apps/pmm` folder, run `yarn link @percona/percona-ui` and uncomment the `exclude` block from `vite.config.ts` diff --git a/ui/apps/pmm/package.json b/ui/apps/pmm/package.json index 0392e21d070..bf51b86c11d 100644 --- a/ui/apps/pmm/package.json +++ b/ui/apps/pmm/package.json @@ -25,7 +25,7 @@ "@mui/icons-material": "^7.3.7", "@mui/material": "^7.3.7", "@mui/x-date-pickers": "^7.5.0", - "@percona/percona-ui": "1.0.13", + "@percona/percona-ui": "1.0.14", "@pmm/shared": "*", "@reactour/tour": "^3.8.0", "@tanstack/react-query": "^5.45.1", @@ -35,6 +35,7 @@ "notistack": "^3.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.71.2", "react-is": "18.3.1", "react-markdown": "^9.0.1", "react-router-dom": "^6.30.2", diff --git a/ui/apps/pmm/src/Providers.tsx b/ui/apps/pmm/src/Providers.tsx index 2750979fc6a..073c25c4e18 100644 --- a/ui/apps/pmm/src/Providers.tsx +++ b/ui/apps/pmm/src/Providers.tsx @@ -26,6 +26,9 @@ const Providers: FC = () => ( { await grafanaApi.get('/frontend/settings'); return res.data; }; + +export const updateSettings = async ( + payload: UpdateSettingsPayload +): Promise => { + const res = await api.put('/server/settings', payload); + return res.data.settings; +}; diff --git a/ui/apps/pmm/src/components/ha-badge/HighAvailabilityBadge.styles.ts b/ui/apps/pmm/src/components/ha-badge/HighAvailabilityBadge.styles.ts index 09263cabdf4..4dbd5e705a8 100644 --- a/ui/apps/pmm/src/components/ha-badge/HighAvailabilityBadge.styles.ts +++ b/ui/apps/pmm/src/components/ha-badge/HighAvailabilityBadge.styles.ts @@ -20,8 +20,8 @@ export const getStyles = (theme: Theme) => ({ : theme.palette.error.dark, backgroundColor: theme.palette.mode === 'light' - ? theme.palette.error.surface - : theme.palette.error.dark, + ? theme.palette.error.surface + : theme.palette.error.dark, transition: 'none', }, down: { diff --git a/ui/apps/pmm/src/components/ha-icon/HighAvailabilityIcon.styles.ts b/ui/apps/pmm/src/components/ha-icon/HighAvailabilityIcon.styles.ts index eb647ad3bfd..744c8ba14c3 100644 --- a/ui/apps/pmm/src/components/ha-icon/HighAvailabilityIcon.styles.ts +++ b/ui/apps/pmm/src/components/ha-icon/HighAvailabilityIcon.styles.ts @@ -1,6 +1,8 @@ import { Theme } from '@mui/material'; -export const getStyles = ({ palette: { background, warning, error, info } }: Theme) => ({ +export const getStyles = ({ + palette: { background, warning, error, info }, +}: Theme) => ({ icon: { width: 20, height: 20, diff --git a/ui/apps/pmm/src/components/main/MainWithNav.test.tsx b/ui/apps/pmm/src/components/main/MainWithNav.test.tsx index 1cddbefdb2a..d1c8f1e6bd9 100644 --- a/ui/apps/pmm/src/components/main/MainWithNav.test.tsx +++ b/ui/apps/pmm/src/components/main/MainWithNav.test.tsx @@ -32,7 +32,7 @@ const setup = ({ )} ); -} +}; describe('MainWithNav', () => { it('shows loading', () => { @@ -60,12 +60,16 @@ describe('MainWithNav', () => { }); it('hides sidebar so the renderer gets a minimal layout', () => { - setup({ isLoading: false, isLoggedIn: true, kioskModeActive: false, search: 'render=1' }); + setup({ + isLoading: false, + isLoggedIn: true, + kioskModeActive: false, + search: 'render=1', + }); expect(screen.queryByTestId('pmm-sidebar')).toBeNull(); }); - it('shows sidebar when not in renderer mode', () => { setup({ isLoading: false, isLoggedIn: true, kioskModeActive: false }); diff --git a/ui/apps/pmm/src/components/main/header/qan-header/qan-header-actions/QanHeaderActions.tsx b/ui/apps/pmm/src/components/main/header/qan-header/qan-header-actions/QanHeaderActions.tsx index 4f6492fa54e..093d33a2e0e 100644 --- a/ui/apps/pmm/src/components/main/header/qan-header/qan-header-actions/QanHeaderActions.tsx +++ b/ui/apps/pmm/src/components/main/header/qan-header/qan-header-actions/QanHeaderActions.tsx @@ -12,7 +12,10 @@ export const QanHeaderActions: FC = () => { const handleCopy = async () => { try { - const path = constructUrl(location).replace(/\/pmm-ui\/(next\/)?graph\//, ''); + const path = constructUrl(location).replace( + /\/pmm-ui\/(next\/)?graph\//, + '' + ); const res = location.pathname.includes('/graph') ? await createShortUrl(path) : { url: window.location.href }; diff --git a/ui/apps/pmm/src/components/page/Page.tsx b/ui/apps/pmm/src/components/page/Page.tsx index 55e568f1c1e..25f20f304d3 100644 --- a/ui/apps/pmm/src/components/page/Page.tsx +++ b/ui/apps/pmm/src/components/page/Page.tsx @@ -2,8 +2,11 @@ import { FC } from 'react'; import { PageProps } from './Page.types'; import { Alert, + Box, Card, CardActions, + Divider, + GlobalStyles, Link, Stack, Typography, @@ -14,46 +17,70 @@ import { PMM_HOME_URL } from 'lib/constants'; import { Footer } from 'components/footer'; import { updateDocumentTitle } from 'utils/document.utils'; -export const Page: FC = ({ title, topBar, footer, children }) => { +export const Page: FC = ({ + title, + topBar, + footer, + children, + fullWidth, + surface, +}) => { const { user } = useUser(); updateDocumentTitle(title); return ( - ({ + <> + {surface && ( + ({ + 'html, body': { + backgroundColor: surface === 'paper' + ? theme.palette.background.paper + : theme.palette.background.default, + }, + })} + /> + )} + {topBar} {!!title && {title}} - {user?.isAuthorized ? ( - children - ) : ( - - - {Messages.noAcccess} - - - - {Messages.goBack} - {Messages.home} - - - - )} + + {user?.isAuthorized ? ( + children + ) : ( + + + {Messages.noAcccess} + + + + {Messages.goBack} + {Messages.home} + + + + )} + + {footer !== undefined ? footer :