diff --git a/packages/manager/.changeset/pr-12115-tech-stories-1745845625727.md b/packages/manager/.changeset/pr-12115-tech-stories-1745845625727.md new file mode 100644 index 00000000000..68d48bfffb4 --- /dev/null +++ b/packages/manager/.changeset/pr-12115-tech-stories-1745845625727.md @@ -0,0 +1,5 @@ +--- +'@linode/manager': Added +--- + +Non-dismissible option support to Dismissible Banner ([#12115](https://github.com/linode/manager/pull/12115)) diff --git a/packages/manager/.changeset/pr-12115-upcoming-features-1745845577281.md b/packages/manager/.changeset/pr-12115-upcoming-features-1745845577281.md new file mode 100644 index 00000000000..f05b99a0828 --- /dev/null +++ b/packages/manager/.changeset/pr-12115-upcoming-features-1745845577281.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Metrics notification banner with legacy-beta preference toggle option ([#12115](https://github.com/linode/manager/pull/12115)) diff --git a/packages/manager/src/components/DismissibleBanner/DismissibleBanner.stories.tsx b/packages/manager/src/components/DismissibleBanner/DismissibleBanner.stories.tsx index 8f94913b11c..33aa84e9052 100644 --- a/packages/manager/src/components/DismissibleBanner/DismissibleBanner.stories.tsx +++ b/packages/manager/src/components/DismissibleBanner/DismissibleBanner.stories.tsx @@ -37,6 +37,29 @@ export const CallToActionBanner: Story = { ), }; +/** + * Example of a non-dismissible banner with an associated action + */ +export const CallToActionNonDismissibleBanner: Story = { + render: () => ( + null}> + Try Beta Now + + } + dismissible={false} + forceImportantIconVerticalCenter + preferenceKey="cluster-v1" + variant="info" + > + + A new Beta version is now available with enhanced features. + + + ), +}; + /** * Beta banners, along with [beta chips](/docs/elements-chips-beta-chips--default-story), provide notice to users about beta features. */ diff --git a/packages/manager/src/components/DismissibleBanner/DismissibleBanner.tsx b/packages/manager/src/components/DismissibleBanner/DismissibleBanner.tsx index 3185c2cb5bc..e1f15c30422 100644 --- a/packages/manager/src/components/DismissibleBanner/DismissibleBanner.tsx +++ b/packages/manager/src/components/DismissibleBanner/DismissibleBanner.tsx @@ -11,6 +11,12 @@ interface Props extends NoticeProps { * Optional element to pass to the banner to trigger actions */ actionButton?: JSX.Element; + /** + * If false, the banner will not be dismissible + * + * @default true + */ + dismissible?: boolean; /** * If true, the important icon will be vertically centered with the text no matter the height of the text. */ @@ -42,7 +48,14 @@ interface Props extends NoticeProps { * - Call to action: Primary Button or text link allows a user to take action directly from the banner. */ export const DismissibleBanner = (props: Props) => { - const { actionButton, children, options, preferenceKey, ...rest } = props; + const { + actionButton, + children, + dismissible = true, // Default to true if not provided + options, + preferenceKey, + ...rest + } = props; const { handleDismiss, hasDismissedBanner } = useDismissibleBanner( preferenceKey, @@ -86,7 +99,7 @@ export const DismissibleBanner = (props: Props) => { spacing={1} > {actionButton} - {dismissibleButton} + {dismissible ? dismissibleButton : null} diff --git a/packages/manager/src/factories/dashboards.ts b/packages/manager/src/factories/dashboards.ts index 013f07ef882..91af9aa7303 100644 --- a/packages/manager/src/factories/dashboards.ts +++ b/packages/manager/src/factories/dashboards.ts @@ -59,8 +59,8 @@ export const widgetFactory = Factory.Sync.makeFactory({ y_label: Factory.each((i) => `y_label_${i}`), }); -export const dashboardMetricFactory = Factory.Sync.makeFactory( - { +export const dashboardMetricFactory = + Factory.Sync.makeFactory({ available_aggregate_functions: ['min', 'max', 'avg', 'sum'], dimensions: [ { @@ -86,11 +86,10 @@ export const dashboardMetricFactory = Factory.Sync.makeFactory (i) => scrape_interval[i % scrape_interval.length] ), unit: 'defaultUnit', - } -); + }); -export const cloudPulseMetricsResponseDataFactory = Factory.Sync.makeFactory( - { +export const cloudPulseMetricsResponseDataFactory = + Factory.Sync.makeFactory({ result: [ { metric: {}, @@ -98,16 +97,14 @@ export const cloudPulseMetricsResponseDataFactory = Factory.Sync.makeFactory( - { +export const cloudPulseMetricsResponseFactory = + Factory.Sync.makeFactory({ data: cloudPulseMetricsResponseDataFactory.build(), isPartial: false, stats: { series_fetched: 2, }, status: 'success', - } -); + }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/AclpMetricsPreferenceToggle.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/AclpMetricsPreferenceToggle.test.tsx new file mode 100644 index 00000000000..a3b350f70af --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/AclpMetricsPreferenceToggle.test.tsx @@ -0,0 +1,137 @@ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import * as React from 'react'; +import { vi } from 'vitest'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { AclpMetricsPreferenceToggle } from './AclpMetricsPreferenceToggle'; + +import type { ManagerPreferences } from '@linode/utilities'; + +const preference: ManagerPreferences['isAclpMetricsBeta'] = true; +const legacyModeBannerText = + 'Try the new Metrics (Beta) with more options and greater flexibility for better data analysis. You can switch back to the current Metrics view anytime.'; +const betaModeBannertext = + 'Welcome to Metrics (Beta) with more options and greater flexibility for better data analysis.'; +const legacyModeButtonText = 'Try the Metrics (Beta)'; +const betaModeButtonText = 'Switch to legacy Metrics'; + +const queryMocks = vi.hoisted(() => ({ + useMutatePreferences: vi.fn(), + usePreferences: vi.fn(), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + useMutatePreferences: queryMocks.useMutatePreferences, + usePreferences: queryMocks.usePreferences, + }; +}); + +describe('AclpMetricsPreferenceToggle', () => { + it('should display loading state correctly', () => { + queryMocks.usePreferences.mockReturnValue({ + data: undefined, + isLoading: true, + }); + queryMocks.useMutatePreferences.mockReturnValue({ + mutateAsync: vi.fn().mockResolvedValue(undefined), + }); + + renderWithTheme(, { + flags: { aclpIntegration: true }, + }); + + const skeleton = screen.getByTestId('metrics-preference-skeleton'); + expect(skeleton).toBeInTheDocument(); + }); + + it('should display banner and button with the correct text in Legacy mode when isAclpMetricsBeta preference is disabled', () => { + queryMocks.usePreferences.mockReturnValue({ + data: false, + isLoading: false, + }); + + renderWithTheme(, { + flags: { aclpIntegration: true }, + }); + + // Check if the banner content and button text is correct in Legacy mode + const typography = screen.getByTestId('metrics-preference-banner-text'); + expect(typography).toHaveTextContent(legacyModeBannerText); + + const expectedLegacyModeButtonText = screen.getByText(legacyModeButtonText); + expect(expectedLegacyModeButtonText).toBeInTheDocument(); + }); + + it('should display banner and button with the correct text in Beta mode when isAclpMetricsBeta preference is enabled', () => { + queryMocks.usePreferences.mockReturnValue({ + data: preference, + isLoading: false, + }); + + renderWithTheme(, { + flags: { aclpIntegration: true }, + }); + + // Check if the banner content and button text is correct in Beta mode + const typography = screen.getByTestId('metrics-preference-banner-text'); + expect(typography).toHaveTextContent(betaModeBannertext); + + const expectedLegacyModeButtonText = screen.getByText(betaModeButtonText); + expect(expectedLegacyModeButtonText).toBeInTheDocument(); + }); + + it('should update ACLP Metrics preference to beta mode when toggling from legacy mode', async () => { + queryMocks.usePreferences.mockReturnValue({ + data: false, + isLoading: false, + }); + const mockUpdatePreferences = vi.fn().mockResolvedValue({ + isAclpMetricsBeta: false, + }); + queryMocks.useMutatePreferences.mockReturnValue({ + mutateAsync: mockUpdatePreferences, + }); + + renderWithTheme(, { + flags: { aclpIntegration: true }, + }); + + // Click the button to switch from legacy to beta + const button = screen.getByText(legacyModeButtonText); + await userEvent.click(button); + + expect(mockUpdatePreferences).toHaveBeenCalledWith({ + isAclpMetricsBeta: true, + }); + }); + + it('should update ACLP Metrics preference to legacy mode when toggling from beta mode', async () => { + queryMocks.usePreferences.mockReturnValue({ + data: preference, + isLoading: false, + }); + const mockUpdatePreferences = vi.fn().mockResolvedValue({ + isAclpMetricsBeta: true, + }); + queryMocks.useMutatePreferences.mockReturnValue({ + mutateAsync: mockUpdatePreferences, + }); + + renderWithTheme(, { + flags: { aclpIntegration: true }, + }); + + // Click the button to switch from beta to legacy + const button = screen.getByText(betaModeButtonText); + await userEvent.click(button); + + expect(mockUpdatePreferences).toHaveBeenCalledWith({ + isAclpMetricsBeta: false, + }); + }); +}); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/AclpMetricsPreferenceToggle.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/AclpMetricsPreferenceToggle.tsx new file mode 100644 index 00000000000..f5a079e4dfb --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/AclpMetricsPreferenceToggle.tsx @@ -0,0 +1,65 @@ +import { useMutatePreferences, usePreferences } from '@linode/queries'; +import { Button, Typography } from '@linode/ui'; +import React from 'react'; + +import { DismissibleBanner } from 'src/components/DismissibleBanner/DismissibleBanner'; +import { Skeleton } from 'src/components/Skeleton'; + +export const AclpMetricsPreferenceToggle = () => { + const { data: isAclpMetricsPreferenceBeta, isLoading } = usePreferences( + (preferences) => preferences?.isAclpMetricsBeta + ); + + const { mutateAsync: updatePreferences } = useMutatePreferences(); + + if (isLoading) { + return ( + ({ + marginTop: `-${theme.tokens.spacing.S20}`, + })} + /> + ); + } + + return ( + + updatePreferences({ + isAclpMetricsBeta: !isAclpMetricsPreferenceBeta, + }) + } + sx={{ textTransform: 'none' }} + > + {isAclpMetricsPreferenceBeta + ? 'Switch to legacy Metrics' + : 'Try the Metrics (Beta)'} + + } + dismissible={false} + forceImportantIconVerticalCenter + preferenceKey="metrics-preference" + variant="info" + > + + {isAclpMetricsPreferenceBeta ? ( + + Welcome to Metrics (Beta) with more options and + greater flexibility for better data analysis. + + ) : ( + + Try the new Metrics (Beta) with more options and + greater flexibility for better data analysis. You can switch back to + the current Metrics view anytime. + + )} + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeMetrics.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeMetrics.tsx new file mode 100644 index 00000000000..e069f8aaf8b --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeMetrics.tsx @@ -0,0 +1,34 @@ +import { usePreferences } from '@linode/queries'; +import { Box, Notice } from '@linode/ui'; +import * as React from 'react'; + +import { useFlags } from 'src/hooks/useFlags'; + +import { AclpMetricsPreferenceToggle } from './AclpMetricsPreferenceToggle'; +import LinodeSummary from './LinodeSummary/LinodeSummary'; + +interface Props { + linodeCreated: string; +} + +const LinodeMetrics = ({ linodeCreated }: Props) => { + const flags = useFlags(); + const { data: isAclpMetricsPreferenceBeta } = usePreferences( + (preferences) => preferences?.isAclpMetricsBeta + ); + + return ( + + {flags.aclpIntegration ? : null} + {flags.aclpIntegration && isAclpMetricsPreferenceBeta ? ( + // Beta ACLP Metrics View + ACLP Metrics Coming soon... + ) : ( + // Legacy Metrics View + + )} + + ); +}; + +export default LinodeMetrics; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/LinodeSummary.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/LinodeSummary.test.tsx similarity index 100% rename from packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/LinodeSummary.test.tsx rename to packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/LinodeSummary.test.tsx diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/LinodeSummary.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/LinodeSummary.tsx similarity index 100% rename from packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/LinodeSummary.tsx rename to packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/LinodeSummary.tsx diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/NetworkGraphs.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/NetworkGraphs.tsx similarity index 98% rename from packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/NetworkGraphs.tsx rename to packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/NetworkGraphs.tsx index 668ae49f14b..cc535f51d6f 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/NetworkGraphs.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/NetworkGraphs.tsx @@ -172,15 +172,8 @@ interface GraphProps { } const Graph = (props: GraphProps) => { - const { - ariaLabel, - data, - metrics, - theme, - timezone, - unit, - xAxisTickFormat, - } = props; + const { ariaLabel, data, metrics, theme, timezone, unit, xAxisTickFormat } = + props; const format = formatBitsPerSecond; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/StatsPanel.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/StatsPanel.test.tsx similarity index 100% rename from packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/StatsPanel.test.tsx rename to packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/StatsPanel.test.tsx diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/StatsPanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/StatsPanel.tsx similarity index 100% rename from packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/StatsPanel.tsx rename to packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/StatsPanel.tsx diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/helpers.ts b/packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/helpers.ts similarity index 100% rename from packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/helpers.ts rename to packages/manager/src/features/Linodes/LinodesDetail/LinodeMetrics/LinodeSummary/helpers.ts diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx index e772218809c..c45170137d4 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx @@ -1,5 +1,5 @@ -import { useLinodeQuery } from '@linode/queries'; -import { CircleProgress, ErrorState } from '@linode/ui'; +import { useLinodeQuery, usePreferences } from '@linode/queries'; +import { BetaChip, CircleProgress, ErrorState } from '@linode/ui'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; import { @@ -17,9 +17,10 @@ import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; import { SMTPRestrictionText } from 'src/features/Linodes/SMTPRestrictionText'; +import { useFlags } from 'src/hooks/useFlags'; import { useTypeQuery } from 'src/queries/types'; -const LinodeSummary = React.lazy(() => import('./LinodeSummary/LinodeSummary')); +const LinodeMetrics = React.lazy(() => import('./LinodeMetrics/LinodeMetrics')); const LinodeNetworking = React.lazy(() => import('./LinodeNetworking/LinodeNetworking').then((module) => ({ default: module.LinodeNetworking, @@ -44,6 +45,10 @@ const LinodesDetailNavigation = () => { const { data: linode, error } = useLinodeQuery(id); const { url } = useRouteMatch(); const history = useHistory(); + const flags = useFlags(); + const { data: isAclpMetricsPreferenceBeta } = usePreferences( + (preferences) => preferences?.isAclpMetricsBeta + ); const { data: type } = useTypeQuery( linode?.type ?? '', @@ -55,6 +60,10 @@ const LinodesDetailNavigation = () => { const tabs = [ { + chip: + flags.aclpIntegration && isAclpMetricsPreferenceBeta ? ( + + ) : null, routeName: `${url}/metrics`, title: 'Metrics', }, @@ -148,7 +157,7 @@ const LinodesDetailNavigation = () => { }> - + diff --git a/packages/utilities/src/types/ManagerPreferences.ts b/packages/utilities/src/types/ManagerPreferences.ts index 2ec3b7d2561..643d3de0266 100644 --- a/packages/utilities/src/types/ManagerPreferences.ts +++ b/packages/utilities/src/types/ManagerPreferences.ts @@ -31,6 +31,7 @@ export type ManagerPreferences = Partial<{ domains_group_by_tag: boolean; firewall_beta_notification: boolean; gst_banner_dismissed: boolean; + isAclpMetricsBeta: boolean; isTableStripingEnabled: boolean; linode_news_banner_dismissed: boolean; linodes_group_by_tag: boolean;