diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index 1bc8a5eb477..f7407c4d4c8 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,3 +1,20 @@ +## [2026-01-26] - v0.156.0 + + +### Fixed: + +- IAM Delegation: fix payload for updateChildAccountDelegates ([#13260](https://github.com/linode/manager/pull/13260)) + +### Tech Stories: + +- Clean up unused VPC IPv6 Large Prefixes tag ([#13245](https://github.com/linode/manager/pull/13245)) + +### Upcoming Features: + +- CloudPulse-Alerts: Add `DeleteChannelPayload` type and request for deletion of a notification channel ([#13256](https://github.com/linode/manager/pull/13256)) +- Added locks property to Linode interface,added lock create and delete event keys, refactored Lock types ([#13286](https://github.com/linode/manager/pull/13286)) +- New type `NotificationChannelAlerts`, request `getAlertsByNotificationChannelId` to fetch alerts associated to a notification channel ([#13294](https://github.com/linode/manager/pull/13294)) + ## [2026-01-12] - v0.155.0 diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index adf478a78a7..47665198bc8 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.155.0", + "version": "0.156.0", "homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4", "bugs": { "url": "https://github.com/linode/manager/issues" diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 9c952bd2dd7..22956b38a27 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -93,7 +93,6 @@ export const accountCapabilities = [ 'Vlans', 'VPCs', 'VPC Dual Stack', - 'VPC IPv6 Large Prefixes', ] as const; export type AccountCapability = (typeof accountCapabilities)[number]; @@ -435,6 +434,8 @@ export const EventActionKeys = [ 'lke_pool_delete', 'lke_pool_recycle', 'lke_token_rotate', + 'lock_create', + 'lock_delete', 'longviewclient_create', 'longviewclient_delete', 'longviewclient_update', diff --git a/packages/api-v4/src/cloudpulse/alerts.ts b/packages/api-v4/src/cloudpulse/alerts.ts index 33e59842ada..5fecd5b21e4 100644 --- a/packages/api-v4/src/cloudpulse/alerts.ts +++ b/packages/api-v4/src/cloudpulse/alerts.ts @@ -23,6 +23,7 @@ import type { EditAlertDefinitionPayload, EditNotificationChannelPayload, NotificationChannel, + NotificationChannelAlerts, } from './types'; export const createAlertDefinition = ( @@ -172,3 +173,25 @@ export const updateNotificationChannel = ( setMethod('PUT'), setData(data, editNotificationChannelPayloadSchema), ); + +export const deleteNotificationChannel = (channelId: number) => + Request( + setURL( + `${API_ROOT}/monitor/alert-channels/${encodeURIComponent(channelId)}`, + ), + setMethod('DELETE'), + ); + +export const getAlertsByNotificationChannelId = ( + channelId: number, + params?: Params, + filters?: Filter, +) => + Request>( + setURL( + `${API_ROOT}/monitor/alert-channels/${encodeURIComponent(channelId)}/alerts`, + ), + setMethod('GET'), + setParams(params), + setXFilter(filters), + ); diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index 43f725b6c7d..58661ef0b5e 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -489,3 +489,18 @@ export interface EditNotificationChannelPayloadWithId */ channelId: number; } + +export interface DeleteChannelPayload { + /** + * The ID of the channel to delete. + */ + channelId: number; +} + +export interface NotificationChannelAlerts { + id: number; + label: string; + service_type: CloudPulseServiceType; + type: 'alerts-definitions'; + url: string; +} diff --git a/packages/api-v4/src/delivery/types.ts b/packages/api-v4/src/delivery/types.ts index cb8691f8211..84f2d82460a 100644 --- a/packages/api-v4/src/delivery/types.ts +++ b/packages/api-v4/src/delivery/types.ts @@ -1,6 +1,7 @@ export const streamStatus = { Active: 'active', Inactive: 'inactive', + Provisioning: 'provisioning', } as const; export type StreamStatus = (typeof streamStatus)[keyof typeof streamStatus]; diff --git a/packages/api-v4/src/iam/delegation.ts b/packages/api-v4/src/iam/delegation.ts index 333546ad6e9..22e5da8f911 100644 --- a/packages/api-v4/src/iam/delegation.ts +++ b/packages/api-v4/src/iam/delegation.ts @@ -64,7 +64,7 @@ export const updateChildAccountDelegates = ({ `${BETA_API_ROOT}/iam/delegation/child-accounts/${encodeURIComponent(euuid)}/users`, ), setMethod('PUT'), - setData(users), + setData({ users }), ); export const getMyDelegatedChildAccounts = ({ diff --git a/packages/api-v4/src/iam/types.ts b/packages/api-v4/src/iam/types.ts index 3ffeaa29952..4196c0c25cb 100644 --- a/packages/api-v4/src/iam/types.ts +++ b/packages/api-v4/src/iam/types.ts @@ -97,9 +97,12 @@ export type AccountAdmin = | 'list_default_firewalls' | 'list_delegate_users' | 'list_enrolled_beta_programs' + | 'list_entities' + | 'list_role_permissions' | 'list_service_transfers' | 'list_user_delegate_accounts' | 'list_user_grants' + | 'list_user_permissions' | 'revoke_profile_app' | 'revoke_profile_device' | 'send_profile_phone_number_verification_code' @@ -254,8 +257,11 @@ export type AccountViewer = | 'list_available_services' | 'list_default_firewalls' | 'list_enrolled_beta_programs' + | 'list_entities' + | 'list_role_permissions' | 'list_service_transfers' | 'list_user_grants' + | 'list_user_permissions' | 'view_account' | 'view_account_login' | 'view_account_settings' diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index 0d298c662a9..04e915b48ad 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -1,5 +1,6 @@ import type { MaintenancePolicySlug } from '../account/types'; import type { CloudPulseAlertsPayload } from '../cloudpulse/types'; +import type { LockType } from '../locks/types'; import type { IPAddress, IPRange } from '../networking/types'; import type { LinodePlacementGroupPayload } from '../placement-groups/types'; import type { Region, RegionSite } from '../regions'; @@ -44,6 +45,7 @@ export interface Linode { ipv6: null | string; label: string; lke_cluster_id: null | number; + locks: LockType[]; maintenance_policy?: MaintenancePolicySlug; placement_group: LinodePlacementGroupPayload | null; region: string; diff --git a/packages/api-v4/src/locks/types.ts b/packages/api-v4/src/locks/types.ts index 5528af7b249..e5d8e7d5c2c 100644 --- a/packages/api-v4/src/locks/types.ts +++ b/packages/api-v4/src/locks/types.ts @@ -1,3 +1,4 @@ +import type { Entity } from '../account/types'; import type { EntityType } from '../entities'; /** @@ -5,16 +6,6 @@ import type { EntityType } from '../entities'; */ export type LockType = 'cannot_delete' | 'cannot_delete_with_subresources'; -/** - * Entity information attached to a lock - */ -export interface LockEntity { - id: number | string; - label?: string; - type: EntityType; - url?: string; -} - /** * Request payload for creating a lock * POST /v4beta/locks @@ -34,7 +25,7 @@ export interface CreateLockPayload { */ export interface ResourceLock { /** Information about the locked entity */ - entity: LockEntity; + entity: Entity; /** Unique identifier for the lock */ id: number; /** Type of lock applied */ diff --git a/packages/api-v4/src/marketplace/marketplace.ts b/packages/api-v4/src/marketplace/marketplace.ts index e253226a592..845326f7807 100644 --- a/packages/api-v4/src/marketplace/marketplace.ts +++ b/packages/api-v4/src/marketplace/marketplace.ts @@ -29,7 +29,7 @@ export const getMarketplaceProducts = (params?: Params, filters?: Filter) => export const getMarketplaceProduct = (productId: number) => Request( setURL( - `${BETA_API_ROOT}/marketplace/products/${encodeURIComponent(productId)}`, + `${BETA_API_ROOT}/marketplace/products/${encodeURIComponent(productId)}/details`, ), setMethod('GET'), ); diff --git a/packages/api-v4/src/marketplace/types.ts b/packages/api-v4/src/marketplace/types.ts index 430f4dc395d..179b7591c0e 100644 --- a/packages/api-v4/src/marketplace/types.ts +++ b/packages/api-v4/src/marketplace/types.ts @@ -1,42 +1,59 @@ export interface MarketplaceProductDetail { - documentation: string; - overview: { + documentation?: string; + overview?: { description: string; }; - pricing: string; - support: string; + pricing?: string; + support?: string; } export interface MarketplaceProduct { category_ids: number[]; + created_at: string; + created_by: string; details?: MarketplaceProductDetail; id: number; info_banner?: string; + logo_url: string; name: string; partner_id: number; product_tags?: string[]; short_description: string; - title_tag?: string; + tile_tag?: string; type_id: number; + updated_at?: string; + updated_by?: string; } export interface MarketplaceCategory { - category: string; + created_at: string; + created_by: string; id: number; - product_count: number; + name: string; + products_count: number; + updated_at?: string; + updated_by?: string; } export interface MarketplaceType { + created_at: string; + created_by: string; id: number; name: string; - product_count: number; + products_count: number; + updated_at?: string; + updated_by?: string; } export interface MarketplacePartner { + created_at: string; + created_by: string; id: number; + logo_url_dark_mode: string; logo_url_light_mode: string; - logo_url_night_mode?: string; name: string; + updated_at?: string; + updated_by?: string; url: string; } diff --git a/packages/manager/.changeset/pr-13299-upcoming-features-1768999121997.md b/packages/manager/.changeset/pr-13299-upcoming-features-1768999121997.md new file mode 100644 index 00000000000..5ba4c7ca4d2 --- /dev/null +++ b/packages/manager/.changeset/pr-13299-upcoming-features-1768999121997.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Fix error handling in ChildAccountList component ([#13299](https://github.com/linode/manager/pull/13299)) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index b4bfcd99a93..d9126407e8b 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,6 +4,72 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2026-01-26] - v1.158.0 + + +### Added: + +- Add Mistral 7B instruct and ChromaDB to the Marketplace ([#13270](https://github.com/linode/manager/pull/13270)) +- Logs Stream - Provisioning status ([#13284](https://github.com/linode/manager/pull/13284)) + +### Changed: + +- Default selection of network interface type to linode interface in Linode create flow ([#13221](https://github.com/linode/manager/pull/13221)) +- Update Generational Plans default sort to show newest (G8) -> oldest (G6) ([#13234](https://github.com/linode/manager/pull/13234)) +- Billing: Disable 'Make payment'button for Akamai users ([#13243](https://github.com/linode/manager/pull/13243)) +- NLB post-demo feedback-fix empty state title casing,rename LKE-E to Cluster and adjust column visibility for smaller screens to prioritize IPv6 ([#13250](https://github.com/linode/manager/pull/13250)) +- NLB post-demo feedback-Resolve special character filtering issue in Nodes table IPv6 column and add NLB to GoTo quick navigation ([#13251](https://github.com/linode/manager/pull/13251)) +- Changes related to private IP field in create linode flow ([#13253](https://github.com/linode/manager/pull/13253)) +- Optimize NLB table column widths and add back button on detail pages ([#13258](https://github.com/linode/manager/pull/13258)) +- Improvements in Add Network interface drawer ([#13264](https://github.com/linode/manager/pull/13264)) +- Handle Incompatibility of linode interfaces in create LKE flow ([#13272](https://github.com/linode/manager/pull/13272)) +- Apply UX and user feedback for linode interfaces feature in Account settings page ([#13280](https://github.com/linode/manager/pull/13280)) +- Apply UX and user feedback for linode interfaces feature in Create linode page ([#13281](https://github.com/linode/manager/pull/13281)) +- Logs texts updates after tech writing review ([#13291](https://github.com/linode/manager/pull/13291)) +- Update design-language-system to v5.3.2 ([#13293](https://github.com/linode/manager/pull/13293)) + +### Fixed: + +- Hide dual stack option if no IPv6 prefixes available in create VPC flow ([#13245](https://github.com/linode/manager/pull/13245)) +- IAM Delegation: UX copy update, wrong breadcrumb fix ([#13259](https://github.com/linode/manager/pull/13259)) +- IAM Delegation: fix payload for updateChildAccountDelegates ([#13260](https://github.com/linode/manager/pull/13260)) +- IAM hydration error on User Detail pages ([#13265](https://github.com/linode/manager/pull/13265)) +- IAM: removing entity/role can cause an empty page ([#13268](https://github.com/linode/manager/pull/13268)) +- IAM Delegation: UI issues in Default Entity Access table, Default Roles labels/messages, and missing Make a Payment tooltip ([#13275](https://github.com/linode/manager/pull/13275)) +- IAM Delegation: fix payload for changing role flow ([#13279](https://github.com/linode/manager/pull/13279)) +- IAM Delegation: User selector not working in Assign Role/Roles drawer ([#13282](https://github.com/linode/manager/pull/13282)) +- IAM: changing entity/role can cause an empty page ([#13285](https://github.com/linode/manager/pull/13285)) +- Wrong time range sent in metrics payload on preference reload in `CloudPulse metrics` ([#13287](https://github.com/linode/manager/pull/13287)) +- IAM routing cleanup ([#13288](https://github.com/linode/manager/pull/13288)) +- Copy in Plans Panel generational plans tooltip ([#13289](https://github.com/linode/manager/pull/13289)) +- ACLP-Alerting List sorting from service_type to service_type label ([#13295](https://github.com/linode/manager/pull/13295)) +- End character validation for ACLP-Alerting Notification Channel form for name field ([#13297](https://github.com/linode/manager/pull/13297)) +- IAM DElegation: remove restriction to update user delegation with empty array, update the delegations after reopening a drawer ([#13300](https://github.com/linode/manager/pull/13300)) + +### Tech Stories: + +- IAM: Cleanup `iamRbacPrimaryNavChanges` feature flag ([#13232](https://github.com/linode/manager/pull/13232)) +- Bump jspdf to 4.0.0 ([#13248](https://github.com/linode/manager/pull/13248)) +- IAM - Clean up beta flag + BETA/LA logic ([#13266](https://github.com/linode/manager/pull/13266)) + +### Tests: + +- Fix time range specification in `timerange-verification.spec.ts` ([#13240](https://github.com/linode/manager/pull/13240)) +- Fix issue in 'chooseRegion' util when specifying an override region ([#13277](https://github.com/linode/manager/pull/13277)) + +### Upcoming Features: + +- CloudPulse-Alerts: Filter linode resources based on associated aclp alerts ([#13163](https://github.com/linode/manager/pull/13163)) +- Add reusable Product Selection Card component for Marketplace ([#13247](https://github.com/linode/manager/pull/13247)) +- CloudPulse-Alerts: Add support for delete action for user alert channels ([#13256](https://github.com/linode/manager/pull/13256)) +- Add Breadcrumb to Marketplace product landing page ([#13257](https://github.com/linode/manager/pull/13257)) +- Add service URIs to Database summary tab ([#13261](https://github.com/linode/manager/pull/13261)) +- Implement the main product grid with category grouping ([#13267](https://github.com/linode/manager/pull/13267)) +- IAM Parent/Child - Various fixes to Parent Account Flow ([#13278](https://github.com/linode/manager/pull/13278)) +- Add MSW crud for Resource Locking feature(RESPROT2) ([#13286](https://github.com/linode/manager/pull/13286)) +- Associated Alerts Table to ACLP-Alerting Notification Channel Details ([#13294](https://github.com/linode/manager/pull/13294)) +- CloudPulse-Alerts: Exclude account/region alerts in api payload while updating alerts for a linode and fix state reset issue on save ([#13301](https://github.com/linode/manager/pull/13301)) + ## [2026-01-12] - v1.157.0 diff --git a/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts b/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts index 14e4ada60c6..9890547fdbc 100644 --- a/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts @@ -27,8 +27,6 @@ describe('Maintenance', () => { // TODO When the Host & VM Maintenance feature rolls out, we want to enable the feature flag and update the test. mockAppendFeatureFlags({ - // TODO M3-10491 - Remove "iamRbacPrimaryNavChanges" feature flag mock once feature flag is deleted. - iamRbacPrimaryNavChanges: true, vmHostMaintenance: { enabled: false, }, diff --git a/packages/manager/cypress/e2e/core/account/network-settings.spec.ts b/packages/manager/cypress/e2e/core/account/network-settings.spec.ts index ef19a5788e8..50a9cea238f 100644 --- a/packages/manager/cypress/e2e/core/account/network-settings.spec.ts +++ b/packages/manager/cypress/e2e/core/account/network-settings.spec.ts @@ -28,9 +28,9 @@ import type { LinodeInterfaceAccountSetting } from '@linode/api-v4'; const interfaceTypeMap = { legacy_config_default_but_linode_allowed: - 'Configuration Profile Interfaces but allow Linode Interfaces', + 'Configuration Profile Interfaces (default) but allow Linode Interfaces', linode_default_but_legacy_config_allowed: - 'Linode Interfaces but allow Configuration Profile Interfaces', + 'Linode Interfaces (default) but allow Configuration Profile Interfaces', legacy_config_only: 'Configuration Profile Interfaces Only', linode_only: 'Linode Interfaces Only', }; @@ -54,7 +54,7 @@ describe('Account network settings', () => { describe('Network interface types', () => { /* * - Confirms that customers can update their account-level Linode interface type. - * - Confirms that "Interfaces for new Linodes" drop-down displays user's set value on page load. + * - Confirms that "Allowed interfaces for new Linodes" drop-down displays user's set value on page load. * - Confirms that save button is initially disabled, but becomes enabled upon changing the selection. * - Confirms that outgoing API request contains expected payload data for chosen interface type. * - Confirms that toast appears upon successful settings update operation. @@ -81,7 +81,7 @@ describe('Account network settings', () => { // Confirm that selected interface type matches API response, and that // "Save" button is disabled by default. - cy.findByLabelText('Interfaces for new Linodes').should( + cy.findByLabelText('Allowed interfaces for new Linodes').should( 'have.value', interfaceTypeMap[defaultInterface] ); @@ -89,7 +89,7 @@ describe('Account network settings', () => { // Confirm that changing selection causes "Save" button to become enabled, // then changing back causes it to become disabled again. - cy.findByLabelText('Interfaces for new Linodes').click(); + cy.findByLabelText('Allowed interfaces for new Linodes').click(); ui.autocompletePopper.find().within(() => { cy.findByText(interfaceTypeMap[otherInterfaces[0]]) .should('be.visible') @@ -97,7 +97,7 @@ describe('Account network settings', () => { }); ui.button.findByTitle('Save').should('be.enabled'); - cy.findByLabelText('Interfaces for new Linodes').click(); + cy.findByLabelText('Allowed interfaces for new Linodes').click(); ui.autocompletePopper.find().within(() => { cy.findByText(interfaceTypeMap[defaultInterface]) .should('be.visible') @@ -108,7 +108,7 @@ describe('Account network settings', () => { // Confirm that we can update our setting using every other choice, // and that the outgoing API request payload contains the expected value. otherInterfaces.forEach((otherInterface, index) => { - cy.findByLabelText('Interfaces for new Linodes').click(); + cy.findByLabelText('Allowed interfaces for new Linodes').click(); ui.autocompletePopper.find().within(() => { cy.findByText(interfaceTypeMap[otherInterface]) .should('be.visible') @@ -161,7 +161,7 @@ describe('Account network settings', () => { .should('be.visible') .within(() => { cy.findByText('Network Interface Type').should('be.visible'); - cy.findByLabelText('Interfaces for new Linodes') + cy.findByLabelText('Allowed interfaces for new Linodes') .should('be.visible') .click(); ui.autocompletePopper.find().within(() => { diff --git a/packages/manager/cypress/e2e/core/account/quotas-nav.spec.ts b/packages/manager/cypress/e2e/core/account/quotas-nav.spec.ts index 6d972eb36b9..bfaf543c4a2 100644 --- a/packages/manager/cypress/e2e/core/account/quotas-nav.spec.ts +++ b/packages/manager/cypress/e2e/core/account/quotas-nav.spec.ts @@ -1,94 +1,12 @@ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; -describe('Quotas accessible when limitsEvolution feature flag enabled', () => { - // TODO M3-10491 - Remove `describe` block and move tests to parent scope once `iamRbacPrimaryNavChanges` feature flag is removed. - describe('When IAM RBAC account navigation feature flag is enabled', () => { - beforeEach(() => { - // TODO M3-10003 - Remove mock once `limitsEvolution` feature flag is removed. - mockAppendFeatureFlags({ - limitsEvolution: { - enabled: true, - }, - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` mock once feature flag is removed. - iamRbacPrimaryNavChanges: true, - }).as('getFeatureFlags'); - }); - - it('can navigate directly to Quotas page', () => { - cy.visitWithLogin('/quotas'); - cy.wait('@getFeatureFlags'); - cy.url().should('endWith', '/quotas'); - cy.contains( - 'View your Object Storage quotas by applying the endpoint filter below' - ).should('be.visible'); - }); - - it('can navigate to the Quotas page via the User Menu', () => { - cy.visitWithLogin('/'); - cy.wait('@getFeatureFlags'); - // Open user menu - ui.userMenuButton.find().click(); - ui.userMenu.find().within(() => { - cy.get('[data-testid="menu-item-Quotas"]').should('be.visible').click(); - cy.url().should('endWith', '/quotas'); - }); - }); - }); - - // TODO M3-10491 - Remove `describe` block and tests once "iamRbacPrimaryNavChanges" feature flag is removed. - describe('When IAM RBAC account navigation feature flag is disabled', () => { - beforeEach(() => { - mockAppendFeatureFlags({ - limitsEvolution: { - enabled: true, - }, - iamRbacPrimaryNavChanges: false, - }).as('getFeatureFlags'); - }); - - it('can navigate directly to Quotas page', () => { - cy.visitWithLogin('/account/quotas'); - cy.wait('@getFeatureFlags'); - cy.url().should('endWith', '/account/quotas'); - cy.contains( - 'View your Object Storage quotas by applying the endpoint filter below' - ).should('be.visible'); - }); - - it('can navigate to the Quotas page via the User Menu', () => { - cy.visitWithLogin('/'); - cy.wait('@getFeatureFlags'); - // Open user menu - ui.userMenuButton.find().click(); - ui.userMenu.find().within(() => { - cy.get('[data-testid="menu-item-Quotas"]').should('be.visible').click(); - cy.url().should('endWith', '/quotas'); - }); - }); - - it('Quotas tab is visible from all other tabs in Account tablist', () => { - cy.visitWithLogin('/account/billing'); - cy.wait('@getFeatureFlags'); - ui.tabList.find().within(() => { - cy.get('a').each(($link) => { - cy.wrap($link).click(); - cy.get('[data-testid="Quotas"]').should('be.visible'); - }); - }); - cy.get('[data-testid="Quotas"]').should('be.visible').click(); - cy.url().should('endWith', '/quotas'); - }); - }); -}); - describe('Quotas inaccessible when limitsEvolution feature flag disabled', () => { beforeEach(() => { mockAppendFeatureFlags({ limitsEvolution: { enabled: false, }, - iamRbacPrimaryNavChanges: true, }).as('getFeatureFlags'); }); diff --git a/packages/manager/cypress/e2e/core/account/restricted-user-details-pages.spec.ts b/packages/manager/cypress/e2e/core/account/restricted-user-details-pages.spec.ts index 61c93e1f830..a03280a344b 100644 --- a/packages/manager/cypress/e2e/core/account/restricted-user-details-pages.spec.ts +++ b/packages/manager/cypress/e2e/core/account/restricted-user-details-pages.spec.ts @@ -106,8 +106,6 @@ describe('restricted user details pages', () => { mockAppendFeatureFlags({ apl: false, dbaasV2: { beta: false, enabled: false }, - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. - iamRbacPrimaryNavChanges: true, }); }); diff --git a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts index 6d0d8e4649e..16d14933dab 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -14,7 +14,6 @@ import { mockInitiateEntityTransferError, mockReceiveEntityTransfer, } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; @@ -127,14 +126,6 @@ describe('Account service transfers', () => { cleanUp(['service-transfers', 'linodes', 'lke-clusters']); }); - beforeEach(() => { - // Mock the iamRbacPrimaryNavChanges feature flag to be disabled. - mockAppendFeatureFlags({ - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. - iamRbacPrimaryNavChanges: true, - }).as('getFeatureFlags'); - }); - /* * - Confirms user can navigate to service transfer page via user menu. */ diff --git a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts index 8efd2e884d0..3a392ef5f89 100644 --- a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts @@ -170,9 +170,7 @@ const assertBillingAccessSelected = ( describe('User permission management', () => { beforeEach(() => { - // TODO M3-10003 - Remove mock once `limitsEvolution` feature flag is removed. mockAppendFeatureFlags({ - iamRbacPrimaryNavChanges: true, iam: { enabled: false, }, diff --git a/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts b/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts index b343ac638d5..91afe8661f6 100644 --- a/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts @@ -81,9 +81,7 @@ const initTestUsers = (profile: Profile, enableChildAccountAccess: boolean) => { describe('Users landing page', () => { beforeEach(() => { - // TODO M3-10003 - Remove mock once `limitsEvolution` feature flag is removed. mockAppendFeatureFlags({ - iamRbacPrimaryNavChanges: true, iam: { enabled: false, }, diff --git a/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts b/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts index 90de20fa000..774e8461400 100644 --- a/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts @@ -9,7 +9,6 @@ import { mockGetInvoice, mockGetInvoiceItems, } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import { buildArray } from 'support/util/arrays'; import { formatUsd } from 'support/util/currency'; @@ -31,12 +30,6 @@ const getRegionLabel = (regionId: string) => { }; describe('Account invoices', () => { - beforeEach(() => { - mockAppendFeatureFlags({ - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. - iamRbacPrimaryNavChanges: true, - }); - }); /* * - Confirms that invoice items are listed on invoice details page using mock API data. * - Confirms that each invoice item is displayed with correct accompanying info. diff --git a/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts b/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts index 4ebaf70e8be..758d517e18d 100644 --- a/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/credit-card-expired-banner.spec.ts @@ -1,5 +1,4 @@ import { mockGetAccount } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetUserPreferences } from 'support/intercepts/profile'; import { ui } from 'support/ui'; @@ -13,10 +12,6 @@ describe('Credit Card Expired Banner', () => { mockGetUserPreferences({ dismissed_notifications: {}, }); - mockAppendFeatureFlags({ - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. - iamRbacPrimaryNavChanges: true, - }).as('getFeatureFlags'); }); it('appears when the expiration date is in the past', () => { diff --git a/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts b/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts index b13bfd3f8c9..4b164903407 100644 --- a/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts @@ -1,5 +1,4 @@ import { mockGetPaymentMethods } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import type { CreditCardData, PaymentMethod } from '@linode/api-v4'; @@ -57,13 +56,6 @@ const braintreeURL = 'https://+(payments.braintree-api.com|payments.sandbox.braintree-api.com)/*'; describe('Google Pay', () => { - beforeEach(() => { - mockAppendFeatureFlags({ - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. - iamRbacPrimaryNavChanges: true, - }); - }); - it('adds google pay method', () => { cy.intercept(braintreeURL).as('braintree'); mockGetPaymentMethods(mockPaymentMethods).as('getPaymentMethods'); diff --git a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts index febde9e4661..4a95323e0c7 100644 --- a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts @@ -6,7 +6,6 @@ import { grantsFactory, profileFactory } from '@linode/utilities'; import { paymentMethodFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { mockGetPaymentMethods, mockGetUser } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetProfile, mockGetProfileGrants, @@ -224,13 +223,6 @@ const assertMakeAPaymentEnabled = () => { describe('restricted user billing flows', () => { beforeEach(() => { mockGetPaymentMethods(mockPaymentMethods); - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. - mockAppendFeatureFlags({ - iamRbacPrimaryNavChanges: true, - iam: { - enabled: false, - }, - }); }); /* @@ -267,8 +259,7 @@ describe('restricted user billing flows', () => { assertEditBillingInfoDisabled(restrictedUserTooltip); assertAddPaymentMethodDisabled(restrictedUserTooltip); assertMakeAPaymentDisabled( - restrictedUserTooltip + - ` Please contact your ${ADMINISTRATOR} to request the necessary permissions.` + `You don't have permissions to make a payment. Please contact your ${ADMINISTRATOR} to request the necessary permissions.` ); }); @@ -297,8 +288,7 @@ describe('restricted user billing flows', () => { assertEditBillingInfoDisabled(restrictedUserTooltip); assertAddPaymentMethodDisabled(restrictedUserTooltip); assertMakeAPaymentDisabled( - restrictedUserTooltip + - ` Please contact your ${PARENT_USER} to request the necessary permissions.` + `You don't have permissions to make a payment. Please contact your ${PARENT_USER} to request the necessary permissions.` ); }); diff --git a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts index d130776eb56..7d5925f57a8 100644 --- a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts @@ -118,8 +118,6 @@ authenticate(); describe('Billing Activity Feed', () => { beforeEach(() => { mockAppendFeatureFlags({ - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. - iamRbacPrimaryNavChanges: true, iam: { enabled: false, }, diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts index adb9afa816c..f2b1701a8de 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts @@ -235,7 +235,7 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { { ascending: [2, 4, 1, 3], column: 'status', descending: [1, 3, 2, 4] }, { ascending: [1, 2, 3, 4], - column: 'service_type', + column: 'service_type_label', descending: [3, 4, 1, 2], }, { diff --git a/packages/manager/cypress/e2e/core/cloudpulse/groupby-tags.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/groupby-tags.spec.ts index e6eb5094c01..8f6165c5ce7 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/groupby-tags.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/groupby-tags.spec.ts @@ -99,7 +99,7 @@ describe('Integration Tests for Grouping Alerts by Tags on the CloudPulse Alerts ui.button.findByAttribute('aria-label', 'Toggle group by tag').click(); // Validate table headers are visible - ['label', 'status', 'service_type', 'created_by', 'updated'].forEach( + ['label', 'status', 'service_type_label', 'created_by', 'updated'].forEach( (header) => { ui.heading.findByText(header).should('be.visible'); } diff --git a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts index 416da6e8546..e6c47d2cd93 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts @@ -99,49 +99,6 @@ const databaseMock: Database = databaseFactory.build({ const mockProfile = profileFactory.build({ timezone: 'UTC', }); -/** - * Generates a date in Indian Standard Time (IST) based on a specified number of days offset, - * hour, and minute. The function also provides individual date components such as day, hour, - * minute, month, and AM/PM. - * - * @param {number} daysOffset - The number of days to adjust from the current date. Positive - * values give a future date, negative values give a past date. - * @param {number} hour - The hour to set for the resulting date (0-23). - * @param {number} [minute=0] - The minute to set for the resulting date (0-59). Defaults to 0. - * - * @returns {Object} - Returns an object containing: - * - `actualDate`: The formatted date and time in GMT (YYYY-MM-DD HH:mm). - * - `day`: The day of the month as a number. - * - `hour`: The hour in the 24-hour format as a number. - * - `minute`: The minute of the hour as a number. - * - `month`: The month of the year as a number. - */ -const getDateRangeInGMT = ( - hour: number, - minute: number = 0, - isStart: boolean = false -) => { - const now = DateTime.now().setZone('GMT'); // Set the timezone to GMT - const targetDate = isStart - ? now.startOf('month').set({ hour, minute }).setZone('GMT') - : now.set({ hour, minute }).setZone('GMT'); - const actualDate = targetDate.setZone('GMT').toFormat('yyyy-LL-dd HH:mm'); - - const previousMonthDate = targetDate.minus({ months: 1 }); - - return { - actualDate, - day: targetDate.day, - hour: targetDate.hour, - minute: targetDate.minute, - month: targetDate.toFormat('LLLL'), - year: targetDate.year, - daysInMonth: targetDate.daysInMonth, - previousMonth: previousMonthDate.toFormat('LLLL'), - previousYear: previousMonthDate.year, - }; -}; - /** * This function calculates the start of the current month and the current date and time, * adjusted by subtracting 5 hours and 30 minutes, and returns them in the ISO 8601 format (UTC). @@ -176,33 +133,7 @@ const getLastMonthRange = (): DateTimeWithPreset => { }; }; -const convertToGmt = (dateStr: string): string => { - return DateTime.fromISO(dateStr.replace(' ', 'T')).toFormat( - 'yyyy-MM-dd HH:mm' - ); -}; -const formatToUtcDateTime = (dateStr: string): string => { - return DateTime.fromISO(dateStr) - .toUTC() // 🌍 keep it in UTC - .toFormat('yyyy-MM-dd HH:mm'); -}; - -/* - * TODO Fix or migrate the tests in `timerange-verification.spec.ts`. - * - * The tests in this spec frequently fail during specific dates and time periods - * throughout the day and year. Because there are so many tests in this spec, the - * timeouts and subsequent failures can delay test runs by several (45+) minutes - * which frequently interferes with unrelated test runs. - * - * Other considerations: - * - * - Would unit tests or component tests be a better fit for this? - * - * - Are these tests adding any value? They fail frequently and the failures do - * not get reviewed. They do not seem to be protecting us from regressions. - */ -describe.skip('Integration tests for verifying Cloudpulse custom and preset configurations', () => { +describe('Integration tests for verifying Cloudpulse custom and preset configurations', () => { /* * - Mocks user preferences for dashboard details (dashboard, engine, resources, and region). * - Simulates loading test data without real API calls. @@ -246,6 +177,10 @@ describe.skip('Integration tests for verifying Cloudpulse custom and preset conf mockGetDatabases([databaseMock]).as('fetchDatabases'); cy.visitWithLogin('/metrics'); + + cy.get('[aria-label="Content is loading"]', { timeout: 1000 }).should( + 'not.exist' + ); cy.wait([ '@fetchServices', '@fetchDashboard', @@ -253,55 +188,58 @@ describe.skip('Integration tests for verifying Cloudpulse custom and preset conf '@fetchDatabases', ]); }); - it('should implement and validate custom date/time picker for a specific date and time range', () => { - // --- Generate start and end date/time in GMT --- - const { - actualDate: startActualDate, - day: startDay, - hour: startHour, - minute: startMinute, - month: startMonth, - year: startYear, - previousMonth, - previousYear, - daysInMonth, - } = getDateRangeInGMT(12, 15, true); - - const { - actualDate: endActualDate, - day: endDay, - hour: endHour, - minute: endMinute, - } = getDateRangeInGMT(12, 30); - - cy.wait(1000); - // --- Select start date --- - ui.button.findByTitle('Last hour').as('startDateInput'); + // --- Mock the start date to ensure consistent test results --- - cy.get('@startDateInput').scrollIntoView(); + const MOCK_START_DATE = new Date('2025-08-01'); - cy.get('@startDateInput').click(); + cy.clock(MOCK_START_DATE.getTime(), ['Date']); + + // --- Define date/time values for the test --- + + const startDayOfMonth = 1; + const endDayOfMonth = 3; + const startHour = 1; + const startMinute = 15; + const endHour = 2; + const endMinute = 45; + + const ACTUAL_START_DATE_TIME = '2025-08-01 01:15'; + const ACTUAL_END_DATE_TIME = '2025-08-03 02:45'; + + // --- Reset the date/time picker to a known state --- + ui.button.findByTitle('Last hour').click(); + + ui.button.findByTitle('Reset').should('be.visible').click(); + + cy.get('[data-qa-preset="Reset"]').should( + 'have.attr', + 'aria-selected', + 'true' + ); + + // --- Open the date picker dialog and select start/end days --- cy.get('[role="dialog"]').within(() => { - cy.findAllByText(startDay).first().click(); - cy.findAllByText(endDay).first().click(); + // --- Select start and end day --- + cy.findAllByText(startDayOfMonth).first().click(); + cy.findAllByText(endDayOfMonth).first().click(); }); + // --- Select start time (hours and minutes) in the time picker --- ui.button .findByAttribute('aria-label^', 'Choose time') .first() - .should('be.visible', { timeout: 10000 }) // waits up to 10 seconds + .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); + cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); // Selects the start hour, minute, and meridiem (AM/PM) in the time picker. - cy.get(`[aria-label="${startHour} hours"]`).click(); - cy.wait(1000); ui.button .findByAttribute('aria-label^', 'Choose time') .first() @@ -310,9 +248,9 @@ describe.skip('Integration tests for verifying Cloudpulse custom and preset conf cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); - cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).first().click(); - cy.get(`[aria-label="${startMinute} minutes"]`).click(); + cy.get(`[aria-label="${startMinute} minutes"]`).first().click(); ui.button .findByAttribute('aria-label^', 'Choose time') @@ -327,44 +265,48 @@ describe.skip('Integration tests for verifying Cloudpulse custom and preset conf cy.findByLabelText('Select meridiem') .as('startMeridiemSelect') .scrollIntoView(); - cy.get('@startMeridiemSelect').find('[aria-label="PM"]').click(); + cy.get('@startMeridiemSelect').find('[aria-label="AM"]').click(); - // --- Select end time --- + // --- Select end time (hours and minutes) in the time picker --- ui.button .findByAttribute('aria-label^', 'Choose time') .last() .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); - cy.get('@timePickerButton', { timeout: 15000 }).click(); + cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); + + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); // Selects the start hour, minute, and meridiem (AM/PM) in the time picker. - cy.findByLabelText('Select hours').scrollIntoView({ - duration: 500, - easing: 'linear', - }); - cy.get(`[aria-label="${endHour} hours"]`).click(); + cy.get(`[aria-label="${endHour} hours"]`).last().click(); - cy.get('[aria-label^="Choose time"]') + ui.button + .findByAttribute('aria-label^', 'Choose time') .last() - .should('be.visible') + .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); - cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); + cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); + + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).last().click(); - cy.get(`[aria-label="${endMinute} minutes"]`).click(); + cy.get(`[aria-label="${endMinute} minutes"]`).last().click(); - cy.get('[aria-label^="Choose time"]') + ui.button + .findByAttribute('aria-label^', 'Choose time') .last() .should('be.visible', { timeout: 10000 }) .as('timePickerButton'); + cy.get('@timePickerButton').scrollIntoView({ easing: 'linear' }); + cy.get('@timePickerButton', { timeout: 15000 }).wait(300).click(); cy.findByLabelText('Select meridiem') .as('endMeridiemSelect') .scrollIntoView(); - cy.get('@endMeridiemSelect').find('[aria-label="PM"]').click(); + cy.get('@endMeridiemSelect').find('[aria-label="AM"]').click(); // --- Set timezone --- cy.findByPlaceholderText('Choose a Timezone').as('timezoneInput').clear(); @@ -376,119 +318,32 @@ describe.skip('Integration tests for verifying Cloudpulse custom and preset conf .and('be.enabled') .click(); - // --- Re-validate after apply --- + // --- Validate that the UI shows the expected start/end date/time --- cy.get('[aria-labelledby="start-date"]').should( 'have.value', - `${startActualDate} PM` + `${ACTUAL_START_DATE_TIME} AM` ); + cy.get('[aria-labelledby="end-date"]').should( 'have.value', - `${endActualDate} PM` + `${ACTUAL_END_DATE_TIME} AM` ); - ui.button.findByTitle('Cancel').and('be.enabled').click(); - - // --- Select Node Type --- ui.autocomplete.findByLabel('Node Type').type('Primary{enter}'); - // --- Validate API requests --- - cy.wait(Array(4).fill('@getMetrics')); + // --- Validate API requests triggered for the selected range --- + cy.wait(['@getMetrics', '@getMetrics', '@getMetrics', '@getMetrics']); cy.get('@getMetrics.all') .should('have.length', 4) .each((xhr: unknown) => { const { request: { body }, } = xhr as Interception; - expect(formatToUtcDateTime(body.absolute_time_duration.start)).to.equal( - convertToGmt(startActualDate) - ); - expect(formatToUtcDateTime(body.absolute_time_duration.end)).to.equal( - convertToGmt(endActualDate) - ); - }); - - // --- Test Time Range Presets --- - mockCreateCloudPulseMetrics(serviceType, metricsAPIResponsePayload).as( - 'getPresets' - ); - - // Open the date range picker to apply the "Last 30 Days" preset - - cy.get('[aria-labelledby="start-date"]').parent().as('startDateInput'); - cy.get('@startDateInput').click(); - - ui.button.findByTitle('Last 30 days').should('be.visible').click(); - - cy.get('[data-qa-preset="Last 30 days"]').should( - 'have.attr', - 'aria-selected', - 'true' - ); - - cy.contains(`${previousMonth} ${previousYear}`) - .closest('div') - .next() - .find('[aria-selected="true"]') - .then(($els) => { - const selectedDays = Array.from($els).map((el) => - Number(el.textContent?.trim()) - ); - - expect(daysInMonth, 'daysInMonth should be defined').to.be.a('number'); - - const totalDays = daysInMonth as number; - const expectedCount = totalDays - endDay; - - expect( - selectedDays.length, - 'number of selected days from the previous month for the last-30-days range' - ).to.eq(expectedCount); - expect( - totalDays - selectedDays.length, - 'start day of Last 30 days' - ).to.eq(endDay); - }); - - cy.contains(`${startMonth} ${startYear}`) - .closest('div') - .next() - .find('[aria-selected="true"]') - .then(($els) => { - const selectedDays = Array.from($els).map((el) => - Number(el.textContent?.trim()) - ); - - expect( - selectedDays.length, - 'number of selected days in the current month for the last-30-days range' - ).to.eq(endDay); - expect(Math.max(...selectedDays), 'end day of Last 30 days').to.eq( - endDay - ); - }); - cy.get('[data-qa-buttons="apply"]') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.button - .findByTitle('Last 30 days') - .should('be.visible') - .should('be.enabled'); - - cy.get('@getPresets.all') - .should('have.length', 4) - .each((xhr: unknown) => { - const { - request: { body }, - } = xhr as Interception; - expect(body).to.have.nested.property( - 'relative_time_duration.unit', - 'days' + expect(body.absolute_time_duration.start).to.equal( + '2025-08-01T01:15:00Z' ); - expect(body).to.have.nested.property( - 'relative_time_duration.value', - 30 + expect(body.absolute_time_duration.end).to.equal( + '2025-08-03T02:45:00Z' ); }); }); 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 ab17f15ce77..c1848efea09 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 @@ -61,9 +61,7 @@ function deleteDestinationViaActionMenu( ui.actionMenuItem.findByTitle('Delete').click(); // Find confirmation modal - cy.findByText( - `Are you sure you want to delete "${destination.label}" destination?` - ); + cy.findByText(`Are you sure you want to delete "${destination.label}"?`); ui.button.findByTitle('Delete').click(); cy.wait('@deleteDestination'); 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 5c9f4ba619e..9ae689d23b5 100644 --- a/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/edit-destination.spec.ts @@ -19,6 +19,8 @@ import { getDestinationTypeOption } from 'src/features/Delivery/deliveryUtils'; import type { AkamaiObjectStorageDetailsExtended } from '@linode/api-v4'; describe('Edit Destination', () => { + const saveChangesButtonText = 'Save Changes'; + beforeEach(() => { mockAppendFeatureFlags({ aclpLogs: { @@ -48,7 +50,9 @@ describe('Edit Destination', () => { ); // Save button should be disabled before test connection - cy.findByRole('button', { name: 'Save' }).should('be.disabled'); + cy.findByRole('button', { name: saveChangesButtonText }).should( + 'be.disabled' + ); // Test connection of the destination form mockTestConnection(400); ui.button @@ -62,7 +66,9 @@ describe('Edit Destination', () => { ); // Save button should be disabled after test connection failed - cy.findByRole('button', { name: 'Save' }).should('be.disabled'); + cy.findByRole('button', { name: saveChangesButtonText }).should( + 'be.disabled' + ); }); it('edit destination with correct data', () => { @@ -75,7 +81,9 @@ describe('Edit Destination', () => { ); // Save button should be disabled before test connection - cy.findByRole('button', { name: 'Save' }).should('be.disabled'); + cy.findByRole('button', { name: saveChangesButtonText }).should( + 'be.disabled' + ); // Test connection of the destination form mockTestConnection(); ui.button @@ -92,7 +100,7 @@ describe('Edit Destination', () => { mockUpdateDestination(mockDestinationPayloadWithId, updatedDestination); mockGetDestinations([updatedDestination]); // Submit the destination edit form - cy.findByRole('button', { name: 'Save' }) + cy.findByRole('button', { name: saveChangesButtonText }) .should('be.enabled') .should('have.attr', 'type', 'button') .click(); 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 ee47317290e..95942ae99b4 100644 --- a/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/edit-stream.spec.ts @@ -22,6 +22,8 @@ import { randomLabel } from 'support/util/random'; import { kubernetesClusterFactory } from 'src/factories'; describe('Edit Stream', () => { + const saveChangesButtonText = 'Save Changes'; + beforeEach(() => { mockAppendFeatureFlags({ aclpLogs: { @@ -48,14 +50,14 @@ describe('Edit Stream', () => { const updatedLabel = randomLabel(); // Change the Name - cy.findByLabelText('Name') + cy.findByLabelText('Stream Name') .should('be.visible') .should('be.enabled') .should('have.value', mockAuditLogsStream.label); logsStreamForm.setLabel(updatedLabel); - cy.findByLabelText('Name') + cy.findByLabelText('Stream Name') .should('be.visible') .should('be.enabled') .should('have.value', updatedLabel); @@ -67,7 +69,7 @@ describe('Edit Stream', () => { .should('have.attr', 'value', 'Audit Logs'); // Save button should be enabled initially - ui.button.findByTitle('Save').should('be.enabled'); + ui.button.findByTitle(saveChangesButtonText).should('be.enabled'); // Test Connection should be disabled for existing destination ui.button.findByTitle('Test Connection').should('be.disabled'); @@ -82,7 +84,7 @@ describe('Edit Stream', () => { ui.button.findByTitle('Test Connection').should('be.enabled'); // Save button should be disabled after changing destination - ui.button.findByTitle('Save').should('be.disabled'); + ui.button.findByTitle(saveChangesButtonText).should('be.disabled'); // Test connection with failure mockTestConnection(400); @@ -93,7 +95,7 @@ describe('Edit Stream', () => { ); // Save button should remain disabled after failed test - ui.button.findByTitle('Save').should('be.disabled'); + ui.button.findByTitle(saveChangesButtonText).should('be.disabled'); // Test connection with success mockTestConnection(200); @@ -104,11 +106,11 @@ describe('Edit Stream', () => { ); // Save button should now be enabled - ui.button.findByTitle('Save').should('be.enabled'); + ui.button.findByTitle(saveChangesButtonText).should('be.enabled'); // Submit the stream edit form - failure in creating destination mockCreateDestination({}, 400); - ui.button.findByTitle('Save').should('be.enabled').click(); + ui.button.findByTitle(saveChangesButtonText).should('be.enabled').click(); ui.toast.assertMessage(`There was an issue creating your destination`); @@ -123,7 +125,7 @@ describe('Edit Stream', () => { mockAuditLogsStream ).as('updateStream'); - ui.button.findByTitle('Save').click(); + ui.button.findByTitle(saveChangesButtonText).click(); cy.wait('@updateStream') .its('request.body') .then((body) => { @@ -176,14 +178,14 @@ describe('Edit Stream', () => { const updatedLabel = randomLabel(); // Change the Name - cy.findByLabelText('Name') + cy.findByLabelText('Stream Name') .should('be.visible') .should('be.enabled') .should('have.value', mockLKEAuditLogsStream.label); logsStreamForm.setLabel(updatedLabel); - cy.findByLabelText('Name') + cy.findByLabelText('Stream Name') .should('be.visible') .should('be.enabled') .should('have.value', updatedLabel); @@ -229,7 +231,7 @@ describe('Edit Stream', () => { logsStreamForm.findClusterCheckbox('all').check(); // Save button should be enabled - ui.button.findByTitle('Save').should('be.enabled'); + ui.button.findByTitle(saveChangesButtonText).should('be.enabled'); // Submit the stream edit form - failure mockUpdateStream( @@ -246,7 +248,7 @@ describe('Edit Stream', () => { 400 ).as('updateStreamFail'); - ui.button.findByTitle('Save').click(); + ui.button.findByTitle(saveChangesButtonText).click(); cy.wait('@updateStreamFail'); ui.toast.assertMessage('There was an issue editing your stream'); @@ -264,7 +266,7 @@ describe('Edit Stream', () => { mockLKEAuditLogsStream ).as('updateStream'); - ui.button.findByTitle('Save').click(); + ui.button.findByTitle(saveChangesButtonText).click(); cy.wait('@updateStream') .its('request.body') .then((body) => { 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 ad9df1e518b..1ba21b19d60 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 @@ -64,9 +64,7 @@ function deleteStreamViaActionMenu(tableAlias: string, stream: Stream) { ui.actionMenuItem.findByTitle('Delete').click(); // Find confirmation modal - cy.findByText( - `Are you sure you want to delete "${stream.label}" stream?` - ); + cy.findByText(`Are you sure you want to delete "${stream.label}"?`); ui.button.findByTitle('Delete').click(); cy.wait('@deleteStream'); @@ -115,7 +113,7 @@ function deactivateStreamViaActionMenu(tableAlias: string, stream: Stream) { // Deactivate stream ui.actionMenuItem.findByTitle('Deactivate').click(); - ui.toast.assertMessage(`Stream ${stream.label} deactivated`); + ui.toast.assertMessage(`${stream.label} deactivated`); }); } diff --git a/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts b/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts index 183dc7dd047..821ea4a8103 100644 --- a/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts +++ b/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts @@ -1,4 +1,3 @@ -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { pages } from 'support/ui/constants'; import type { Page } from 'support/ui/constants'; @@ -9,10 +8,6 @@ beforeEach(() => { describe('smoke - deep links', () => { beforeEach(() => { cy.visitWithLogin('/null'); - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. - mockAppendFeatureFlags({ - iamRbacPrimaryNavChanges: true, - }).as('getFeatureFlags'); }); it('Go to each route and validate deep links', () => { diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts index 07bd5f6edd6..4623ef33e52 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts @@ -1405,6 +1405,13 @@ describe('LKE cluster updates', () => { .findByTitle(`Add a Node Pool: ${mockCluster.label}`) .should('be.visible') .within(() => { + // For the "Dedicated 4 GB", use filter to select G6 Dedicated instead of relying on pagination + ui.autocomplete.findByLabel('Dedicated Plans').click(); + + ui.autocompletePopper.find().within(() => { + cy.findByText('G6 Dedicated').should('be.visible').click(); + }); + cy.findByText('Dedicated 4 GB') .should('be.visible') .closest('tr') diff --git a/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts b/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts index dcb02fd5f66..43325319275 100644 --- a/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/alerts-create.spec.ts @@ -123,6 +123,12 @@ describe('Create flow when beta alerts enabled by region and feature flag', func cy.get('[data-qa-tp="Linode Plan"]') .should('be.visible') .within(() => { + // For the Dedicated 8 GB, Use filter to select G6 Dedicated instead of relying on pagination + ui.autocomplete.findByLabel('Dedicated Plans').click(); + ui.autocompletePopper.find().within(() => { + cy.findByText('G6 Dedicated').should('be.visible').click(); + }); + cy.get('[data-qa-plan-row="Dedicated 8 GB"]').click(); }); cy.get('[type="password"]').should('be.visible').scrollIntoView(); @@ -283,6 +289,12 @@ describe('Create flow when beta alerts enabled by region and feature flag', func cy.get('[data-qa-tp="Linode Plan"]') .should('be.visible') .within(() => { + // For the Dedicated 8 GB, Use filter to select G6 Dedicated instead of relying on pagination + ui.autocomplete.findByLabel('Dedicated Plans').click(); + ui.autocompletePopper.find().within(() => { + cy.findByText('G6 Dedicated').should('be.visible').click(); + }); + cy.get('[data-qa-plan-row="Dedicated 8 GB"]').click(); }); cy.get('[type="password"]').should('be.visible').scrollIntoView(); @@ -432,6 +444,12 @@ describe('Create flow when beta alerts enabled by region and feature flag', func cy.get('[data-qa-tp="Linode Plan"]') .should('be.visible') .within(() => { + // For the Dedicated 8 GB, Use filter to select G6 Dedicated instead of relying on pagination + ui.autocomplete.findByLabel('Dedicated Plans').click(); + ui.autocompletePopper.find().within(() => { + cy.findByText('G6 Dedicated').should('be.visible').click(); + }); + cy.get('[data-qa-plan-row="Dedicated 8 GB"]').click(); }); cy.get('[type="password"]').should('be.visible').scrollIntoView(); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts index bfae1112c27..b87f8dea920 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts @@ -421,6 +421,9 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to legacy Config Interfaces + linodeCreatePage.selectLegacyConfigInterfacesType(); + // Confirm that mocked Firewall is shown in the Autocomplete, and then select it. cy.findByLabelText('Firewall').should('be.visible'); cy.get('[data-qa-autocomplete="Firewall"]').within(() => { @@ -491,9 +494,6 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to Linode Interfaces - linodeCreatePage.selectLinodeInterfacesType(); - // Confirm that mocked Firewall is shown in the Autocomplete, and then select it. cy.findByLabelText('Public Interface Firewall').should('be.visible'); cy.get('[data-qa-autocomplete="Public Interface Firewall"]').within(() => { @@ -566,6 +566,9 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to legacy Config Interfaces + linodeCreatePage.selectLegacyConfigInterfacesType(); + cy.findByText('Create Firewall').should('be.visible').click(); ui.drawer @@ -661,9 +664,6 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to Linode Interfaces - linodeCreatePage.selectLinodeInterfacesType(); - cy.findByText('Create Firewall').should('be.visible').click(); ui.drawer @@ -766,6 +766,9 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to legacy Config Interfaces + linodeCreatePage.selectLegacyConfigInterfacesType(); + // Creating the linode without a firewall should display a warning. ui.button .findByTitle('Create Linode') @@ -869,9 +872,6 @@ describe('Create Linode with Firewall (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to Linode Interfaces - linodeCreatePage.selectLinodeInterfacesType(); - // Creating the linode without a firewall should display a warning. ui.button .findByTitle('Create Linode') diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts index 1b4a1c62f3e..1ed9356b23b 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts @@ -319,6 +319,9 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to legacy Config Interfaces + linodeCreatePage.selectLegacyConfigInterfacesType(); + // select existing VLAN. linodeCreatePage.selectInterface('vlan'); // Confirm that mocked VLAN is shown in the Autocomplete, and then select it. @@ -400,9 +403,6 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to Linode Interfaces - linodeCreatePage.selectLinodeInterfacesType(); - // Select VLAN card linodeCreatePage.selectInterface('vlan'); @@ -485,6 +485,9 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to legacy Config Interfaces + linodeCreatePage.selectLegacyConfigInterfacesType(); + // Select VLAN card linodeCreatePage.selectInterface('vlan'); @@ -567,9 +570,6 @@ describe('Create Linode with VLANs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to Linode Interfaces - linodeCreatePage.selectLinodeInterfacesType(); - // Select VLAN card linodeCreatePage.selectInterface('vlan'); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts index ea1688c42d8..0f9fa5ab253 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts @@ -476,6 +476,9 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to legacy Config Interfaces + linodeCreatePage.selectLegacyConfigInterfacesType(); + // Select VPC linodeCreatePage.selectInterface('vpc'); @@ -612,9 +615,6 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to Linode Interfaces - linodeCreatePage.selectLinodeInterfacesType(); - // Select VPC option linodeCreatePage.selectInterface('vpc'); @@ -750,6 +750,9 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); + // Switch to legacy Config Interfaces + linodeCreatePage.selectLegacyConfigInterfacesType(); + // Select VPC card linodeCreatePage.selectInterface('vpc'); @@ -933,9 +936,6 @@ describe('Create Linode with VPCs (Linode Interfaces)', () => { // Confirm the Linode Interfaces section is shown. assertNewLinodeInterfacesIsAvailable(); - // Switch to Linode Interfaces - linodeCreatePage.selectLinodeInterfacesType(); - // Select VPC card linodeCreatePage.selectInterface('vpc'); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts index ef7213c5fae..eb6eec87f92 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -93,6 +93,16 @@ describe('Create Linode', () => { linodeCreatePage.setLabel(linodeLabel); linodeCreatePage.selectImage('Debian 12'); linodeCreatePage.selectRegionById(linodeRegion.id); + + // For the "Dedicated 4 GB" plan under the "Dedicated CPU" plan type, use filter to select G6 Dedicated instead of relying on pagination + if (planConfig.planType === 'Dedicated CPU') { + ui.autocomplete.findByLabel('Dedicated Plans').click(); + + ui.autocompletePopper.find().within(() => { + cy.findByText('G6 Dedicated').should('be.visible').click(); + }); + } + linodeCreatePage.selectPlan( planConfig.planType, planConfig.planLabel diff --git a/packages/manager/cypress/e2e/core/linodes/maintenance-policy-region-support.spec.ts b/packages/manager/cypress/e2e/core/linodes/maintenance-policy-region-support.spec.ts index 8126eac56bf..3481b60fa99 100644 --- a/packages/manager/cypress/e2e/core/linodes/maintenance-policy-region-support.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/maintenance-policy-region-support.spec.ts @@ -123,12 +123,6 @@ describe('maintenance policy region support - Linode Details > Settings', () => cleanUp(['linodes', 'lke-clusters']); }); - beforeEach(() => { - mockAppendFeatureFlags({ - iamRbacPrimaryNavChanges: false, - }).as('getFeatureFlags'); - }); - it('disables maintenance policy selector when region does not support it', () => { // Mock a linode in a region that doesn't support maintenance policies const mockRegion = regionFactory.build({ diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts index 3cacea66f7f..efaa1174406 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts @@ -69,9 +69,7 @@ const preferenceOverrides = { authenticate(); describe('linode landing checks', () => { beforeEach(() => { - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. mockAppendFeatureFlags({ - iamRbacPrimaryNavChanges: true, iam: { enabled: false, }, @@ -478,10 +476,6 @@ describe('linode landing checks', () => { describe('linode landing checks for empty state', () => { beforeEach(() => { - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. - mockAppendFeatureFlags({ - iamRbacPrimaryNavChanges: true, - }); // Mock setup to display the Linode landing page in an empty state mockGetLinodes([]).as('getLinodes'); }); @@ -584,10 +578,6 @@ describe('linode landing checks for empty state', () => { describe('linode landing checks for non-empty state with restricted user', () => { beforeEach(() => { - // TODO M3-10491 - Remove `iamRbacPrimaryNavChanges` feature flag mock once flag is deleted. - mockAppendFeatureFlags({ - iamRbacPrimaryNavChanges: true, - }); // Mock setup to display the Linode landing page in an non-empty state const mockLinodes: Linode[] = new Array(1) .fill(null) diff --git a/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts b/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts index bd5f13f4f28..8c1e15ab9bd 100644 --- a/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts +++ b/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts @@ -221,6 +221,11 @@ describe('OneClick Apps (OCA)', () => { cy.focused().type(`${region.id}{enter}`); // Choose a Linode plan + // For the Dedicated 8 GB, Use filter to select G6 Dedicated instead of relying on pagination + ui.autocomplete.findByLabel('Dedicated Plans').click(); + ui.autocompletePopper.find().within(() => { + cy.findByText('G6 Dedicated').should('be.visible').click(); + }); cy.get('[data-qa-plan-row="Dedicated 8 GB"]') .closest('tr') .within(() => { diff --git a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts index 01a7dbe891e..2d793df73f5 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts @@ -80,9 +80,15 @@ const fillOutStackscriptForm = ( cy.findByLabelText('Description').should('be.visible').click(); cy.focused().type(description); } - - ui.autocomplete.findByLabel('Target Images').should('be.visible').click(); - ui.autocompletePopper.findByTitle(targetImage).should('be.visible').click(); + ui.autocomplete + .findByLabel('Target Images') + .should('be.visible') + .type(targetImage); + // need selector in case item label is same as category label + ui.autocompletePopper + .findByTitle(targetImage, { selector: 'li div p' }) + .should('be.visible') + .click(); ui.autocomplete.findByLabel('Target Images').click(); // Close autocomplete popper // Insert a script. @@ -114,6 +120,13 @@ const fillOutLinodeForm = (label: string, regionName: string) => { cy.focused().type(label); cy.findByText('Dedicated CPU').should('be.visible').click(); + + // Use filter to select G6 Dedicated instead of relying on pagination + ui.autocomplete.findByLabel('Dedicated Plans').click(); + ui.autocompletePopper.find().within(() => { + cy.findByText('G6 Dedicated').should('be.visible').click(); + }); + cy.get('[id="g6-dedicated-2"]').click(); cy.findByLabelText('Root Password').should('be.visible').type(password); }; @@ -187,7 +200,9 @@ describe('Create stackscripts', () => { const stackscriptLabel = randomLabel(); const stackscriptDesc = randomPhrase(); // use random image. can specify image w/ getImageByLabel, then set images option in chooseImage - const randomImage = chooseImage(); + const randomImage = chooseImage({ + capabilities: ['cloud-init', 'distributed-sites'], + }); const stackscriptImage = randomImage.label; const linodeLabel = randomLabel(); const linodeRegion = chooseRegion({ capabilities: ['Vlans'] }); diff --git a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts index 26b17237855..7e137192517 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts @@ -379,6 +379,13 @@ describe('Community Stackscripts integration tests', () => { // An error message shows up when no region is selected cy.contains('Plan is required.').should('be.visible'); + + // For the Dedicated 8 GB, Use filter to select G6 Dedicated instead of relying on pagination + ui.autocomplete.findByLabel('Dedicated Plans').click(); + ui.autocompletePopper.find().within(() => { + cy.findByText('G6 Dedicated').should('be.visible').click(); + }); + cy.get('[data-qa-plan-row="Dedicated 8 GB"]') .closest('tr') .within(() => { diff --git a/packages/manager/cypress/support/ui/pages/logs-destination-form.ts b/packages/manager/cypress/support/ui/pages/logs-destination-form.ts index 1a15feffa7a..01621e540b8 100644 --- a/packages/manager/cypress/support/ui/pages/logs-destination-form.ts +++ b/packages/manager/cypress/support/ui/pages/logs-destination-form.ts @@ -16,7 +16,6 @@ export const logsDestinationForm = { cy.findByLabelText('Destination Name') .should('be.visible') .should('be.enabled') - .should('have.attr', 'placeholder', 'Destination Name') .clear(); cy.focused().type(label); }, @@ -30,7 +29,7 @@ export const logsDestinationForm = { cy.findByLabelText('Host') .should('be.visible') .should('be.enabled') - .should('have.attr', 'placeholder', 'Host') + .should('have.attr', 'placeholder', 'Host for the destination') .clear(); cy.focused().type(host); }, diff --git a/packages/manager/cypress/support/ui/pages/logs-stream-form.ts b/packages/manager/cypress/support/ui/pages/logs-stream-form.ts index 7f2ac7f42dc..0e97a983285 100644 --- a/packages/manager/cypress/support/ui/pages/logs-stream-form.ts +++ b/packages/manager/cypress/support/ui/pages/logs-stream-form.ts @@ -21,10 +21,9 @@ export const logsStreamForm = { * @param label - stream label to set */ setLabel: (label: string) => { - cy.findByLabelText('Name') + cy.findByLabelText('Stream Name') .should('be.visible') .should('be.enabled') - .should('have.attr', 'placeholder', 'Stream name') .clear(); cy.focused().type(label); }, @@ -57,7 +56,11 @@ export const logsStreamForm = { cy.findByLabelText('Destination Name') .should('be.visible') .should('be.enabled') - .should('have.attr', 'placeholder', 'Create or Select Destination Name') + .should( + 'have.attr', + 'placeholder', + 'Select existing or enter new destination' + ) .clear(); // Select the Destination Name ui.autocompletePopper @@ -93,7 +96,11 @@ export const logsStreamForm = { cy.findByLabelText('Destination Name') .should('be.visible') .should('be.enabled') - .should('have.attr', 'placeholder', 'Create or Select Destination Name') + .should( + 'have.attr', + 'placeholder', + 'Select existing or enter new destination' + ) .clear(); cy.focused().type(label); cy.findByText(new RegExp(`"${label}"`)).click(); diff --git a/packages/manager/cypress/support/util/regions.ts b/packages/manager/cypress/support/util/regions.ts index 6c399851cbe..8fb42e0da7f 100644 --- a/packages/manager/cypress/support/util/regions.ts +++ b/packages/manager/cypress/support/util/regions.ts @@ -326,8 +326,12 @@ const resolveSearchRegions = ( ]; // If the user has specified an override region for this run, it takes precedent - // over any other specified criteria. - if (overrideRegion && detectOverrideRegion) { + // over any other specified criteria unless mock regions are passed in `options`. + if ( + overrideRegion && + detectOverrideRegion && + (!options?.regions || options.regions.length === 0) + ) { // TODO Consider skipping instead of failing when test isn't applicable to override region. if (!regionHasCapabilities(overrideRegion, requiredCapabilities)) { throw new Error( diff --git a/packages/manager/package.json b/packages/manager/package.json index 3c1bd6e0a91..20510a085fa 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -2,7 +2,7 @@ "name": "linode-manager", "author": "Linode", "description": "The Linode Manager website", - "version": "1.157.0", + "version": "1.158.0", "private": true, "type": "module", "bugs": { @@ -24,7 +24,7 @@ "@fontsource/nunito-sans": "^5.1.1", "@hookform/resolvers": "3.9.1", "@linode/api-v4": "workspace:*", - "@linode/design-language-system": "^5.0.0", + "@linode/design-language-system": "^5.3.2", "@linode/queries": "workspace:*", "@linode/search": "workspace:*", "@linode/shared": "workspace:*", @@ -59,7 +59,7 @@ "he": "^1.2.0", "immer": "^9.0.6", "ipaddr.js": "^1.9.1", - "jspdf": "^3.0.2", + "jspdf": "^4.0.0", "jspdf-autotable": "^5.0.2", "launchdarkly-react-client-sdk": "3.0.10", "libphonenumber-js": "^1.10.6", @@ -140,7 +140,6 @@ "@types/eslint-plugin-jsx-a11y": "^6.10.0", "@types/he": "^1.1.0", "@types/history": "4", - "@types/jspdf": "^1.3.3", "@types/luxon": "3.4.2", "@types/markdown-it": "^14.1.2", "@types/md5": "^2.1.32", diff --git a/packages/manager/public/assets/chroma.svg b/packages/manager/public/assets/chroma.svg new file mode 100644 index 00000000000..f6f7ea3da05 --- /dev/null +++ b/packages/manager/public/assets/chroma.svg @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/manager/public/assets/mistral.svg b/packages/manager/public/assets/mistral.svg new file mode 100644 index 00000000000..ebbdfd9d0a5 --- /dev/null +++ b/packages/manager/public/assets/mistral.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/openwebui.svg b/packages/manager/public/assets/openwebui.svg new file mode 100644 index 00000000000..c28acbd8c8a --- /dev/null +++ b/packages/manager/public/assets/openwebui.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/manager/public/assets/vllm.svg b/packages/manager/public/assets/vllm.svg new file mode 100644 index 00000000000..6b9e5d1df89 --- /dev/null +++ b/packages/manager/public/assets/vllm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/manager/public/assets/white/chroma.svg b/packages/manager/public/assets/white/chroma.svg new file mode 100644 index 00000000000..e9276cb4b0c --- /dev/null +++ b/packages/manager/public/assets/white/chroma.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/manager/public/assets/white/mistral.svg b/packages/manager/public/assets/white/mistral.svg new file mode 100644 index 00000000000..998f945024e --- /dev/null +++ b/packages/manager/public/assets/white/mistral.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/manager/public/assets/white/openwebui.svg b/packages/manager/public/assets/white/openwebui.svg new file mode 100644 index 00000000000..eff91d30859 --- /dev/null +++ b/packages/manager/public/assets/white/openwebui.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/manager/public/assets/white/vllm.svg b/packages/manager/public/assets/white/vllm.svg new file mode 100644 index 00000000000..b85e94fd71d --- /dev/null +++ b/packages/manager/public/assets/white/vllm.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/manager/src/GoTo.tsx b/packages/manager/src/GoTo.tsx index 93a0bddf0b7..791cb6d675c 100644 --- a/packages/manager/src/GoTo.tsx +++ b/packages/manager/src/GoTo.tsx @@ -4,10 +4,9 @@ import { useNavigate } from '@tanstack/react-router'; import * as React from 'react'; import { useIsDatabasesEnabled } from './features/Databases/utilities'; -import { usePermissions } from './features/IAM/hooks/usePermissions'; import { useIsMarketplaceV2Enabled } from './features/Marketplace/utils'; +import { useIsNetworkLoadBalancerEnabled } from './features/NetworkLoadBalancers/utils'; import { useIsPlacementGroupsEnabled } from './features/PlacementGroups/utils'; -import { useFlags } from './hooks/useFlags'; import { useGlobalKeyboardListener } from './hooks/useGlobalKeyboardListener'; import type { SelectOption } from '@linode/ui'; @@ -17,15 +16,12 @@ export const GoTo = React.memo(() => { const { data: accountSettings } = useAccountSettings(); - const { iamRbacPrimaryNavChanges } = useFlags(); - const isManagedAccount = accountSettings?.managed ?? false; - const { data: permissions } = usePermissions('account', ['is_account_admin']); - const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled(); const { isDatabasesEnabled } = useIsDatabasesEnabled(); const { isMarketplaceV2FeatureEnabled } = useIsMarketplaceV2Enabled(); + const { isNetworkLoadBalancerEnabled } = useIsNetworkLoadBalancerEnabled(); const { goToOpen, setGoToOpen } = useGlobalKeyboardListener(); @@ -58,6 +54,11 @@ export const GoTo = React.memo(() => { display: 'VPC', href: '/vpcs', }, + { + display: 'Network Load Balancer', + hide: !isNetworkLoadBalancerEnabled, + href: '/netloadbalancers', + }, { display: 'NodeBalancers', href: '/nodebalancers', @@ -108,22 +109,12 @@ export const GoTo = React.memo(() => { : 'Quick Deploy Apps', href: '/linodes/create/marketplace', }, - ...(iamRbacPrimaryNavChanges - ? [ - { display: 'Billing', href: '/billing' }, - { display: 'Identity & Access', href: '/iam' }, - { display: 'Login History', href: '/login-history' }, - { display: 'Service Transfers', href: '/service-transfers' }, - { display: 'Maintenance', href: '/maintenance' }, - { display: 'Settings', href: '/settings' }, - ] - : [ - { - display: 'Account', - hide: !permissions.is_account_admin, - href: '/account/billing', - }, - ]), + { display: 'Billing', href: '/billing' }, + { display: 'Identity & Access', href: '/iam' }, + { display: 'Login History', href: '/login-history' }, + { display: 'Service Transfers', href: '/service-transfers' }, + { display: 'Maintenance', href: '/maintenance' }, + { display: 'Settings', href: '/settings' }, { display: 'Help & Support', href: '/support', @@ -134,12 +125,11 @@ export const GoTo = React.memo(() => { }, ], [ - permissions.is_account_admin, isDatabasesEnabled, isManagedAccount, isMarketplaceV2FeatureEnabled, + isNetworkLoadBalancerEnabled, isPlacementGroupsEnabled, - iamRbacPrimaryNavChanges, ] ); diff --git a/packages/manager/src/components/MaskableText/MaskableTextArea.tsx b/packages/manager/src/components/MaskableText/MaskableTextArea.tsx index 5075bc68dc9..921e7181ef6 100644 --- a/packages/manager/src/components/MaskableText/MaskableTextArea.tsx +++ b/packages/manager/src/components/MaskableText/MaskableTextArea.tsx @@ -1,8 +1,6 @@ import { Typography } from '@linode/ui'; import React from 'react'; -import { useFlags } from 'src/hooks/useFlags'; - import { Link } from '../Link'; /** @@ -10,21 +8,11 @@ import { Link } from '../Link'; * Example: Billing Contact info, rather than masking many individual fields */ export const MaskableTextAreaCopy = () => { - const { iamRbacPrimaryNavChanges } = useFlags(); return ( This data is sensitive and hidden for privacy. To unmask all sensitive data by default, go to{' '} - - profile settings - - . + profile settings. ); }; diff --git a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx index 62810bba634..f4b47bbceff 100644 --- a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx +++ b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.test.tsx @@ -259,6 +259,13 @@ describe('Payment Method Row', () => { }); it('Opens "Make a Payment" drawer with the payment method preselected if "Make a Payment" action is clicked', async () => { + queryMocks.userPermissions.mockReturnValue({ + data: { + make_billing_payment: true, + set_default_payment_method: false, + delete_payment_method: false, + }, + }); const paymentMethods = [ paymentMethodFactory.build({ type: 'paypal', @@ -274,23 +281,22 @@ describe('Payment Method Row', () => { * The component is responsible for rendering the "Make a Payment" drawer, * and is required for this test. We may want to consider decoupling these components in the future. */ - const { getByLabelText, getByTestId, getByText, getAllByTestId } = - renderWithTheme( - - - - , - { - initialRoute: '/account/billing', - } - ); + const { getByLabelText, getByText, router } = renderWithTheme( + + + + , + { + initialRoute: '/billing', + } + ); const actionMenu = getByLabelText('Action menu for card ending in 1881'); await userEvent.click(actionMenu); @@ -300,21 +306,12 @@ describe('Payment Method Row', () => { await userEvent.click(makePaymentButton); await waitFor(() => { - expect(getByTestId('drawer')).toBeVisible(); + expect(router.state.location.pathname).toBe('/billing'); + }); + await waitFor(() => { + expect(router.state.location.searchStr).toBe( + '?action=make-payment&paymentMethodId=12' + ); }); - - expect(getByTestId('drawer-title')).toHaveTextContent('Make a Payment'); - - const expectedSelectionCard = getAllByTestId('selection-card')[1]; - - expect(expectedSelectionCard).toBeVisible(); - expect(expectedSelectionCard).toHaveTextContent('1881'); - expect(expectedSelectionCard).toHaveAttribute( - 'data-qa-selection-card-checked', - 'true' - ); - - // In the future, if we have access to the router's state, - // we can assert the search params. }); }); diff --git a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx index f01ad45292e..40134cfb8d4 100644 --- a/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx +++ b/packages/manager/src/components/PaymentMethodRow/PaymentMethodRow.tsx @@ -8,7 +8,6 @@ import * as React from 'react'; import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import CreditCard from 'src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/CreditCard'; import { usePermissions } from 'src/features/IAM/hooks/usePermissions'; -import { useFlags } from 'src/hooks/useFlags'; import { ThirdPartyPayment } from './ThirdPartyPayment'; @@ -40,7 +39,6 @@ export const PaymentMethodRow = (props: Props) => { const { is_default, type } = paymentMethod; const { enqueueSnackbar } = useSnackbar(); const navigate = useNavigate(); - const flags = useFlags(); const { mutateAsync: makePaymentMethodDefault } = useMakeDefaultPaymentMethodMutation(props.paymentMethod.id); @@ -65,7 +63,7 @@ export const PaymentMethodRow = (props: Props) => { disabled: isChildUser || !permissions.make_billing_payment, onClick: () => { navigate({ - to: flags?.iamRbacPrimaryNavChanges ? '/billing' : '/account/billing', + to: '/billing', search: (prev) => ({ ...prev, action: 'make-payment', diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx index 7c078ccc248..05870b0fede 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx @@ -22,7 +22,6 @@ const queryString = 'menu-item-Managed'; const queryMocks = vi.hoisted(() => ({ useIsIAMEnabled: vi.fn(() => ({ - isIAMBeta: false, isIAMEnabled: false, })), usePreferences: vi.fn().mockReturnValue({}), @@ -494,11 +493,9 @@ describe('PrimaryNav', () => { expect(queryByTestId('menu-item-Logs')).toBeNull(); }); - it('should show Administration links if iamRbacPrimaryNavChanges flag is enabled', async () => { + it('should show Administration links', async () => { const flags: Partial = { - iamRbacPrimaryNavChanges: true, iam: { - beta: true, enabled: true, }, limitsEvolution: { @@ -509,7 +506,6 @@ describe('PrimaryNav', () => { }; queryMocks.useIsIAMEnabled.mockReturnValue({ - isIAMBeta: true, isIAMEnabled: true, }); @@ -544,15 +540,12 @@ describe('PrimaryNav', () => { it('should hide Identity & Access link for non beta users', async () => { const flags: Partial = { - iamRbacPrimaryNavChanges: true, iam: { - beta: true, enabled: false, }, }; queryMocks.useIsIAMEnabled.mockReturnValue({ - isIAMBeta: true, isIAMEnabled: false, }); @@ -570,47 +563,6 @@ describe('PrimaryNav', () => { }); }); - it('should show Account link and hide Administration if iamRbacPrimaryNavChanges flag is disabled', async () => { - const flags: Partial = { - iamRbacPrimaryNavChanges: false, - iam: { - beta: true, - enabled: true, - }, - }; - - queryMocks.useIsIAMEnabled.mockReturnValue({ - isIAMBeta: true, - isIAMEnabled: true, - }); - - renderWithTheme(, { - flags, - }); - - const adminLink = screen.queryByRole('button', { name: 'Administration' }); - expect(adminLink).toBeNull(); - - await waitFor(() => { - expect(screen.queryByRole('link', { name: 'Billing' })).toBeNull(); - expect(screen.queryByRole('link', { name: 'Quotas' })).toBeNull(); - expect(screen.queryByRole('link', { name: 'Login History' })).toBeNull(); - expect( - screen.queryByRole('link', { name: 'Service Transfers' }) - ).toBeNull(); - expect(screen.queryByRole('link', { name: 'Maintenance' })).toBeNull(); - expect( - screen.queryByRole('link', { name: 'Account' }) - ).toBeInTheDocument(); - expect( - screen.queryByRole('link', { name: 'Identity & Access' }) - ).toBeInTheDocument(); - expect( - screen.queryByRole('link', { name: 'Account Settings' }) - ).toBeNull(); - }); - }); - it('should show Network Load Balancers menu item if the user has the account capability and the flag is enabled', async () => { const account = accountFactory.build({ capabilities: ['Network LoadBalancer'], diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index 1a4d0499baa..ebe54255732 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -114,12 +114,12 @@ export const PrimaryNav = (props: PrimaryNavProps) => { flags.aclpAlerting?.recentActivity || flags.aclpAlerting?.notificationChannels); - const { iamRbacPrimaryNavChanges, limitsEvolution } = flags; + const { limitsEvolution } = flags; const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled(); const { isDatabasesEnabled, isDatabasesV2Beta } = useIsDatabasesEnabled(); - const { isIAMBeta, isIAMEnabled } = useIsIAMEnabled(); + const { isIAMEnabled } = useIsIAMEnabled(); const showLimitedAvailabilityBadges = flags.iamLimitedAvailabilityBadges; const { isNetworkLoadBalancerEnabled } = useIsNetworkLoadBalancerEnabled(); @@ -282,36 +282,6 @@ export const PrimaryNav = (props: PrimaryNavProps) => { name: 'Monitor', }, { - icon: , - links: [ - { - display: 'Betas', - hide: !flags.selfServeBetas, - to: '/betas', - }, - { - display: 'Identity & Access', - hide: !isIAMEnabled || iamRbacPrimaryNavChanges, - to: '/iam', - isBeta: isIAMBeta, - isNew: !isIAMBeta && showLimitedAvailabilityBadges, - }, - { - display: 'Account', - hide: iamRbacPrimaryNavChanges, - to: '/account', - }, - { - display: 'Help & Support', - to: '/support', - }, - ], - name: 'More', - }, - ]; - - if (iamRbacPrimaryNavChanges) { - groups.splice(groups.length - 1, 0, { icon: , links: [ { @@ -327,8 +297,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => { display: 'Identity & Access', hide: !isIAMEnabled, to: '/iam', - isBeta: isIAMBeta, - isNew: !isIAMBeta && showLimitedAvailabilityBadges, + isNew: isIAMEnabled && showLimitedAvailabilityBadges, }, { display: 'Quotas', @@ -353,8 +322,23 @@ export const PrimaryNav = (props: PrimaryNavProps) => { }, ], name: 'Administration', - }); - } + }, + { + icon: , + links: [ + { + display: 'Betas', + hide: !flags.selfServeBetas, + to: '/betas', + }, + { + display: 'Help & Support', + to: '/support', + }, + ], + name: 'More', + }, + ]; return groups; }, @@ -367,9 +351,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => { isACLPEnabled, isACLPLogsBeta, isACLPLogsEnabled, - isIAMBeta, isIAMEnabled, - iamRbacPrimaryNavChanges, isMarketplaceV2FeatureEnabled, isNetworkLoadBalancerEnabled, limitsEvolution, diff --git a/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.test.tsx b/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.test.tsx index 8b8da628179..665c47ba819 100644 --- a/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.test.tsx +++ b/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.test.tsx @@ -13,4 +13,20 @@ describe('QuotaUsageBanner', () => { const quotaUsageText = getByText('1 of 10 Bytes used'); expect(quotaUsageText).toBeVisible(); }); + + it.each([1000000000, 100000000, 10000000, 1000000])( + 'should display content usage in proper format', + (usage) => { + const { getByText } = renderWithTheme( + + ); + + const quotaUsageText = getByText('<0.01 of 100 TB used'); + expect(quotaUsageText).toBeVisible(); + } + ); }); diff --git a/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.tsx b/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.tsx index d7a19a57304..d041a256567 100644 --- a/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.tsx +++ b/packages/manager/src/components/QuotaUsageBar/QuotaUsageBar.tsx @@ -23,6 +23,18 @@ export const QuotaUsageBar = ({ limit, usage, resourceMetric }: Props) => { initialLimit: limit, }); + function getUsageText() { + let convertedUsageString = convertedUsage.toLocaleString(); + const convertedLimitString = convertedLimit.toLocaleString(); + + // Special case to display storage usage + if (convertedUsage === 0 && convertedResourceMetric === 'TB') { + convertedUsageString = '<0.01'; + } + + return `${convertedUsageString} of ${convertedLimitString} ${convertedResourceMetric} used`; + } + return ( <> { sx={{ mb: 1, mt: 2, padding: '3px' }} value={usage} /> - - {`${convertedUsage?.toLocaleString() ?? 'unknown'} of ${ - convertedLimit?.toLocaleString() ?? 'unknown' - } ${convertedResourceMetric} used`} - + {getUsageText()} ); }; diff --git a/packages/manager/src/components/SelectionCard/SelectionCard.tsx b/packages/manager/src/components/SelectionCard/SelectionCard.tsx index cc9985b5f56..4e4ab92047e 100644 --- a/packages/manager/src/components/SelectionCard/SelectionCard.tsx +++ b/packages/manager/src/components/SelectionCard/SelectionCard.tsx @@ -39,7 +39,7 @@ export interface SelectionCardProps { * The heading of the card. * @example Linode 1GB */ - heading: string; + heading: JSX.Element | string; /** * An optional decoration to display next to the heading. * @example (Current) diff --git a/packages/manager/src/components/TypeToConfirm/TypeToConfirm.test.tsx b/packages/manager/src/components/TypeToConfirm/TypeToConfirm.test.tsx index 2c18dc04d40..c21a5742fb9 100644 --- a/packages/manager/src/components/TypeToConfirm/TypeToConfirm.test.tsx +++ b/packages/manager/src/components/TypeToConfirm/TypeToConfirm.test.tsx @@ -72,7 +72,7 @@ describe('TypeToConfirm Component', () => { expect( queryByTestId('instructions-to-enable-or-disable') ).toBeInTheDocument(); - expect(getByRole('link')).toHaveAttribute('href', '/profile/settings'); + expect(getByRole('link')).toHaveAttribute('href', '/profile/preferences'); }); it('Should not display instructions when toggled to hidden', () => { diff --git a/packages/manager/src/components/TypeToConfirm/TypeToConfirm.tsx b/packages/manager/src/components/TypeToConfirm/TypeToConfirm.tsx index 29bb8314cee..389401f1698 100644 --- a/packages/manager/src/components/TypeToConfirm/TypeToConfirm.tsx +++ b/packages/manager/src/components/TypeToConfirm/TypeToConfirm.tsx @@ -5,7 +5,6 @@ import type { JSX } from 'react'; import { FormGroup } from 'src/components/FormGroup'; import { Link } from 'src/components/Link'; -import { useFlags } from 'src/hooks/useFlags'; import type { TextFieldProps, TypographyProps } from '@linode/ui'; import type { Theme } from '@mui/material'; @@ -53,8 +52,6 @@ export const TypeToConfirm = (props: TypeToConfirmProps) => { (preferences) => preferences?.type_to_confirm ?? true ); - const { iamRbacPrimaryNavChanges } = useFlags(); - /* There is an edge case where preferences?.type_to_confirm is undefined when the user has not yet set a preference as seen in /profile/settings?preferenceEditor=true. @@ -118,17 +115,7 @@ export const TypeToConfirm = (props: TypeToConfirmProps) => { sx={{ marginTop: 1 }} > To {disableOrEnable} type-to-confirm, go to the Type-to-Confirm - section of{' '} - - {iamRbacPrimaryNavChanges ? 'Preferences' : 'My Settings'} - - . + section of Preferences. ) : null} diff --git a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx index fc3f9e1337f..40b33ec8478 100644 --- a/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx +++ b/packages/manager/src/components/TypeToConfirmDialog/TypeToConfirmDialog.tsx @@ -37,6 +37,7 @@ interface EntityInfo { | 'Managed Credential' | 'Managed Service Monitor' | 'NodeBalancer' + | 'Notification Channel' | 'Placement Group' | 'Subnet' | 'Volume' diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index a3b79b4945d..6a6d8e27948 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -68,7 +68,6 @@ const options: { flag: keyof Flags; label: string }[] = [ label: 'IAM Limited Availability Badges', }, { flag: 'iamDelegation', label: 'IAM Delegation (Parent/Child)' }, - { flag: 'iamRbacPrimaryNavChanges', label: 'IAM Primary Nav Changes' }, { flag: 'linodeCloneFirewall', label: 'Linode Clone Firewall', @@ -78,6 +77,14 @@ const options: { flag: keyof Flags; label: string }[] = [ label: 'VM Host Maintenance Policy', }, { flag: 'volumeSummaryPage', label: 'Volume Summary Page' }, + { + flag: 'blockStorageContextualMetrics', + label: 'Block Storage Contextual Metrics', + }, + { + flag: 'objectStorageContextualMetrics', + label: 'Object Storage Contextual Metrics', + }, { flag: 'objSummaryPage', label: 'OBJ Summary Page' }, { flag: 'vpcIpv6', label: 'VPC IPv6' }, ]; diff --git a/packages/manager/src/dev-tools/utils.ts b/packages/manager/src/dev-tools/utils.ts index 88839cdf6b8..dbed6769e15 100644 --- a/packages/manager/src/dev-tools/utils.ts +++ b/packages/manager/src/dev-tools/utils.ts @@ -92,9 +92,9 @@ export const saveSeedsCountMap = (countMap: { [key: string]: number }) => { /** * Retrieves the presets map from local storage. */ -export const getExtraPresetsMap = (): { +export const getExtraPresetsMap = (): Partial<{ [K in MockPresetExtraId]: number; -} => { +}> => { const encodedPresetsMap = localStorage.getItem(LOCAL_STORAGE_PRESETS_MAP_KEY); return encodedPresetsMap ? JSON.parse(encodedPresetsMap) : {}; diff --git a/packages/manager/src/factories/account.ts b/packages/manager/src/factories/account.ts index f463fe6f83c..de029b12345 100644 --- a/packages/manager/src/factories/account.ts +++ b/packages/manager/src/factories/account.ts @@ -56,6 +56,7 @@ export const accountFactory = Factory.Sync.makeFactory({ 'Placement Group', 'Vlans', 'Kubernetes Enterprise', + 'VPC Dual Stack', ], city: 'Philadelphia', company: Factory.each((i) => `company-${i}`), diff --git a/packages/manager/src/factories/cloudpulse/channels.ts b/packages/manager/src/factories/cloudpulse/channels.ts index ee361063bb0..970569d5df3 100644 --- a/packages/manager/src/factories/cloudpulse/channels.ts +++ b/packages/manager/src/factories/cloudpulse/channels.ts @@ -1,6 +1,9 @@ import { Factory } from '@linode/utilities'; -import type { NotificationChannel } from '@linode/api-v4'; +import type { + NotificationChannel, + NotificationChannelAlerts, +} from '@linode/api-v4'; export const notificationChannelFactory = Factory.Sync.makeFactory({ @@ -26,3 +29,12 @@ export const notificationChannelFactory = updated: new Date().toISOString(), updated_by: 'user1', }); + +export const notificationChannelAlertsFactory = + Factory.Sync.makeFactory({ + type: 'alerts-definitions', + id: Factory.each((i) => i), + service_type: 'linode', + label: Factory.each((id) => `Alert-${id}`), + url: Factory.each((i) => `monitor/alert-definitions/${i}`), + }); diff --git a/packages/manager/src/factories/index.ts b/packages/manager/src/factories/index.ts index 03dd55cb4cf..39b2e8ae99f 100644 --- a/packages/manager/src/factories/index.ts +++ b/packages/manager/src/factories/index.ts @@ -24,6 +24,7 @@ export * from './images'; export * from './kernels'; export * from './kubernetesCluster'; export * from './linodeConfigs'; +export * from './locks'; export * from './longviewClient'; export * from './longviewDisks'; export * from './longviewProcess'; diff --git a/packages/manager/src/factories/locks.ts b/packages/manager/src/factories/locks.ts new file mode 100644 index 00000000000..59685657a6f --- /dev/null +++ b/packages/manager/src/factories/locks.ts @@ -0,0 +1,13 @@ +import { Factory } from '@linode/utilities'; + +import { entityFactory } from './events'; + +import type { ResourceLock } from '@linode/api-v4'; + +export const lockFactory = Factory.Sync.makeFactory({ + id: Factory.each((i) => i), + lock_type: 'cannot_delete', + entity: entityFactory.build({ + type: 'linode', + }), +}); diff --git a/packages/manager/src/factories/userAccountPermissions.ts b/packages/manager/src/factories/userAccountPermissions.ts index 1aa5ed98ca6..db1385b263a 100644 --- a/packages/manager/src/factories/userAccountPermissions.ts +++ b/packages/manager/src/factories/userAccountPermissions.ts @@ -3,4 +3,5 @@ import type { PermissionType } from '@linode/api-v4'; export const userAccountPermissionsFactory: PermissionType[] = [ 'create_linode', 'create_firewall', + 'create_vpc', ]; diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index c0bfac6e2fa..2fe9b36763d 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -203,6 +203,7 @@ export interface Flags { apl: boolean; aplGeneralAvailability: boolean; aplLkeE: boolean; + blockStorageContextualMetrics: boolean; blockStorageEncryption: boolean; blockStorageVolumeLimit: boolean; cloudManagerDesignUpdatesBanner: DesignUpdatesBannerFlag; @@ -223,10 +224,9 @@ export interface Flags { gecko2: GeckoFeatureFlag; generationalPlansv2: GenerationalPlansFlag; gpuv2: GpuV2; - iam: BetaFeatureFlag; + iam: BaseFeatureFlag; iamDelegation: BaseFeatureFlag; iamLimitedAvailabilityBadges: boolean; - iamRbacPrimaryNavChanges: boolean; ipv6Sharing: boolean; kubernetesBlackwellPlans: boolean; limitsEvolution: LimitsEvolution; @@ -242,6 +242,7 @@ export interface Flags { networkLoadBalancer: boolean; nodebalancerIpv6: boolean; nodebalancerVpc: boolean; + objectStorageContextualMetrics: boolean; objectStorageGen2: BaseFeatureFlag; objMultiCluster: boolean; objSummaryPage: boolean; diff --git a/packages/manager/src/features/Account/AccountLanding.tsx b/packages/manager/src/features/Account/AccountLanding.tsx index 0f626cc9850..9e87f756a9b 100644 --- a/packages/manager/src/features/Account/AccountLanding.tsx +++ b/packages/manager/src/features/Account/AccountLanding.tsx @@ -1,5 +1,4 @@ import { useAccount, useProfile } from '@linode/queries'; -import { NotFound } from '@linode/ui'; import { Outlet, useLocation, @@ -39,7 +38,7 @@ export const AccountLanding = () => { }); const { data: account } = useAccount(); const { data: profile } = useProfile(); - const { iamRbacPrimaryNavChanges, limitsEvolution } = useFlags(); + const { limitsEvolution } = useFlags(); const { data: permissions } = usePermissions('account', [ 'make_billing_payment', @@ -101,16 +100,13 @@ export const AccountLanding = () => { React.useEffect(() => { if (match.routeId === '/account/quotas' && !showQuotasTab) { navigate({ - to: iamRbacPrimaryNavChanges ? '/quotas' : '/account/billing', + to: '/quotas', }); } - }, [match.routeId, showQuotasTab, navigate, iamRbacPrimaryNavChanges]); + }, [match.routeId, showQuotasTab, navigate]); // This is the default route for the account route, so we need to redirect to the billing tab but keep /account as legacy if (location.pathname === '/account') { - if (iamRbacPrimaryNavChanges) { - return ; - } navigate({ to: '/account/billing', }); @@ -153,7 +149,7 @@ export const AccountLanding = () => { if (!isAkamaiAccount) { landingHeaderProps.onButtonClick = () => navigate({ - to: iamRbacPrimaryNavChanges ? '/billing' : '/account/billing', + to: '/billing', search: { action: 'make-payment' }, }); } diff --git a/packages/manager/src/features/Account/AccountLogins.tsx b/packages/manager/src/features/Account/AccountLogins.tsx index 232a99f4f13..79cdae2ec1c 100644 --- a/packages/manager/src/features/Account/AccountLogins.tsx +++ b/packages/manager/src/features/Account/AccountLogins.tsx @@ -15,7 +15,6 @@ import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableRowError } from 'src/components/TableRowError/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; import { TableSortCell } from 'src/components/TableSortCell'; -import { useFlags } from 'src/hooks/useFlags'; import { useOrderV2 } from 'src/hooks/useOrderV2'; import { usePaginationV2 } from 'src/hooks/usePaginationV2'; @@ -44,14 +43,11 @@ const useStyles = makeStyles()((theme: Theme) => ({ const AccountLogins = () => { const { classes } = useStyles(); - const flags = useFlags(); const { data: permissions } = usePermissions('account', [ 'list_account_logins', ]); const pagination = usePaginationV2({ - currentRoute: flags?.iamRbacPrimaryNavChanges - ? '/login-history' - : '/account/login-history', + currentRoute: '/login-history', preferenceKey: 'account-logins-pagination', }); @@ -61,9 +57,7 @@ const AccountLogins = () => { order: 'desc', orderBy: 'datetime', }, - from: flags?.iamRbacPrimaryNavChanges - ? '/login-history' - : '/account/login-history', + from: '/login-history', }, preferenceKey: `${preferenceKey}-order`, }); diff --git a/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx b/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx index 8a283a565d7..ec2c6cb9bb7 100644 --- a/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx +++ b/packages/manager/src/features/Account/Maintenance/MaintenanceTable.tsx @@ -73,9 +73,7 @@ export const MaintenanceTable = ({ type }: Props) => { const flags = useFlags(); const pagination = usePaginationV2({ - currentRoute: flags?.iamRbacPrimaryNavChanges - ? `/maintenance` - : `/account/maintenance`, + currentRoute: '/maintenance', preferenceKey: `${preferenceKey}-${type}`, queryParamsPrefix: type, }); @@ -86,9 +84,7 @@ export const MaintenanceTable = ({ type }: Props) => { order: 'desc', orderBy: 'status', }, - from: flags?.iamRbacPrimaryNavChanges - ? `/maintenance` - : `/account/maintenance`, + from: '/maintenance', }, preferenceKey: `${preferenceKey}-order-${type}`, prefix: type, diff --git a/packages/manager/src/features/Account/NetworkInterfaceType.test.tsx b/packages/manager/src/features/Account/NetworkInterfaceType.test.tsx index 41ba4809a79..f43ba89293a 100644 --- a/packages/manager/src/features/Account/NetworkInterfaceType.test.tsx +++ b/packages/manager/src/features/Account/NetworkInterfaceType.test.tsx @@ -21,7 +21,7 @@ describe('NetworkInterfaces', () => { const { getByText } = renderWithTheme(); expect(getByText('Network Interface Type')).toBeVisible(); - expect(getByText('Interfaces for new Linodes')).toBeVisible(); + expect(getByText('Allowed interfaces for new Linodes')).toBeVisible(); expect(getByText('Save')).toBeVisible(); }); @@ -49,9 +49,9 @@ describe('NetworkInterfaces', () => { ); - expect(getByLabelText('Interfaces for new Linodes')).toHaveAttribute( - 'disabled' - ); + expect( + getByLabelText('Allowed interfaces for new Linodes') + ).toHaveAttribute('disabled'); expect(getByText('Save')).toHaveAttribute('aria-disabled', 'true'); }); }); diff --git a/packages/manager/src/features/Account/NetworkInterfaceType.tsx b/packages/manager/src/features/Account/NetworkInterfaceType.tsx index 9e42490ddd0..37837ebd431 100644 --- a/packages/manager/src/features/Account/NetworkInterfaceType.tsx +++ b/packages/manager/src/features/Account/NetworkInterfaceType.tsx @@ -1,11 +1,20 @@ import { useAccountSettings, useMutateAccountSettings } from '@linode/queries'; -import { Box, Button, Paper, Select, Stack, Typography } from '@linode/ui'; +import { + Box, + Button, + Paper, + Select, + Stack, + Typography, + useTheme, +} from '@linode/ui'; import { useSnackbar } from 'notistack'; import React from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { Link } from 'src/components/Link'; + import { usePermissions } from '../IAM/hooks/usePermissions'; -import { LinodeInterfaceFeatureStatusChip } from '../Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaceFeatureChip'; import type { AccountSettings, @@ -18,29 +27,44 @@ type InterfaceSettingValues = Pick< 'interfaces_for_new_linodes' >; -const accountSettingInterfaceOptions: SelectOption[] = - [ - { - label: 'Linode Interfaces but allow Configuration Profile Interfaces', - value: 'linode_default_but_legacy_config_allowed', - }, - { - label: 'Linode Interfaces Only', - value: 'linode_only', - }, - { - label: 'Configuration Profile Interfaces but allow Linode Interfaces', - value: 'legacy_config_default_but_linode_allowed', - }, - { - label: 'Configuration Profile Interfaces Only', - value: 'legacy_config_only', - }, - ]; +interface AccountSettingInterfaceOptionType + extends SelectOption { + tooltipText: string; +} + +const accountSettingInterfaceOptions: AccountSettingInterfaceOptionType[] = [ + { + label: + 'Linode Interfaces (default) but allow Configuration Profile Interfaces', + value: 'linode_default_but_legacy_config_allowed', + tooltipText: + 'Linode Interfaces are used by default unless you select Configuration Profile Interfaces. Linodes with Configuration Profile Interfaces can be upgraded to Linode Interfaces.', + }, + { + label: 'Linode Interfaces Only', + value: 'linode_only', + tooltipText: + 'Existing Linodes with Configuration Profile Interfaces will continue to work. You can upgrade these Linodes to use Linode Interfaces.', + }, + { + label: + 'Configuration Profile Interfaces (default) but allow Linode Interfaces', + value: 'legacy_config_default_but_linode_allowed', + tooltipText: + 'Configuration Profile Interfaces are used by default unless you select Linode Interfaces. You can upgrade to Linode Interfaces at any time.', + }, + { + label: 'Configuration Profile Interfaces Only', + value: 'legacy_config_only', + tooltipText: + 'Existing Linodes with Linode Interfaces will continue to work. Upgrades to Linode Interfaces are not available.', + }, +]; export const NetworkInterfaceType = () => { const { enqueueSnackbar } = useSnackbar(); const { data: accountSettings } = useAccountSettings(); + const theme = useTheme(); const { mutateAsync: updateAccountSettings } = useMutateAccountSettings(); const { data: permissions } = usePermissions('account', [ @@ -75,14 +99,23 @@ export const NetworkInterfaceType = () => { return ( - Network Interface Type + + Network Interface Type +
- - Choose whether to use Configuration Profile Interfaces or Linode - Interfaces - - when creating new Linodes or upgrading existing ones. + + When creating new Linodes or upgrading existing ones, select between + Configuration Profile Interfaces and Linode Interfaces.{' '} + + Learn more + + . + + + Linode Interfaces are recommended. However, use Configuration + Profile Interfaces with LKE or when a Linode needs a private IP + address. {