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;