diff --git a/packages/api-v4/.changeset/pr-13062-added-1762436128763.md b/packages/api-v4/.changeset/pr-13062-added-1762436128763.md new file mode 100644 index 00000000000..73d512d75e8 --- /dev/null +++ b/packages/api-v4/.changeset/pr-13062-added-1762436128763.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +Added `Akamai Cloud Pulse Logs ` to the `AccountCapability` type ([#13062](https://github.com/linode/manager/pull/13062)) diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 02fb1516200..73d008377cf 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -62,6 +62,7 @@ export type BillingSource = 'akamai' | 'linode'; export const accountCapabilities = [ 'Akamai Cloud Load Balancer', 'Akamai Cloud Pulse', + 'Akamai Cloud Pulse Logs', 'Block Storage', 'Block Storage Encryption', 'Cloud Firewall', diff --git a/packages/manager/.changeset/pr-13062-upcoming-features-1762436186094.md b/packages/manager/.changeset/pr-13062-upcoming-features-1762436186094.md new file mode 100644 index 00000000000..98df19db365 --- /dev/null +++ b/packages/manager/.changeset/pr-13062-upcoming-features-1762436186094.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Limit Logs feature based on `Akamai Cloud Pulse Logs` Account Capability ([#13062](https://github.com/linode/manager/pull/13062)) diff --git a/packages/manager/cypress/e2e/core/delivery/create-destination.spec.ts b/packages/manager/cypress/e2e/core/delivery/create-destination.spec.ts index 8b1583caba5..0418ab542b0 100644 --- a/packages/manager/cypress/e2e/core/delivery/create-destination.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/create-destination.spec.ts @@ -16,7 +16,11 @@ import type { AkamaiObjectStorageDetailsExtended } from '@linode/api-v4'; describe('Create Destination', () => { beforeEach(() => { mockAppendFeatureFlags({ - aclpLogs: { enabled: true, beta: true }, + aclpLogs: { + enabled: true, + beta: true, + bypassAccountCapabilities: true, + }, }); }); diff --git a/packages/manager/cypress/e2e/core/delivery/create-stream.spec.ts b/packages/manager/cypress/e2e/core/delivery/create-stream.spec.ts index 84fa2802a4b..1ca9b4cb643 100644 --- a/packages/manager/cypress/e2e/core/delivery/create-stream.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/create-stream.spec.ts @@ -17,7 +17,11 @@ import { kubernetesClusterFactory } from 'src/factories'; describe('Create Stream', () => { beforeEach(() => { mockAppendFeatureFlags({ - aclpLogs: { enabled: true, beta: true }, + aclpLogs: { + enabled: true, + beta: true, + bypassAccountCapabilities: true, + }, }); }); diff --git a/packages/manager/cypress/e2e/core/delivery/destinations-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/delivery/destinations-empty-landing-page.spec.ts index ff8de40f562..e9ad224226b 100644 --- a/packages/manager/cypress/e2e/core/delivery/destinations-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/destinations-empty-landing-page.spec.ts @@ -8,6 +8,7 @@ describe('Destinations empty landing page', () => { aclpLogs: { enabled: true, beta: true, + bypassAccountCapabilities: true, }, }); }); diff --git a/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts index c3bff68803e..ab17f15ce77 100644 --- a/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/destinations-non-empty-landing-page.spec.ts @@ -105,7 +105,11 @@ const mockDestinations: Destination[] = new Array(3) describe('destinations landing checks for non-empty state', () => { beforeEach(() => { mockAppendFeatureFlags({ - aclpLogs: { enabled: true, beta: true }, + aclpLogs: { + enabled: true, + beta: true, + bypassAccountCapabilities: true, + }, }); // Mock setup to display the Destinations landing page in a non-empty state diff --git a/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts b/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts index fbbd0a99328..5c9f4ba619e 100644 --- a/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts @@ -21,7 +21,11 @@ import type { AkamaiObjectStorageDetailsExtended } from '@linode/api-v4'; describe('Edit Destination', () => { beforeEach(() => { mockAppendFeatureFlags({ - aclpLogs: { enabled: true, beta: true }, + aclpLogs: { + enabled: true, + beta: true, + bypassAccountCapabilities: true, + }, }); cy.visitWithLogin(`/logs/delivery/destinations/${mockDestination.id}/edit`); mockGetDestination(mockDestination); diff --git a/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts b/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts index 2b6d5bc28c0..9006b1935be 100644 --- a/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts @@ -24,7 +24,11 @@ import { kubernetesClusterFactory } from 'src/factories'; describe('Edit Stream', () => { beforeEach(() => { mockAppendFeatureFlags({ - aclpLogs: { enabled: true, beta: true }, + aclpLogs: { + enabled: true, + beta: true, + bypassAccountCapabilities: true, + }, }); }); diff --git a/packages/manager/cypress/e2e/core/delivery/streams-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/delivery/streams-empty-landing-page.spec.ts index c0e8aadfa2c..3b24062ea15 100644 --- a/packages/manager/cypress/e2e/core/delivery/streams-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/streams-empty-landing-page.spec.ts @@ -8,6 +8,7 @@ describe('Streams empty landing page', () => { aclpLogs: { enabled: true, beta: true, + bypassAccountCapabilities: true, }, }); }); diff --git a/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts index db20e8c35ff..ad9df1e518b 100644 --- a/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/streams-non-empty-landing-page.spec.ts @@ -130,7 +130,11 @@ const mockStreams: Stream[] = new Array(3) describe('Streams non-empty landing page', () => { beforeEach(() => { mockAppendFeatureFlags({ - aclpLogs: { enabled: true, beta: true }, + aclpLogs: { + enabled: true, + beta: true, + bypassAccountCapabilities: true, + }, }); // Mock setup to display the Streams landing page in a non-empty state diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx index c560e1d0815..916dad9342c 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx @@ -333,6 +333,96 @@ describe('PrimaryNav', () => { expect(betaChip).toBeVisible(); }); + it('should show Logs menu item if user has capability and aclpLogs flag is enabled', async () => { + const account = accountFactory.build({ + capabilities: ['Akamai Cloud Pulse Logs'], + }); + server.use(http.get('*/account', () => HttpResponse.json(account))); + + const flags = { + aclpLogs: { + enabled: true, + beta: false, + bypassAccountCapabilities: false, + }, + }; + + const { findByTestId, queryByTestId } = renderWithTheme( + , + { flags } + ); + + const logsNavItem = await findByTestId('menu-item-Logs'); + expect(logsNavItem).toBeVisible(); + expect(queryByTestId('betaChip')).toBeNull(); + }); + + it('should not show Logs menu item if aclpLogs flag is not enabled', async () => { + const account = accountFactory.build({ + capabilities: ['Akamai Cloud Pulse Logs'], + }); + server.use(http.get('*/account', () => HttpResponse.json(account))); + + const flags = { + aclpLogs: { + enabled: false, + beta: false, + bypassAccountCapabilities: true, + }, + }; + + const { queryByTestId } = renderWithTheme(, { + flags, + }); + + expect(queryByTestId('menu-item-Logs')).toBeNull(); + }); + + it('should show Logs menu item if user lacks capability, bypassAccountCapabilities is true and aclpLogs flag is enabled', async () => { + const account = accountFactory.build({ + capabilities: [], + }); + server.use(http.get('*/account', () => HttpResponse.json(account))); + + const flags = { + aclpLogs: { + enabled: true, + beta: true, + bypassAccountCapabilities: true, + }, + }; + + const { findByTestId } = renderWithTheme(, { + flags, + }); + + const logsNavItem = await findByTestId('menu-item-Logs'); + expect(logsNavItem).toBeVisible(); + const betaChip = await findByTestId('betaChip'); + expect(betaChip).toBeVisible(); + }); + + it('should not show Logs menu item if user lacks capability, bypassAccountCapabilities is false and aclpLogs flag is enabled', async () => { + const account = accountFactory.build({ + capabilities: [], + }); + server.use(http.get('*/account', () => HttpResponse.json(account))); + + const flags = { + aclpLogs: { + enabled: true, + beta: false, + bypassAccountCapabilities: false, + }, + }; + + const { queryByTestId } = renderWithTheme(, { + flags, + }); + + expect(queryByTestId('menu-item-Logs')).toBeNull(); + }); + it('should show Administration links if iamRbacPrimaryNavChanges flag is enabled', async () => { const flags: Partial = { iamRbacPrimaryNavChanges: true, diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index 4c14e98bbf3..a3f43866a76 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -20,6 +20,7 @@ import { } from 'src/components/PrimaryNav/constants'; import { useIsACLPEnabled } from 'src/features/CloudPulse/Utils/utils'; import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; +import { useIsACLPLogsEnabled } from 'src/features/Delivery/deliveryUtils'; import { useIsIAMEnabled } from 'src/features/IAM/hooks/useIsIAMEnabled'; import { useIsPlacementGroupsEnabled } from 'src/features/PlacementGroups/utils'; import { useFlags } from 'src/hooks/useFlags'; @@ -99,6 +100,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => { const isManaged = accountSettings?.managed ?? false; const { isACLPEnabled } = useIsACLPEnabled(); + const { isACLPLogsEnabled, isACLPLogsBeta } = useIsACLPLogsEnabled(); const isAlertsEnabled = isACLPEnabled && @@ -241,9 +243,9 @@ export const PrimaryNav = (props: PrimaryNavProps) => { }, { display: 'Logs', - hide: !flags.aclpLogs?.enabled, + hide: !isACLPLogsEnabled, to: '/logs/delivery', - isBeta: flags.aclpLogs?.beta, + isBeta: isACLPLogsBeta, }, ], name: 'Monitor', @@ -330,6 +332,8 @@ export const PrimaryNav = (props: PrimaryNavProps) => { isManaged, isPlacementGroupsEnabled, isACLPEnabled, + isACLPLogsBeta, + isACLPLogsEnabled, isIAMBeta, isIAMEnabled, iamRbacPrimaryNavChanges, diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index ac48bcb0230..ce42d10a2e6 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -105,6 +105,13 @@ interface AclpFlag { showWidgetDimensionFilters?: boolean; } +interface AclpLogsFlag extends BetaFeatureFlag { + /** + * This property indicates whether to bypass account capabilities check or not + */ + bypassAccountCapabilities?: boolean; +} + interface LkeEnterpriseFlag extends BaseFeatureFlag { ga: boolean; la: boolean; @@ -169,7 +176,7 @@ export interface Flags { aclp: AclpFlag; aclpAlerting: AclpAlerting; aclpAlertServiceTypeConfig: AclpAlertServiceTypeConfig[]; - aclpLogs: BetaFeatureFlag; + aclpLogs: AclpLogsFlag; aclpReadEndpoint: string; aclpResourceTypeMap: CloudPulseResourceTypeMapFlag[]; aclpServices: Partial; diff --git a/packages/manager/src/features/Delivery/deliveryUtils.ts b/packages/manager/src/features/Delivery/deliveryUtils.ts index 4ad6dddbbf1..4710be9407e 100644 --- a/packages/manager/src/features/Delivery/deliveryUtils.ts +++ b/packages/manager/src/features/Delivery/deliveryUtils.ts @@ -7,12 +7,15 @@ import { type StreamType, streamType, } from '@linode/api-v4'; +import { useAccount } from '@linode/queries'; import { omitProps } from '@linode/ui'; +import { isFeatureEnabledV2 } from '@linode/utilities'; import { destinationTypeOptions, streamTypeOptions, } from 'src/features/Delivery/Shared/types'; +import { useFlags } from 'src/hooks/useFlags'; import type { AutocompleteOption, @@ -20,6 +23,32 @@ import type { FormMode, } from 'src/features/Delivery/Shared/types'; +/** + * Hook to determine if the ACLP Logs feature is enabled for the current user. + + * @returns {{ isACLPLogsEnabled: boolean, isACLPLogsBeta: boolean }} An object indicating if the feature is enabled and if it is in beta. + */ +export const useIsACLPLogsEnabled = (): { + isACLPLogsBeta: boolean; + isACLPLogsEnabled: boolean; +} => { + const { data: account } = useAccount(); + const flags = useFlags(); + + const isACLPLogsEnabled = + (flags.aclpLogs?.enabled && flags.aclpLogs?.bypassAccountCapabilities) || + isFeatureEnabledV2( + 'Akamai Cloud Pulse Logs', + !!flags.aclpLogs?.enabled, + account?.capabilities ?? [] + ); + + return { + isACLPLogsBeta: !!flags.aclpLogs?.beta, + isACLPLogsEnabled, + }; +}; + export const getDestinationTypeOption = ( destinationTypeValue: string ): AutocompleteOption | undefined => diff --git a/packages/manager/src/routes/delivery/DeliveryRoute.tsx b/packages/manager/src/routes/delivery/DeliveryRoute.tsx index b5b3166f892..2a79b5ffee1 100644 --- a/packages/manager/src/routes/delivery/DeliveryRoute.tsx +++ b/packages/manager/src/routes/delivery/DeliveryRoute.tsx @@ -3,14 +3,14 @@ import { Outlet } from '@tanstack/react-router'; import React from 'react'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; -import { useFlags } from 'src/hooks/useFlags'; +import { useIsACLPLogsEnabled } from 'src/features/Delivery/deliveryUtils'; export const DeliveryRoute = () => { - const flags = useFlags(); - const { aclpLogs } = flags; + const { isACLPLogsEnabled } = useIsACLPLogsEnabled(); + return ( }> - {aclpLogs?.enabled ? : } + {isACLPLogsEnabled ? : } ); };