diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index 87635e8b4fa..bc1b61f6703 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,9 +1,32 @@ -## [2025-08-26] - v0.147.0 +## [2025-09-09] - v0.148.0 + +### Added: + +- Support for Node Pool `label` field ([#12710](https://github.com/linode/manager/pull/12710)) +- Volumes IAM RBAC permissions ([#12744](https://github.com/linode/manager/pull/12744)) +- NodeBalancers IAM RBAC permissions ([#12780](https://github.com/linode/manager/pull/12780)) +- Additional device slots to `Devices` type to match new API limits ([#12791](https://github.com/linode/manager/pull/12791)) + +### Changed: + +- Use `v4beta` API endpoint for `updateNodePool` ([#12710](https://github.com/linode/manager/pull/12710)) +- Update `CreateNodePoolData` to satisfy @linode/validation's `CreateNodePoolSchema`'s type ([#12793](https://github.com/linode/manager/pull/12793)) + +### Fixed: +- Wrong import path for EntityType ([#12764](https://github.com/linode/manager/pull/12764)) + +### Upcoming Features: + +- Add DELETE, PUT API endpoints for Streams ([#12645](https://github.com/linode/manager/pull/12645)) +- ACLP Alert: Add `regions` property in `CreateAlertDefinitionPayload` and `EditAlertDefinitionPayload` ([#12745](https://github.com/linode/manager/pull/12745)) +- Add DELETE, PUT API endpoints for Destinations ([#12749](https://github.com/linode/manager/pull/12749)) + +## [2025-08-26] - v0.147.0 ### Added: -- ACLP: `CloudPulseServiceType` type for type safety across cloudpulse ([#12646](https://github.com/linode/manager/pull/12646)) +- ACLP: `CloudPulseServiceType` type for type safety across cloudpulse ([#12646](https://github.com/linode/manager/pull/12646)) ### Changed: @@ -18,12 +41,11 @@ - API endpoint for Datastream - Create Destination ([#12627](https://github.com/linode/manager/pull/12627)) - Updated AccontMaintenance interface to make time fields nullable to match API ([#12665](https://github.com/linode/manager/pull/12665)) -- Update `KubernetesCluster` `vpc_id` and `subnet_id` types to include `null` ([#12700](https://github.com/linode/manager/pull/12700)) +- Update `KubernetesCluster` `vpc_id` and `subnet_id` types to include `null` ([#12700](https://github.com/linode/manager/pull/12700)) - CloudPulse: Update cloud pulse metrics request payload type at `types.ts` ([#12704](https://github.com/linode/manager/pull/12704)) ## [2025-08-12] - v0.146.0 - ### Added: - ACLP: `string` type for `capabilityServiceTypeMapping` constant ([#12573](https://github.com/linode/manager/pull/12573)) @@ -51,7 +73,6 @@ ## [2025-07-29] - v0.145.0 - ### Added: - `VPC Dual Stack` and `VPC IPv6 Large Prefixes` to account capabilities ([#12309](https://github.com/linode/manager/pull/12309)) @@ -69,14 +90,13 @@ ### Upcoming Features: - CloudPulse: Update service type in `types.ts` ([#12508](https://github.com/linode/manager/pull/12508)) -- ACLP-Alerting: Add nodebalancer to AlertServiceType for Alerts onboarding ([#12510](https://github.com/linode/manager/pull/12510)) +- ACLP-Alerting: Add nodebalancer to AlertServiceType for Alerts onboarding ([#12510](https://github.com/linode/manager/pull/12510)) - Add vpc_id and subnet_id to KubernetesCluster payload type ([#12513](https://github.com/linode/manager/pull/12513)) - Add API endpoints (GET, POST) for Streams ([#12524](https://github.com/linode/manager/pull/12524)) - ACLP-Alerting: Add firewall to AlertServiceType for Alerts onboarding ([#12550](https://github.com/linode/manager/pull/12550)) ## [2025-07-15] - v0.144.0 - ### Changed: - ACLP:Alerting - fixed the typo from evaluation_periods_seconds to evaluation_period_seconds ([#12466](https://github.com/linode/manager/pull/12466)) @@ -96,7 +116,7 @@ ### Changed: -- Allow `authorized_keys` to be null in `Profile` type ([#12390](https://github.com/linode/manager/pull/12390)) +- Allow `authorized_keys` to be null in `Profile` type ([#12390](https://github.com/linode/manager/pull/12390)) ### Removed: @@ -114,7 +134,6 @@ ## [2025-06-17] - v0.142.0 - ### Added: - `has_user_data` to `Linode` type ([#12352](https://github.com/linode/manager/pull/12352)) diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index 26480538eb5..fe6accebbe4 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.147.0", + "version": "0.148.0", "homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4", "bugs": { "url": "https://github.com/linode/manager/issues" @@ -70,4 +70,4 @@ "tsc -p tsconfig.json --noEmit true --emitDeclarationOnly false" ] } -} +} \ No newline at end of file diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 36def13c55b..31b1c186491 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -344,6 +344,8 @@ export const EventActionKeys = [ 'database_migrate', 'database_upgrade', 'destination_create', + 'destination_delete', + 'destination_update', 'disk_create', 'disk_delete', 'disk_duplicate', @@ -470,6 +472,8 @@ export const EventActionKeys = [ 'stackscript_revise', 'stackscript_update', 'stream_create', + 'stream_delete', + 'stream_update', 'subnet_create', 'subnet_delete', 'subnet_update', diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index 837c0503bbe..72b3d72d9db 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -196,6 +196,7 @@ export interface CreateAlertDefinitionPayload { description?: string; entity_ids?: string[]; label: string; + regions?: string[]; rule_criteria: { rules: MetricCriteria[]; }; @@ -337,10 +338,10 @@ export interface EditAlertDefinitionPayload { description?: string; entity_ids?: string[]; label?: string; + regions?: string[]; rule_criteria?: { rules: MetricCriteria[]; }; - scope?: AlertDefinitionScope; severity?: AlertSeverityType; status?: AlertStatusType; tags?: string[]; diff --git a/packages/api-v4/src/datastream/destinations.ts b/packages/api-v4/src/datastream/destinations.ts index 66784f50ff3..c3c8b00a231 100644 --- a/packages/api-v4/src/datastream/destinations.ts +++ b/packages/api-v4/src/datastream/destinations.ts @@ -1,4 +1,4 @@ -import { createDestinationSchema } from '@linode/validation'; +import { destinationSchema } from '@linode/validation'; import { BETA_API_ROOT } from '../constants'; import Request, { @@ -10,7 +10,11 @@ import Request, { } from '../request'; import type { Filter, ResourcePage as Page, Params } from '../types'; -import type { CreateDestinationPayload, Destination } from './types'; +import type { + CreateDestinationPayload, + Destination, + UpdateDestinationPayload, +} from './types'; /** * Returns all the information about a specified Destination. @@ -45,7 +49,38 @@ export const getDestinations = (params?: Params, filter?: Filter) => */ export const createDestination = (data: CreateDestinationPayload) => Request( - setData(data, createDestinationSchema), + setData(data, destinationSchema), setURL(`${BETA_API_ROOT}/monitor/streams/destinations`), setMethod('POST'), ); + +/** + * Updates a Destination. + * + * @param destinationId { number } The ID of the Destination. + * @param data { object } Options for type, label, etc. + */ +export const updateDestination = ( + destinationId: number, + data: UpdateDestinationPayload, +) => + Request( + setData(data, destinationSchema), + setURL( + `${BETA_API_ROOT}/monitor/streams/destinations/${encodeURIComponent(destinationId)}`, + ), + setMethod('PUT'), + ); + +/** + * Deletes a Destination. + * + * @param destinationId { number } The ID of the Destination. + */ +export const deleteDestination = (destinationId: number) => + Request<{}>( + setURL( + `${BETA_API_ROOT}/monitor/streams/destinations/${encodeURIComponent(destinationId)}`, + ), + setMethod('DELETE'), + ); diff --git a/packages/api-v4/src/datastream/streams.ts b/packages/api-v4/src/datastream/streams.ts index 2e9ef2229cd..621bafa7247 100644 --- a/packages/api-v4/src/datastream/streams.ts +++ b/packages/api-v4/src/datastream/streams.ts @@ -1,4 +1,4 @@ -import { createStreamSchema } from '@linode/validation'; +import { createStreamSchema, updateStreamSchema } from '@linode/validation'; import { BETA_API_ROOT } from '../constants'; import Request, { @@ -10,7 +10,7 @@ import Request, { } from '../request'; import type { Filter, ResourcePage as Page, Params } from '../types'; -import type { CreateStreamPayload, Stream } from './types'; +import type { CreateStreamPayload, Stream, UpdateStreamPayload } from './types'; /** * Returns all the information about a specified Stream. @@ -47,3 +47,27 @@ export const createStream = (data: CreateStreamPayload) => setURL(`${BETA_API_ROOT}/monitor/streams`), setMethod('POST'), ); + +/** + * Updates a Stream. + * + * @param streamId { number } The ID of the Stream. + * @param data { object } Options for type, status, etc. + */ +export const updateStream = (streamId: number, data: UpdateStreamPayload) => + Request( + setData(data, updateStreamSchema), + setURL(`${BETA_API_ROOT}/monitor/streams/${encodeURIComponent(streamId)}`), + setMethod('PUT'), + ); + +/** + * Deletes a Stream. + * + * @param streamId { number } The ID of the Stream. + */ +export const deleteStream = (streamId: number) => + Request<{}>( + setURL(`${BETA_API_ROOT}/monitor/streams/${encodeURIComponent(streamId)}`), + setMethod('DELETE'), + ); diff --git a/packages/api-v4/src/datastream/types.ts b/packages/api-v4/src/datastream/types.ts index 76f50d5e919..15b3129fdd3 100644 --- a/packages/api-v4/src/datastream/types.ts +++ b/packages/api-v4/src/datastream/types.ts @@ -103,14 +103,33 @@ interface CustomHeader { export interface CreateStreamPayload { destinations: number[]; - details?: StreamDetails; + details: StreamDetails; label: string; status?: StreamStatus; type: StreamType; } +export interface UpdateStreamPayload { + destinations: number[]; + details: StreamDetails; + label: string; + status: StreamStatus; + type: StreamType; +} + +export interface UpdateStreamPayloadWithId extends UpdateStreamPayload { + id: number; +} + export interface CreateDestinationPayload { details: CustomHTTPsDetails | LinodeObjectStorageDetails; label: string; type: DestinationType; } + +export type UpdateDestinationPayload = CreateDestinationPayload; + +export interface UpdateDestinationPayloadWithId + extends UpdateDestinationPayload { + id: number; +} diff --git a/packages/api-v4/src/iam/types.ts b/packages/api-v4/src/iam/types.ts index 677cb9cafb5..fdf1a6bba56 100644 --- a/packages/api-v4/src/iam/types.ts +++ b/packages/api-v4/src/iam/types.ts @@ -1,4 +1,4 @@ -import type { EntityType } from 'src/entities/types'; +import type { EntityType } from '../entities/types'; export type AccountType = 'account'; @@ -102,7 +102,9 @@ export type AccountAdmin = | AccountBillingAdmin | AccountFirewallAdmin | AccountLinodeAdmin - | AccountOauthClientAdmin; + | AccountNodeBalancerAdmin + | AccountOauthClientAdmin + | AccountVolumeAdmin; /** Permissions associated with the "account_billing_admin" role. */ export type AccountBillingAdmin = @@ -141,6 +143,20 @@ export type AccountLinodeAdmin = AccountLinodeCreator | LinodeAdmin; /** Permissions associated with the "account_linode_creator" role. */ export type AccountLinodeCreator = 'create_linode'; +/** Permissions associated with the "account_nodebalancer_admin" role. */ +export type AccountNodeBalancerAdmin = + | AccountNodeBalancerCreator + | NodeBalancerAdmin; + +/** Permissions associated with the "account_nodebalancer_creator" role. */ +export type AccountNodeBalancerCreator = 'create_nodebalancer'; + +/** Permissions associated with the "account_volume_admin" role. */ +export type AccountVolumeAdmin = AccountVolumeCreator | VolumeAdmin; + +/** Permissions associated with the "account_volume_creator" role. */ +export type AccountVolumeCreator = 'create_volume'; + /** Permissions associated with the "account_maintenance_viewer" role. */ export type AccountMaintenanceViewer = 'list_maintenances'; @@ -207,7 +223,8 @@ export type AccountViewer = | AccountOauthClientViewer | AccountProfileViewer | FirewallViewer - | LinodeViewer; + | LinodeViewer + | VolumeViewer; /** Permissions associated with the "firewall_admin role. */ export type FirewallAdmin = @@ -287,45 +304,50 @@ export type LinodeViewer = | 'view_linode_network_transfer' | 'view_linode_stats'; -/** Facade roles represent the existing Grant model for entities that are not yet migrated to IAM */ -export type AccountRoleFacade = - | 'account_database_creator' - | 'account_domain_creator' - | 'account_image_creator' - | 'account_ip_admin' - | 'account_ip_viewer' - | 'account_lkecluster_creator' - | 'account_longview_creator' - | 'account_longview_subscription_admin' - | 'account_nodebalancer_creator' - | 'account_placement_group_creator' - | 'account_stackscript_creator' - | 'account_vlan_admin' - | 'account_vlan_viewer' - | 'account_volume_creator' - | 'account_vpc_creator'; - -/** Facade roles represent the existing Grant model for entities that are not yet migrated to IAM */ -export type EntityRoleFacade = - | 'database_admin' - | 'database_viewer' - | 'domain_admin' - | 'domain_viewer' - | 'image_admin' - | 'image_viewer' - | 'lkecluster_admin' - | 'lkecluster_viewer' - | 'longview_admin' - | 'longview_viewer' - | 'nodebalancer_admin' - | 'nodebalancer_viewer' - | 'placement_group_admin' - | 'placement_group_viewer' - | 'stackscript_admin' - | 'stackscript_viewer' - | 'volume_admin' - | 'volume_viewer' - | 'vpc_admin'; +/** Permissions associated with the "nodebalancer_admin" role. */ +// TODO: UIE-9154 - verify mapping for Nodebalancer as this is not migrated yet +export type NodeBalancerAdmin = + | 'delete_nodebalancer' + | 'delete_nodebalancer_config' + | 'delete_nodebalancer_config_node' + | NodeBalancerContributor; + +/** Permissions associated with the "nodebalancer_contributor" role. */ +export type NodeBalancerContributor = + | 'create_nodebalancer_config' + | 'create_nodebalancer_config_node' + | 'rebuild_nodebalancer_config' + | 'update_nodebalancer' + | 'update_nodebalancer_config' + | 'update_nodebalancer_config_node' + | 'update_nodebalancer_firewalls' + | NodeBalancerViewer; + +/** Permissions associated with the "nodebalancer_viewer" role. */ +export type NodeBalancerViewer = + | 'list_nodebalancer_config_nodes' + | 'list_nodebalancer_configs' + | 'list_nodebalancer_firewalls' + | 'view_nodebalancer' + | 'view_nodebalancer_config' + | 'view_nodebalancer_config_node' + | 'view_nodebalancer_statistics'; + +/** Permissions associated with the "volume_admin" role. */ +export type VolumeAdmin = 'delete_volume' | VolumeContributor; + +/** Permissions associated with the "volume_contributor" role. */ +export type VolumeContributor = + | 'attach_volume' + | 'clone_volume' + | 'delete_volume' + | 'detach_volume' + | 'resize_volume' + | 'update_volume' + | VolumeViewer; + +/** Permissions associated with the "volume_viewer" role. */ +export type VolumeViewer = 'view_volume'; /** Union of all permissions */ export type PermissionType = AccountAdmin; diff --git a/packages/api-v4/src/kubernetes/nodePools.ts b/packages/api-v4/src/kubernetes/nodePools.ts index 89b8d6b7daa..faf8895e28c 100644 --- a/packages/api-v4/src/kubernetes/nodePools.ts +++ b/packages/api-v4/src/kubernetes/nodePools.ts @@ -1,4 +1,4 @@ -import { nodePoolSchema } from '@linode/validation/lib/kubernetes.schema'; +import { CreateNodePoolSchema, EditNodePoolSchema } from '@linode/validation'; import { API_ROOT, BETA_API_ROOT } from '../constants'; import Request, { @@ -61,7 +61,7 @@ export const createNodePool = (clusterID: number, data: CreateNodePoolData) => setURL( `${BETA_API_ROOT}/lke/clusters/${encodeURIComponent(clusterID)}/pools`, ), - setData(data, nodePoolSchema), + setData(data, CreateNodePoolSchema), ); /** @@ -77,11 +77,11 @@ export const updateNodePool = ( Request( setMethod('PUT'), setURL( - `${API_ROOT}/lke/clusters/${encodeURIComponent( + `${BETA_API_ROOT}/lke/clusters/${encodeURIComponent( clusterID, )}/pools/${encodeURIComponent(nodePoolID)}`, ), - setData(data, nodePoolSchema), + setData(data, EditNodePoolSchema), ); /** diff --git a/packages/api-v4/src/kubernetes/types.ts b/packages/api-v4/src/kubernetes/types.ts index a518d0260d7..f0181314853 100644 --- a/packages/api-v4/src/kubernetes/types.ts +++ b/packages/api-v4/src/kubernetes/types.ts @@ -70,6 +70,12 @@ export interface KubeNodePoolResponse { * @note Only returned for LKE Enterprise clusters */ k8s_version?: string; + /** + * A label/name for the Node Pool + * + * @default "" + */ + label: string; labels: Label; nodes: PoolNodeResponse[]; tags: string[]; @@ -104,7 +110,7 @@ export interface CreateNodePoolData { * * @note Only supported on LKE Enterprise clusters */ - firewall_id?: number; + firewall_id?: null | number; /** * The LKE version that the node pool should use. * @@ -112,15 +118,21 @@ export interface CreateNodePoolData { * @note This field may be required when creating a Node Pool on a LKE Enterprise cluster */ k8s_version?: string; + /** + * An optional label/name for the Node Pool. + * + * @default "" + */ + label?: string; /** * Key-value pairs added as labels to nodes in the node pool. */ - labels?: Label; - tags?: string[]; + labels?: Label | null; + tags?: null | string[]; /** * Kubernetes taints to add to node pool nodes. */ - taints?: Taint[]; + taints?: null | Taint[]; /** * The Linode Type for all of the nodes in the Node Pool. */ @@ -132,7 +144,7 @@ export interface CreateNodePoolData { * @note Only supported on LKE Enterprise clusters * @default on_recycle */ - update_strategy?: NodePoolUpdateStrategy; + update_strategy?: NodePoolUpdateStrategy | null; } export type UpdateNodePoolData = Partial; diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index 48005cd73d2..114a7dce89d 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -388,15 +388,73 @@ export interface VolumeDevice { volume_id: null | number; } +export type ConfigDevice = DiskDevice | null | VolumeDevice; + export interface Devices { - sda: DiskDevice | null | VolumeDevice; - sdb: DiskDevice | null | VolumeDevice; - sdc: DiskDevice | null | VolumeDevice; - sdd: DiskDevice | null | VolumeDevice; - sde: DiskDevice | null | VolumeDevice; - sdf: DiskDevice | null | VolumeDevice; - sdg: DiskDevice | null | VolumeDevice; - sdh: DiskDevice | null | VolumeDevice; + sda?: ConfigDevice; + sdaa?: ConfigDevice; + sdab?: ConfigDevice; + sdac?: ConfigDevice; + sdad?: ConfigDevice; + sdae?: ConfigDevice; + sdaf?: ConfigDevice; + sdag?: ConfigDevice; + sdah?: ConfigDevice; + sdai?: ConfigDevice; + sdaj?: ConfigDevice; + sdak?: ConfigDevice; + sdal?: ConfigDevice; + sdam?: ConfigDevice; + sdan?: ConfigDevice; + sdao?: ConfigDevice; + sdap?: ConfigDevice; + sdaq?: ConfigDevice; + sdar?: ConfigDevice; + sdas?: ConfigDevice; + sdat?: ConfigDevice; + sdau?: ConfigDevice; + sdav?: ConfigDevice; + sdaw?: ConfigDevice; + sdax?: ConfigDevice; + sday?: ConfigDevice; + sdaz?: ConfigDevice; + sdb?: ConfigDevice; + sdba?: ConfigDevice; + sdbb?: ConfigDevice; + sdbc?: ConfigDevice; + sdbd?: ConfigDevice; + sdbe?: ConfigDevice; + sdbf?: ConfigDevice; + sdbg?: ConfigDevice; + sdbh?: ConfigDevice; + sdbi?: ConfigDevice; + sdbj?: ConfigDevice; + sdbk?: ConfigDevice; + sdbl?: ConfigDevice; + sdc?: ConfigDevice; + sdd?: ConfigDevice; + sde?: ConfigDevice; + sdf?: ConfigDevice; + sdg?: ConfigDevice; + sdh?: ConfigDevice; + sdi?: ConfigDevice; + sdj?: ConfigDevice; + sdk?: ConfigDevice; + sdl?: ConfigDevice; + sdm?: ConfigDevice; + sdn?: ConfigDevice; + sdo?: ConfigDevice; + sdp?: ConfigDevice; + sdq?: ConfigDevice; + sdr?: ConfigDevice; + sds?: ConfigDevice; + sdt?: ConfigDevice; + sdu?: ConfigDevice; + sdv?: ConfigDevice; + sdw?: ConfigDevice; + sdx?: ConfigDevice; + sdy?: ConfigDevice; + sdz?: ConfigDevice; } export type KernelArchitecture = 'i386' | 'x86_64'; @@ -678,10 +736,7 @@ export interface MigrateLinodeRequest { region: string; } -export type RescueRequestObject = Pick< - Devices, - 'sda' | 'sdb' | 'sdc' | 'sdd' | 'sde' | 'sdf' | 'sdg' ->; +export type RescueRequestObject = Omit; export interface LinodeCloneData { backups_enabled?: boolean | null; diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index e647ca67c2e..aff53521328 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,6 +4,83 @@ 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/). +## [2025-09-09] - v1.150.0 + +### Added: + +- Type-to-confirm to Image deletion dialog ([#12740](https://github.com/linode/manager/pull/12740)) +- Volumes IAM RBAC permissions ([#12744](https://github.com/linode/manager/pull/12744)) +- Notification banner for Account Settings if user doesn't have permission to see ([#12774](https://github.com/linode/manager/pull/12774)) +- Linked Node Pool firewall in Node Pool footer for LKE-E clusters ([#12779](https://github.com/linode/manager/pull/12779)) +- IAM RBAC: Implement IAM RBAC permissions for NodeBalancer ([#12780](https://github.com/linode/manager/pull/12780)) +- IAM RBAC: Implement IAM RBAC permissions for NodeBalancer summary tab ([#12790](https://github.com/linode/manager/pull/12790)) + +### Changed: + +- IAM RBAC: change docs links to be relevant to content on page ([#12743](https://github.com/linode/manager/pull/12743)) +- Support IAM/RBAC permission segmentation for BETA/LA features ([#12764](https://github.com/linode/manager/pull/12764)) +- Aggregation Function labels from Average,Minimum,Maximum to Avg,Min,Max in ACLP-Alerting service ([#12787](https://github.com/linode/manager/pull/12787)) +- Add data-pendo-id attribute to TabbedPanel for Linode Plan tab tracking ([#12806](https://github.com/linode/manager/pull/12806)) +- Update self-hosted Pendo agent script to support data-pendo-id attribute ([#12828](https://github.com/linode/manager/pull/12828)) + +### Fixed: + +- Empty paginated Access Keys pages ([#12598](https://github.com/linode/manager/pull/12598)) +- MUI Autocomplete console errors ([#12706](https://github.com/linode/manager/pull/12706)) +- IAM - Cross browser AssignedRoles entities chips truncation ([#12720](https://github.com/linode/manager/pull/12720)) +- Selected region not being reset when switching between Core and Distributed tabs ([#12767](https://github.com/linode/manager/pull/12767)) +- Jumping UI on LKE Create HA Control Plane when enabling Akamai App Platform ([#12768](https://github.com/linode/manager/pull/12768)) +- Profile preferences not retained across sessions ([#12795](https://github.com/linode/manager/pull/12795)) +- Make a Payment and Add Payment drawers not closing when browser back button is clicked ([#12796](https://github.com/linode/manager/pull/12796)) +- Node Pool footer layout ([#12798](https://github.com/linode/manager/pull/12798)) +- Redirects /settings to /account-settings ([#12841](https://github.com/linode/manager/pull/12841)) + +### Removed: + +- Unused LKE-E related code from `LinodeConfigDialog` ([#12812](https://github.com/linode/manager/pull/12812)) + +### Tech Stories: + +- Improve usePermissions hook type safety ([#12732](https://github.com/linode/manager/pull/12732)) +- MSW CRUD: Fix type error when using custom restricted profile preset and add grants preset support ([#12756](https://github.com/linode/manager/pull/12756)) +- Update `jspdf` to 3.0.2 ([#12797](https://github.com/linode/manager/pull/12797)) + +### Tests: + +- Fix Linode resize test failures ([#12727](https://github.com/linode/manager/pull/12727)) +- Add tests for Host & VM Maintenance in Linode create page ([#12734](https://github.com/linode/manager/pull/12734)) +- Fix for flaky test in alerts-listing-page.spec.ts ([#12736](https://github.com/linode/manager/pull/12736)) +- Fix LKE Create Smoke Test Flake ([#12738](https://github.com/linode/manager/pull/12738)) +- Add Cypress error handling test coverage for LKE-E Phase 2 (BYO VPC, IP Stack) ([#12755](https://github.com/linode/manager/pull/12755)) +- Fix Cypress test result summary duration accuracy ([#12765](https://github.com/linode/manager/pull/12765)) +- Add test for empty string in numeric input validation ([#12769](https://github.com/linode/manager/pull/12769)) +- Add Cypress test coverage for standard cluster creation with LKE-E phase2Mtc flag enabled ([#12770](https://github.com/linode/manager/pull/12770)) +- Add Cypress LKE-E 'phase2Mtc' feature flag smoke tests ([#12773](https://github.com/linode/manager/pull/12773)) +- Add mock IntersectionObserver in testSetup.ts and check disabled tooltip in region select component tests ([#12777](https://github.com/linode/manager/pull/12777)) +- Add tests for Host & VM Maintenance Linode details page changes ([#12786](https://github.com/linode/manager/pull/12786)) +- Fix disk deletion test following API release changes ([#12794](https://github.com/linode/manager/pull/12794)) + +### Upcoming Features: + +- Volume details page ([#12757](https://github.com/linode/manager/pull/12757)) +- DataStreams: add actions with handlers in Streams list, add Edit Stream component ([#12645](https://github.com/linode/manager/pull/12645)) +- Add Configure Node Pool Drawer to Kubernetes Cluster details page ([#12710](https://github.com/linode/manager/pull/12710)) +- CloudPulse - Alerts: Update handler in `AlertReusableComponenr.tsx`, reset state in `AlertInformationActionTable.tsx`, update query in `alerts.ts` ([#12730](https://github.com/linode/manager/pull/12730)) +- ACLP: Order of each legend row label value is based on group by sequence ([#12742](https://github.com/linode/manager/pull/12742)) +- ACLP Alert: Add `EntityScopeSelection` drop down in Alert creation and edit form([#12745](https://github.com/linode/manager/pull/12745)) +- Update dual-stack labeling in VPC Create ([#12746](https://github.com/linode/manager/pull/12746)) +- Fix and extend ACLP-supported region Linode mock examples ([#12747](https://github.com/linode/manager/pull/12747)) +- DataStreams: add actions with handlers in Destinations list, add Edit Destination component ([#12749](https://github.com/linode/manager/pull/12749)) +- Update dual-stack labeling for LKE-E clusters in create cluster flow ([#12754](https://github.com/linode/manager/pull/12754)) +- CloudPulse - Metrics: Add/Update 'no region' info message for all services in `constants.ts` ([#12759](https://github.com/linode/manager/pull/12759)) +- CloudPulse - Metrics: Add missing props and enhance utils for firewalls contextual view ([#12760](https://github.com/linode/manager/pull/12760)) +- ACLP-Metrics,Alerts: enforce validation for 100 characters for TextField components ([#12771](https://github.com/linode/manager/pull/12771)) +- Disable legacy interface selection for Linode Interfaces when creating a Linode from backups ([#12772](https://github.com/linode/manager/pull/12772)) +- UX feedback: Change /settings to /account-settings and profile/settings to profile/preferences ([#12785](https://github.com/linode/manager/pull/12785)) +- Add additional device slots to Linode Config and Rescue Dialog to match new API limits ([#12791](https://github.com/linode/manager/pull/12791)) +- Add Firewall option to the Add Node Pool Drawer for LKE Enterprise Kubernetes Clusters ([#12793](https://github.com/linode/manager/pull/12793)) +- CloudPulse-Metrics: Update CloudPulseRegionSelect.tsx to handle default linode region selection in firewalls contextual view ([#12805](https://github.com/linode/manager/pull/12805)) + ## [2025-08-28] - v1.149.1 ### Fixed: diff --git a/packages/manager/cypress/component/components/region-select.spec.tsx b/packages/manager/cypress/component/components/region-select.spec.tsx index 91d5200a39c..5ff3a7ef941 100644 --- a/packages/manager/cypress/component/components/region-select.spec.tsx +++ b/packages/manager/cypress/component/components/region-select.spec.tsx @@ -1,3 +1,4 @@ +import { Typography } from '@linode/ui'; import { accountAvailabilityFactory, regionFactory } from '@linode/utilities'; import * as React from 'react'; import { mockGetAccountAvailability } from 'support/intercepts/account'; @@ -29,7 +30,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={() => {}} regions={[region]} - value={undefined} + value={null} /> ); @@ -58,7 +59,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={() => {}} regions={[region]} - value={undefined} + value={null} /> ); @@ -88,7 +89,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={() => {}} regions={[region]} - value={undefined} + value={null} /> ); @@ -118,7 +119,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={() => {}} regions={[region]} - value={undefined} + value={null} /> ); @@ -152,7 +153,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={() => {}} regions={regions} - value={undefined} + value={null} /> ); @@ -271,7 +272,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={() => {}} regions={regions} - value={undefined} + value={null} /> ); @@ -289,7 +290,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={spyFn} regions={regions} - value={undefined} + value={null} /> ); @@ -359,7 +360,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={() => {}} regions={regions} - value={undefined} + value={null} />, { dcGetWell: true, @@ -394,7 +395,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={() => {}} regions={regions} - value={undefined} + value={null} /> ); @@ -424,7 +425,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={() => {}} regions={regions} - value={undefined} + value={null} /> ); @@ -441,6 +442,53 @@ componentTests('RegionSelect', (mount) => { .should('be.visible'); }); }); + + it('should display a tooltip for disabled regions', () => { + const disabledRegion = 'US, Fremont, CA (us-west)'; + const disabledReason = `You've reached the limit of placement groups you can create in this region.`; + + mount( + {disabledReason}, + tooltipWidth: 300, + }, + }} + isGeckoLAEnabled={false} + onChange={() => {}} + regions={[ + ...regions, + regionFactory.build({ + id: 'us-west', + label: 'US, Fremont, CA', + capabilities: ['Object Storage'], + }), + ]} + value={null} + /> + ); + + ui.button + .findByAttribute('title', 'Open') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.findByText(disabledRegion).as('regionItem').scrollIntoView(); + + cy.get('@regionItem').should('be.visible'); + + cy.findByText(disabledRegion) + .closest('li') + .should('have.attr', 'data-qa-disabled-item', 'true'); + + cy.findByText(disabledRegion).closest('li').click(); + cy.findByRole('tooltip') + .should('be.visible') + .and('contain.text', disabledReason); + }); }); visualTests((mount) => { @@ -455,7 +503,7 @@ componentTests('RegionSelect', (mount) => { isGeckoLAEnabled={false} onChange={() => {}} regions={regions} - value={undefined} + value={null} /> ); checkComponentA11y(); diff --git a/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts b/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts index a4ced12e972..8db26e068d3 100644 --- a/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts @@ -181,6 +181,7 @@ describe('Account cancellation', () => { mockCancelAccountError('Unauthorized', 403).as('cancelAccount'); // Navigate to Account Settings page, click "Close Account" button. + cy.visitWithLogin('/account/settings'); cy.wait(['@getAccount', '@getProfile', '@getGrants']); 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 6e7a65ed47a..09712fbff0f 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 @@ -187,7 +187,7 @@ describe('restricted user details pages', () => { .and('be.disabled') .trigger('mouseover'); ui.tooltip.findByText( - 'You must be an unrestricted User in order to add or modify tags on Linodes.' + 'You must be an unrestricted User in order to add or modify tags on a Linode.' ); }); 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 485dd160cb7..7c1a0643c1f 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 @@ -260,24 +260,27 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { }); it('should validate UI elements and alert details', () => { - // Validate navigation links and buttons - cy.findByText('Alerts').should('be.visible'); + // filter to main content area to avoid confusion w/ 'Alerts' nav link in left sidebar + cy.get('main').within(() => { + // Validate breadcrumb and buttons + cy.findByText('Alerts', { exact: false }).should('be.visible'); - cy.findByText('Definitions') - .should('be.visible') - .and('have.attr', 'href', alertDefinitionsUrl); - ui.buttonGroup.findButtonByTitle('Create Alert').should('be.visible'); + cy.findByText('Definitions') + .should('be.visible') + .and('have.attr', 'href', alertDefinitionsUrl); + ui.buttonGroup.findButtonByTitle('Create Alert').should('be.visible'); - // Validate table headers - cy.get('[data-qa="alert-table"]').within(() => { - expectedHeaders.forEach((header) => { - cy.findByText(header).should('have.text', header); + // Validate table headers + cy.get('[data-qa="alert-table"]').within(() => { + expectedHeaders.forEach((header) => { + cy.findByText(header).should('have.text', header); + }); }); - }); - // Validate alert details - mockAlerts.forEach((alert) => { - validateAlertDetails(alert); + // Validate alert details + mockAlerts.forEach((alert) => { + validateAlertDetails(alert); + }); }); }); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts index 82b1f5fd4a4..f5cfa16fe10 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts @@ -120,7 +120,7 @@ const mockAlerts = alertFactory.build({ * Fills metric details in the form. * @param ruleIndex - The index of the rule to fill. * @param dataField - The metric's data field (e.g., "CPU Utilization"). - * @param aggregationType - The aggregation type (e.g., "Average"). + * @param aggregationType - The aggregation type (e.g., "Avg"). * @param operator - The operator (e.g., ">=", "=="). * @param threshold - The threshold value for the metric. */ @@ -242,7 +242,7 @@ describe('Create Alert', () => { // Fill metric details for the first rule const cpuUsageMetricDetails = { - aggregationType: 'Average', + aggregationType: 'Avg', dataField: 'CPU Utilization', operator: '=', ruleIndex: 0, @@ -288,7 +288,7 @@ describe('Create Alert', () => { // Fill metric details for the second rule const memoryUsageMetricDetails = { - aggregationType: 'Average', + aggregationType: 'Avg', dataField: 'Memory Usage', operator: '=', ruleIndex: 1, diff --git a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts index 8de25cf4d55..3522398093c 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts @@ -145,6 +145,7 @@ const getWidgetLegendRowValuesFromResponse = ( status: 'success', unit, serviceType, + groupBy: ['entity_id'], }); // Destructure metrics data from the first legend row diff --git a/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts index 3ab96ff6578..c50f8955077 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts @@ -205,7 +205,7 @@ describe('Integration Tests for Edit Alert', () => { // Assert rule values 1 assertRuleValues(0, { - aggregationType: 'Average', + aggregationType: 'Avg', dataField: 'CPU Utilization', operator: '=', threshold: '1000', @@ -213,7 +213,7 @@ describe('Integration Tests for Edit Alert', () => { // Assert rule values 2 assertRuleValues(1, { - aggregationType: 'Average', + aggregationType: 'Avg', dataField: 'Memory Usage', operator: '=', threshold: '1000', @@ -295,8 +295,8 @@ describe('Integration Tests for Edit Alert', () => { cy.get('[data-testid="rule_criteria.rules.0-id"]').within(() => { ui.autocomplete.findByLabel('Data Field').type('Disk I/O'); ui.autocompletePopper.findByTitle('Disk I/O').click(); - ui.autocomplete.findByLabel('Aggregation Type').type('Minimum'); - ui.autocompletePopper.findByTitle('Minimum').click(); + ui.autocomplete.findByLabel('Aggregation Type').type('Min'); + ui.autocompletePopper.findByTitle('Min').click(); ui.autocomplete.findByLabel('Operator').type('>'); ui.autocompletePopper.findByTitle('>').click(); cy.get('[data-qa-threshold]').should('be.visible').clear(); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts index 9c30ccef63e..becda1f1629 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts @@ -130,6 +130,7 @@ const getWidgetLegendRowValuesFromResponse = ( status: 'success', unit, serviceType, + groupBy: ['entity_id'], }); // Destructure metrics data from the first legend row diff --git a/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts index 77d1c026048..e56b912b86f 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/nodebalancer-widget-verification.spec.ts @@ -123,6 +123,7 @@ const getWidgetLegendRowValuesFromResponse = ( status: 'success', unit, serviceType, + groupBy: ['entity_id'], }); // Destructure metrics data from the first legend row diff --git a/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts b/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts index f1dac1ca995..e36d10bb960 100644 --- a/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts +++ b/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts @@ -16,6 +16,8 @@ import { apiMatcher } from 'support/util/intercepts'; import { randomLabel, randomPhrase } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; +import { DISALLOWED_IMAGE_REGIONS } from 'src/constants'; + import type { EventStatus } from '@linode/api-v4'; import type { RecPartial } from 'factory.ts'; @@ -121,7 +123,7 @@ const uploadImage = (label: string) => { // See also BAC-862. const region = chooseRegion({ capabilities: ['Object Storage'], - exclude: ['au-mel', 'gb-lon', 'sg-sin-2'], + exclude: DISALLOWED_IMAGE_REGIONS, }); const upload = 'machine-images/test-image.gz'; cy.visitWithLogin('/images/create/upload'); @@ -238,8 +240,13 @@ describe('machine image', () => { .findByTitle(`Delete Image ${updatedLabel}`) .should('be.visible') .within(() => { + cy.findByLabelText('Image Label') + .should('be.visible') + .should('be.enabled') + .type(updatedLabel); + ui.buttonGroup - .findButtonByTitle('Delete Image') + .findButtonByTitle('Delete') .should('be.visible') .should('be.enabled') .click(); diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index dca95f300a8..3ecf54685fb 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -121,12 +121,18 @@ describe('LKE Cluster Creation', () => { beforeEach(() => { // Mock feature flag -- @TODO LKE-E: Remove feature flag once LKE-E is fully rolled out mockAppendFeatureFlags({ - lkeEnterprise: { enabled: true, la: true, postLa: false }, + lkeEnterprise: { + enabled: true, + la: true, + postLa: false, + phase2Mtc: true, + }, }).as('getFeatureFlags'); }); /* * - Confirms that users can create a cluster by completing the LKE create form. + * - Confirms that no IP Stack or VPC options are visible for standard tier clusters (LKE-E only). * - Confirms that LKE cluster is created. * - Confirms that user is redirected to new LKE cluster summary page. * - Confirms that correct information is shown on the LKE cluster summary page @@ -199,6 +205,15 @@ describe('LKE Cluster Creation', () => { cy.get('[data-testid="ha-radio-button-no"]').should('be.visible').click(); + // Confirms LKE-E Phase 2 IP Stack and VPC options do not display for a standard LKE cluster. + cy.findByText('IP Stack').should('not.exist'); + cy.findByText('IPv4', { exact: true }).should('not.exist'); + cy.findByText('IPv4 + IPv6 (dual-stack)').should('not.exist'); + cy.findByText('Automatically generate a VPC for this cluster').should( + 'not.exist' + ); + cy.findByText('Use an existing VPC').should('not.exist'); + let monthPrice = 0; // Confirm the expected available plans display. @@ -269,12 +284,19 @@ describe('LKE Cluster Creation', () => { .click(); }); + // Confirm request payload does not include LKE-E-specific values. + cy.wait('@createCluster').then((intercept) => { + const payload = intercept.request.body; + expect(payload.stack_type).to.be.undefined; + expect(payload.vpc_id).to.be.undefined; + expect(payload.subnet_id).to.be.undefined; + }); + // Wait for LKE cluster to be created and confirm that we are redirected // to the cluster summary page. cy.wait([ '@getCluster', '@getClusterPools', - '@createCluster', '@getLKEClusterTypes', '@getDashboardUrl', '@getControlPlaneACL', diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-enterprise-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-enterprise-create.spec.ts index bc84016b588..78dc2c33408 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-enterprise-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-enterprise-create.spec.ts @@ -15,6 +15,7 @@ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetLinodeTypes } from 'support/intercepts/linodes'; import { mockCreateCluster, + mockCreateClusterError, mockGetKubernetesVersions, mockGetLKEClusterTypes, mockGetTieredKubernetesVersions, @@ -31,35 +32,91 @@ import { vpcFactory, } from 'src/factories'; +const clusterLabel = randomLabel(); +const selectedVpcId = 1; +const selectedSubnetId = 1; + +const mockEnterpriseCluster = kubernetesClusterFactory.build({ + k8s_version: latestEnterpriseTierKubernetesVersion.id, + label: clusterLabel, + region: 'us-iad', + tier: 'enterprise', +}); + +const mockVpcs = [ + { + ...vpcFactory.build(), + id: selectedVpcId, + label: 'test-vpc', + region: 'us-iad', + subnets: [ + subnetFactory.build({ + id: selectedSubnetId, + label: 'subnet-a', + ipv4: '10.0.0.0/13', + }), + ], + }, +]; + describe('LKE Cluster Creation with LKE-E', () => { - describe('LKE-E Phase 2 Networking Configurations', () => { - const clusterLabel = randomLabel(); - const selectedVpcId = 1; - const selectedSubnetId = 1; - - const mockEnterpriseCluster = kubernetesClusterFactory.build({ - k8s_version: latestEnterpriseTierKubernetesVersion.id, - label: clusterLabel, - region: 'us-iad', - tier: 'enterprise', - }); + beforeEach(() => { + // TODO LKE-E: Remove feature flag mocks once we're in GA + mockAppendFeatureFlags({ + lkeEnterprise: { + enabled: true, + la: true, + postLa: false, + phase2Mtc: true, + }, + }).as('getFeatureFlags'); + mockGetAccount( + accountFactory.build({ + capabilities: ['Kubernetes Enterprise'], + }) + ).as('getAccount'); - const mockVpcs = [ - { - ...vpcFactory.build(), - id: selectedVpcId, - label: 'test-vpc', - region: 'us-iad', - subnets: [ - subnetFactory.build({ - id: selectedSubnetId, - label: 'subnet-a', - ipv4: '10.0.0.0/13', - }), + mockGetTieredKubernetesVersions('enterprise', [ + latestEnterpriseTierKubernetesVersion, + ]).as('getTieredKubernetesVersions'); + mockGetKubernetesVersions([latestKubernetesVersion]).as( + 'getKubernetesVersions' + ); + + mockGetLinodeTypes(mockedLKEClusterTypes).as('getLinodeTypes'); + mockGetLKEClusterTypes(mockedLKEEnterprisePrices).as( + 'getLKEEnterpriseClusterTypes' + ); + mockCreateCluster(mockEnterpriseCluster).as('createCluster'); + + mockGetRegions([ + regionFactory.build({ + capabilities: [ + 'Linodes', + 'Kubernetes', + 'Kubernetes Enterprise', + 'VPCs', ], - }, - ]; + id: 'us-iad', + label: 'Washington, DC', + }), + ]).as('getRegions'); + + mockGetVPCs(mockVpcs).as('getVPCs'); + cy.visitWithLogin('/kubernetes/clusters'); + cy.wait(['@getAccount']); + + ui.button.findByTitle('Create Cluster').click(); + cy.url().should('endWith', '/kubernetes/create'); + cy.wait([ + '@getKubernetesVersions', + '@getTieredKubernetesVersions', + '@getLinodeTypes', + ]); + }); + + describe('LKE-E Phase 2 Networking Configurations', () => { // Accounts for the different combination of IP Networking and VPC/Subnet radio selections const possibleNetworkingConfigurations = [ { @@ -88,62 +145,6 @@ describe('LKE Cluster Creation with LKE-E', () => { }, ]; - beforeEach(() => { - // TODO LKE-E: Remove feature flag mocks once we're in GA - mockAppendFeatureFlags({ - lkeEnterprise: { - enabled: true, - la: true, - postLa: false, - phase2Mtc: true, - }, - }).as('getFeatureFlags'); - mockGetAccount( - accountFactory.build({ - capabilities: ['Kubernetes Enterprise'], - }) - ).as('getAccount'); - - mockGetTieredKubernetesVersions('enterprise', [ - latestEnterpriseTierKubernetesVersion, - ]).as('getTieredKubernetesVersions'); - mockGetKubernetesVersions([latestKubernetesVersion]).as( - 'getKubernetesVersions' - ); - - mockGetLinodeTypes(mockedLKEClusterTypes).as('getLinodeTypes'); - mockGetLKEClusterTypes(mockedLKEEnterprisePrices).as( - 'getLKEEnterpriseClusterTypes' - ); - mockCreateCluster(mockEnterpriseCluster).as('createCluster'); - - mockGetRegions([ - regionFactory.build({ - capabilities: [ - 'Linodes', - 'Kubernetes', - 'Kubernetes Enterprise', - 'VPCs', - ], - id: 'us-iad', - label: 'Washington, DC', - }), - ]).as('getRegions'); - - mockGetVPCs(mockVpcs).as('getVPCs'); - - cy.visitWithLogin('/kubernetes/clusters'); - cy.wait(['@getAccount']); - - ui.button.findByTitle('Create Cluster').click(); - cy.url().should('endWith', '/kubernetes/create'); - cy.wait([ - '@getKubernetesVersions', - '@getTieredKubernetesVersions', - '@getLinodeTypes', - ]); - }); - possibleNetworkingConfigurations.forEach( ({ description, isUsingOwnVPC, stackType }) => { it(`${description}`, () => { @@ -158,9 +159,7 @@ describe('LKE Cluster Creation with LKE-E', () => { // Select either the autogenerated or existing (BYO) VPC radio button if (isUsingOwnVPC) { - cy.findByTestId('isUsingOwnVpc').within(() => { - cy.findByLabelText('Use an existing VPC').click(); - }); + cy.findByLabelText('Use an existing VPC').click(); // Select the existing VPC and Subnet to use ui.autocomplete.findByLabel('VPC').click(); @@ -171,7 +170,7 @@ describe('LKE Cluster Creation with LKE-E', () => { // Select either the IPv4 or IPv4 + IPv6 (dual-stack) IP Networking radio button cy.findByLabelText( - stackType === 'ipv4' ? 'IPv4' : 'IPv4 + IPv6' + stackType === 'ipv4' ? 'IPv4' : 'IPv4 + IPv6 (dual-stack)' ).click(); // Select a plan and add nodes @@ -223,4 +222,178 @@ describe('LKE Cluster Creation with LKE-E', () => { } ); }); + + describe('LKE-E Cluster Error Handling', () => { + /* + * Surfaces an API errors on the page. + */ + it('surfaces API error when creating cluster with an invalid configuration', () => { + const mockErrorMessage = + 'There was a general error when creating your cluster.'; + + mockGetVPCs(mockVpcs).as('getVPCs'); + mockCreateClusterError(mockErrorMessage).as('createClusterError'); + + cy.findByLabelText('Cluster Label').type(clusterLabel); + cy.findByText('LKE Enterprise').click(); + cy.wait(['@getLKEEnterpriseClusterTypes', '@getRegions']); + + ui.regionSelect.find().clear().type('Washington, DC{enter}'); + cy.wait('@getVPCs'); + + cy.findByLabelText( + 'Automatically generate a VPC for this cluster' + ).click(); + cy.findByLabelText('IPv4 + IPv6 (dual-stack)').click(); + + // Bypass ACL validation + cy.get('input[name="acl-acknowledgement"]').check(); + + // Select a plan and add nodes + cy.findByText(clusterPlans[0].tab).should('be.visible').click(); + cy.findByText(clusterPlans[0].planName) + .should('be.visible') + .closest('tr') + .within(() => { + cy.get('[name="Quantity"]').should('be.visible').click(); + cy.focused().type(`{selectall}${clusterPlans[0].nodeCount}`); + + ui.button + .findByTitle('Add') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.get('[data-testid="kube-checkout-bar"]').within(() => { + ui.button.findByTitle('Create Cluster').click(); + }); + + cy.wait('@createClusterError'); + cy.findByText(mockErrorMessage).should('be.visible'); + }); + + /** + * Surfaces field-level errors on the page. + */ + it('surfaces field-level errors on VPC fields', () => { + // Intercept the create cluster request and force an error response + cy.intercept('POST', '/v4beta/lke/clusters', { + statusCode: 400, + body: { + errors: [ + { + reason: 'There is an error configuring this VPC.', + field: 'vpc_id', + }, + { + reason: 'There is an error configuring this subnet.', + field: 'subnet_id', + }, + ], + }, + }).as('createClusterError'); + + cy.findByLabelText('Cluster Label').type(clusterLabel); + cy.findByText('LKE Enterprise').click(); + + // Select region, VPC, subnet, and IP stack + ui.regionSelect.find().clear().type('Washington, DC{enter}'); + cy.findByLabelText('Use an existing VPC').click(); + ui.autocomplete.findByLabel('VPC').click(); + cy.findByText('test-vpc').click(); + ui.autocomplete.findByLabel('Subnet').click(); + cy.findByText(/subnet-a/).click(); + cy.findByLabelText('IPv4 + IPv6 (dual-stack)').click(); + + // Bypass ACL validation + cy.get('input[name="acl-acknowledgement"]').check(); + + // Select a plan and add nodes + cy.findByText(clusterPlans[0].tab).should('be.visible').click(); + cy.findByText(clusterPlans[0].planName) + .should('be.visible') + .closest('tr') + .within(() => { + cy.get('[name="Quantity"]').should('be.visible').click(); + cy.focused().type(`{selectall}${clusterPlans[0].nodeCount}`); + + ui.button + .findByTitle('Add') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Try to submit the form + cy.get('[data-testid="kube-checkout-bar"]').within(() => { + ui.button.findByTitle('Create Cluster').click(); + }); + + // Confirm error messages display + cy.wait('@createClusterError'); + cy.findByText('There is an error configuring this VPC.').should( + 'be.visible' + ); + cy.findByText('There is an error configuring this subnet.').should( + 'be.visible' + ); + }); + + /* + * Surfaces client-side validation error for VPC selection. + */ + it('surfaces a client-side validation error when BYO VPC is selected but no VPC is chosen', () => { + mockGetVPCs(mockVpcs).as('getVPCs'); + const errorText = + 'You must either select a VPC or select automatic VPC generation.'; + + cy.findByLabelText('Cluster Label').type(clusterLabel); + cy.findByText('LKE Enterprise').click(); + cy.wait(['@getLKEEnterpriseClusterTypes', '@getRegions']); + + ui.regionSelect.find().clear().type('Washington, DC{enter}'); + cy.wait('@getVPCs'); + + // Bypass ACL validation + cy.get('input[name="acl-acknowledgement"]').check(); + + // Select a plan and add nodes + cy.findByText(clusterPlans[0].tab).should('be.visible').click(); + cy.findByText(clusterPlans[0].planName) + .should('be.visible') + .closest('tr') + .within(() => { + cy.get('[name="Quantity"]').should('be.visible').click(); + cy.focused().type(`{selectall}${clusterPlans[0].nodeCount}`); + + ui.button + .findByTitle('Add') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Select the 'bring your own' VPC option + cy.findByLabelText('Use an existing VPC').click(); + + // Try to create the cluster without actually selecting a VPC + cy.get('[data-testid="kube-checkout-bar"]').within(() => { + ui.button.findByTitle('Create Cluster').click(); + }); + + // Confirm error surfaces on the VPC field + cy.findByText(errorText).should('be.visible'); + + // Confirm switching to an autogenerated VPC clears the error + cy.findByLabelText( + 'Automatically generate a VPC for this cluster' + ).click(); + cy.findByText(errorText).should('not.exist'); + + // Confirm the error stays cleared when switching back to the existing VPC option + cy.findByLabelText('Use an existing VPC').click(); + cy.findByText(errorText).should('not.exist'); + }); + }); }); diff --git a/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-enterprise.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-enterprise.spec.ts new file mode 100644 index 00000000000..01cc1e7ab25 --- /dev/null +++ b/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-enterprise.spec.ts @@ -0,0 +1,279 @@ +/** + * Tests basic functionality for LKE-E feature-flagged work. + * TODO: M3-10365 - Add `postLa` smoke tests to this file. + * TODO: M3-8838 - Delete this spec file once LKE-E is released to GA. + */ + +import { regionFactory } from '@linode/utilities'; +import { + accountFactory, + kubernetesClusterFactory, + nodePoolFactory, + subnetFactory, + vpcFactory, +} from '@src/factories'; +import { + latestEnterpriseTierKubernetesVersion, + minimumNodeNotice, +} from 'support/constants/lke'; +import { mockGetAccount } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockCreateCluster, + mockGetCluster, + mockGetClusterPools, + mockGetTieredKubernetesVersions, +} from 'support/intercepts/lke'; +import { mockGetClusters } from 'support/intercepts/lke'; +import {} from 'support/intercepts/profile'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { mockGetVPC } from 'support/intercepts/vpc'; +import { ui } from 'support/ui'; +import { addNodes } from 'support/util/lke'; +import { randomLabel } from 'support/util/random'; + +const mockCluster = kubernetesClusterFactory.build({ + id: 1, + vpc_id: 123, + label: randomLabel(), + tier: 'enterprise', +}); + +const mockVPC = vpcFactory.build({ + id: 123, + label: 'lke-e-vpc', + subnets: [subnetFactory.build()], +}); + +const mockNodePools = [nodePoolFactory.build()]; + +// Mock a valid region for LKE-E to avoid test flake. +const mockRegions = [ + regionFactory.build({ + capabilities: ['Linodes', 'Kubernetes', 'Kubernetes Enterprise', 'VPCs'], + id: 'us-iad', + label: 'Washington, DC', + }), +]; + +/** + * - Confirms VPC and IP Stack selections are shown with `phase2Mtc` feature flag is enabled. + * - Confirms VPC and IP Stack selections are not shown in create flow with `phase2Mtc` feature flag is disabled. + */ +describe('LKE-E Cluster Create', () => { + beforeEach(() => { + mockGetAccount( + accountFactory.build({ + capabilities: ['Kubernetes Enterprise'], + }) + ).as('getAccount'); + }); + + it('Simple Page Check - Phase 2 MTC Flag ON', () => { + mockAppendFeatureFlags({ + lkeEnterprise: { + enabled: true, + la: true, + postLa: false, + phase2Mtc: true, + }, + }).as('getFeatureFlags'); + + mockCreateCluster(mockCluster).as('createCluster'); + mockGetTieredKubernetesVersions('enterprise', [ + latestEnterpriseTierKubernetesVersion, + ]).as('getTieredKubernetesVersions'); + mockGetRegions(mockRegions); + + cy.visitWithLogin('/kubernetes/create'); + cy.findByText('Add Node Pools').should('be.visible'); + + cy.findByLabelText('Cluster Label').click(); + cy.focused().type(mockCluster.label); + + cy.findByText('LKE Enterprise').click(); + + ui.regionSelect.find().click().type(`${mockRegions[0].label}`); + ui.regionSelect.findItemByRegionId(mockRegions[0].id).click(); + + cy.findByLabelText('Kubernetes Version').should('be.visible').click(); + cy.findByText(latestEnterpriseTierKubernetesVersion.id) + .should('be.visible') + .click(); + + // Confirms LKE-E Phase 2 IP Stack and VPC options display with the flag ON. + cy.findByText('IP Stack').should('be.visible'); + cy.findByText('IPv4', { exact: true }).should('be.visible'); + cy.findByText('IPv4 + IPv6 (dual-stack)').should('be.visible'); + cy.findByText('Automatically generate a VPC for this cluster').should( + 'be.visible' + ); + cy.findByText('Use an existing VPC').should('be.visible'); + + cy.findByText('Shared CPU').should('be.visible').click(); + addNodes('Linode 2 GB'); + + // Bypass ACL validation + cy.get('input[name="acl-acknowledgement"]').check(); + + // Confirm change is reflected in checkout bar. + cy.get('[data-testid="kube-checkout-bar"]').within(() => { + cy.findByText('Linode 2 GB Plan').should('be.visible'); + cy.findByTitle('Remove Linode 2GB Node Pool').should('be.visible'); + + cy.get('[data-qa-notice="true"]').within(() => { + cy.findByText(minimumNodeNotice).should('be.visible'); + }); + + ui.button + .findByTitle('Create Cluster') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@createCluster'); + cy.url().should( + 'endWith', + `/kubernetes/clusters/${mockCluster.id}/summary` + ); + }); + + it('Simple Page Check - Phase 2 MTC Flag OFF', () => { + mockAppendFeatureFlags({ + lkeEnterprise: { + enabled: true, + la: true, + postLa: false, + phase2Mtc: false, + }, + }).as('getFeatureFlags'); + + mockCreateCluster(mockCluster).as('createCluster'); + mockGetTieredKubernetesVersions('enterprise', [ + latestEnterpriseTierKubernetesVersion, + ]).as('getTieredKubernetesVersions'); + mockGetRegions(mockRegions); + + cy.visitWithLogin('/kubernetes/create'); + cy.findByText('Add Node Pools').should('be.visible'); + + cy.findByLabelText('Cluster Label').click(); + cy.focused().type(mockCluster.label); + + cy.findByText('LKE Enterprise').click(); + + ui.regionSelect.find().click().type(`${mockRegions[0].label}`); + ui.regionSelect.findItemByRegionId(mockRegions[0].id).click(); + + cy.findByLabelText('Kubernetes Version').should('be.visible').click(); + cy.findByText(latestEnterpriseTierKubernetesVersion.id) + .should('be.visible') + .click(); + + // Confirms LKE-E Phase 2 IP Stack and VPC options do not display with the flag OFF. + cy.findByText('IP Stack').should('not.exist'); + cy.findByText('IPv4', { exact: true }).should('not.exist'); + cy.findByText('IPv4 + IPv6 (dual-stack)').should('not.exist'); + cy.findByText('Automatically generate a VPC for this cluster').should( + 'not.exist' + ); + cy.findByText('Use an existing VPC').should('not.exist'); + + cy.findByText('Shared CPU').should('be.visible').click(); + addNodes('Linode 2 GB'); + + // Bypass ACL validation + cy.get('input[name="acl-acknowledgement"]').check(); + + // Confirm change is reflected in checkout bar. + cy.get('[data-testid="kube-checkout-bar"]').within(() => { + cy.findByText('Linode 2 GB Plan').should('be.visible'); + cy.findByTitle('Remove Linode 2GB Node Pool').should('be.visible'); + + cy.get('[data-qa-notice="true"]').within(() => { + cy.findByText(minimumNodeNotice).should('be.visible'); + }); + + ui.button + .findByTitle('Create Cluster') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@createCluster'); + cy.url().should( + 'endWith', + `/kubernetes/clusters/${mockCluster.id}/summary` + ); + }); +}); + +/** + * - Confirms cluster shows linked VPC and Node Pool VPC IP columns when `phase2Mtc` flag is enabled. + * - Confirms cluster's linked VPC and Node Pool VPC IP columns are hidden when `phase2Mtc` flag is disabled. + */ +describe('LKE-E Cluster Read', () => { + beforeEach(() => { + mockGetAccount( + accountFactory.build({ + capabilities: ['Kubernetes Enterprise'], + }) + ).as('getAccount'); + }); + + it('Simple Page Check - Phase 2 MTC Flag ON', () => { + mockAppendFeatureFlags({ + lkeEnterprise: { enabled: true, la: true, phase2Mtc: true }, + }).as('getFeatureFlags'); + + mockGetClusters([mockCluster]).as('getClusters'); + mockGetCluster(mockCluster).as('getCluster'); + mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools'); + mockGetVPC(mockVPC).as('getVPC'); + + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}/summary`); + cy.wait(['@getCluster', '@getVPC', '@getNodePools']); + + // Confirm linked VPC is present + cy.get('[data-qa-kube-entity-footer]').within(() => { + cy.contains('VPC:').should('exist'); + cy.findByTestId('assigned-lke-cluster-label').should( + 'contain.text', + mockVPC.label + ); + }); + + // Confirm VPC IP columns are present in the node table header + cy.get('[aria-label="List of Your Cluster Nodes"] thead').within(() => { + cy.contains('th', 'VPC IPv4').should('be.visible'); + cy.contains('th', 'VPC IPv6').should('be.visible'); + }); + }); + + it('Simple Page Check - Phase 2 MTC Flag OFF', () => { + mockAppendFeatureFlags({ + lkeEnterprise: { enabled: true, la: true, phase2Mtc: false }, + }).as('getFeatureFlags'); + + mockGetClusters([mockCluster]).as('getClusters'); + mockGetCluster(mockCluster).as('getCluster'); + mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools'); + + cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}/summary`); + cy.wait(['@getCluster', '@getNodePools']); + + // Confirm linked VPC is not present + cy.get('[data-qa-kube-entity-footer]').within(() => { + cy.contains('VPC:').should('not.exist'); + cy.findByTestId('assigned-lke-cluster-label').should('not.exist'); + }); + + // Confirm VPC IP columns are not present in the node table header + cy.get('[aria-label="List of Your Cluster Nodes"] thead').within(() => { + cy.contains('th', 'VPC IPv4').should('not.exist'); + cy.contains('th', 'VPC IPv6').should('not.exist'); + }); + }); +}); diff --git a/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-standard-create.spec.ts similarity index 76% rename from packages/manager/cypress/e2e/core/kubernetes/smoke-lke-create.spec.ts rename to packages/manager/cypress/e2e/core/kubernetes/smoke-lke-standard-create.spec.ts index ad0d420eeaa..2dc7c4c1cd3 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-standard-create.spec.ts @@ -1,5 +1,10 @@ +/** + * Tests basic functionality for standard LKE creation. + */ + import { grantsFactory, profileFactory } from '@linode/utilities'; import { accountUserFactory, kubernetesClusterFactory } from '@src/factories'; +import { minimumNodeNotice } from 'support/constants/lke'; import { mockGetUser } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateCluster } from 'support/intercepts/lke'; @@ -8,56 +13,10 @@ import { mockGetProfileGrants, } from 'support/intercepts/profile'; import { ui } from 'support/ui'; +import { addNodes } from 'support/util/lke'; import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -/** - * Performs a click operation on Cypress subject a given number of times. - * - * @param subject - Cypress subject to click. - * @param count - Number of times to perform click. - * - * @returns Cypress chainable. - */ -const multipleClick = ( - subject: Cypress.Chainable, - count: number -): Cypress.Chainable => { - if (count == 1) { - return subject.click(); - } - return multipleClick(subject.click(), count - 1); -}; - -/** - * Adds a random-sized node pool of the given plan. - * - * @param plan Name of plan for which to add nodes. - */ -const addNodes = (plan: string) => { - const defaultNodes = 3; - const extraNodes = randomNumber(1, 5); - - cy.get(`[data-qa-plan-row="${plan}"`).within(() => { - multipleClick(cy.get('[data-testid="increment-button"]'), extraNodes); - multipleClick(cy.get('[data-testid="decrement-button"]'), extraNodes + 1); - - cy.get('[data-testid="textfield-input"]') - .invoke('val') - .should('eq', `${defaultNodes - 1}`); - - ui.button - .findByTitle('Add') - .should('be.visible') - .should('be.enabled') - .click(); - }); -}; - -// Warning that's shown when recommended minimum number of nodes is not met. -const minimumNodeNotice = - 'We recommend a minimum of 3 nodes in each Node Pool to avoid downtime during upgrades and maintenance.'; - describe('LKE Create Cluster', () => { beforeEach(() => { // Mock feature flag -- @TODO LKE-E: Remove feature flag once LKE-E is fully rolled out @@ -78,7 +37,12 @@ describe('LKE Create Cluster', () => { cy.findByLabelText('Cluster Label').click(); cy.focused().type(mockCluster.label); - ui.regionSelect.find().click().type(`${chooseRegion().label}{enter}`); + const lkeRegion = chooseRegion({ + capabilities: ['Kubernetes'], + }); + + ui.regionSelect.find().click().type(`${lkeRegion.label}`); + ui.regionSelect.findItemByRegionId(lkeRegion.id).click(); cy.findByLabelText('Kubernetes Version').should('be.visible').click(); cy.findByText('1.32').should('be.visible').click(); diff --git a/packages/manager/cypress/e2e/core/linodes/alerts-edit.spec.ts b/packages/manager/cypress/e2e/core/linodes/alerts-edit.spec.ts index 0bbe2c41235..d2b7e3267a4 100644 --- a/packages/manager/cypress/e2e/core/linodes/alerts-edit.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/alerts-edit.spec.ts @@ -2,8 +2,8 @@ import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockGetAlertDefinition } from 'support/intercepts/cloudpulse'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { + interceptUpdateLinode, mockGetLinodeDetails, - mockUpdateLinode, } from 'support/intercepts/linodes'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; @@ -119,7 +119,7 @@ describe('region enables alerts', function () { ); }); - it('Legacy alerts = 0, Beta alerts = [] => legacy disabled', function () { + xit('Legacy alerts = 0, Beta alerts = [] => legacy disabled', function () { const mockLinode = linodeFactory.build({ id: MOCK_LINODE_ID, label: randomLabel(), @@ -180,7 +180,7 @@ describe('region enables alerts', function () { }); }); - it('Legacy alerts > 0, Beta alerts = [] => legacy enabled. can upgrade to beta enabled', function () { + xit('Legacy alerts > 0, Beta alerts = [] => legacy enabled. can upgrade to beta enabled', function () { const mockLinode = linodeFactory.build({ id: MOCK_LINODE_ID, label: randomLabel(), @@ -234,7 +234,7 @@ describe('region enables alerts', function () { // toggles in table are on but can be turned off assertLinodeAlertsEnabled(this.alertDefinitions); - mockUpdateLinode(mockLinode.id).as('updateLinode'); + interceptUpdateLinode(mockLinode.id).as('updateLinode'); ui.button .findByTitle('Save') .should('be.visible') @@ -243,7 +243,7 @@ describe('region enables alerts', function () { }); }); - it('Legacy alerts = 0, Beta alerts > 0, => beta enabled', function () { + xit('Legacy alerts = 0, Beta alerts > 0, => beta enabled', function () { const mockLinode = linodeFactory.build({ id: 2, label: randomLabel(), @@ -310,7 +310,7 @@ describe('region enables alerts', function () { }); }); - it('Legacy alerts > 0, Beta alerts > 0, => beta enabled. can downgrade to legacy enabled', function () { + xit('Legacy alerts > 0, Beta alerts > 0, => beta enabled. can downgrade to legacy enabled', function () { const mockLinode = linodeFactory.build({ id: MOCK_LINODE_ID, label: randomLabel(), @@ -366,7 +366,7 @@ describe('region enables alerts', function () { .click({ multiple: true }); ui.toggle.find().should('have.attr', 'data-qa-toggle', 'false'); - mockUpdateLinode(mockLinode.id).as('updateLinode'); + interceptUpdateLinode(mockLinode.id).as('updateLinode'); cy.scrollTo('bottom'); // save changes ui.button @@ -383,7 +383,7 @@ describe('region enables alerts', function () { }); }); - it('in default beta mode, edits to beta alerts do not trigger confirmation modal', function () { + xit('in default beta mode, edits to beta alerts do not trigger confirmation modal', function () { const mockLinode = linodeFactory.build({ id: 2, label: randomLabel(), @@ -411,7 +411,7 @@ describe('region enables alerts', function () { // toggles in table are on but can be turned off assertLinodeAlertsEnabled(this.alertDefinitions); - mockUpdateLinode(mockLinode.id).as('updateLinode'); + interceptUpdateLinode(mockLinode.id).as('updateLinode'); ui.button .findByTitle('Save') .should('be.visible') @@ -422,7 +422,7 @@ describe('region enables alerts', function () { ui.dialog.find().should('not.exist'); }); - it('in default legacy mode, edits to beta alerts trigger confirmation modal ', function () { + xit('in default legacy mode, edits to beta alerts trigger confirmation modal ', function () { const mockLinode = linodeFactory.build({ id: 2, label: randomLabel(), @@ -492,7 +492,7 @@ describe('region enables alerts', function () { }); }); - mockUpdateLinode(mockLinode.id).as('updateLinode'); + interceptUpdateLinode(mockLinode.id).as('updateLinode'); ui.button .findByTitle('Save') .should('be.visible') @@ -536,7 +536,7 @@ describe('region disables alerts. beta alerts not available regardless of linode mockGetRegions([mockDisabledRegion]).as('getRegions'); }); - it('Legacy alerts = 0, Beta alerts > 0, => legacy disabled', function () { + xit('Legacy alerts = 0, Beta alerts > 0, => legacy disabled', function () { const mockLinode = linodeFactory.build({ id: MOCK_LINODE_ID, label: randomLabel(), @@ -572,7 +572,7 @@ describe('region disables alerts. beta alerts not available regardless of linode }); }); - it('Legacy alerts > 0, Beta alerts = 0, => legacy enabled', function () { + xit('Legacy alerts > 0, Beta alerts = 0, => legacy enabled', function () { const mockLinode = linodeFactory.build({ id: MOCK_LINODE_ID, label: randomLabel(), @@ -604,4 +604,57 @@ describe('region disables alerts. beta alerts not available regardless of linode }); }); }); + + it('Deleting entire value in numeric input triggers validation error', function () { + const mockLinode = linodeFactory.build({ + id: MOCK_LINODE_ID, + label: randomLabel(), + region: this.mockDisabledRegion.id, + alerts: { ...mockEnabledLegacyAlerts }, + }); + mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); + cy.visitWithLogin(`/linodes/${mockLinode.id}/alerts`); + cy.wait(['@getFeatureFlags', '@getRegions', '@getLinode']); + ui.tabList.findTabByTitle('Alerts').within(() => { + cy.get('[data-testid="betaChip"]').should('not.exist'); + }); + cy.get('[data-reach-tab-panels]') + .should('be.visible') + .within(() => { + const strNumericInputSelector = 'input[data-testid="textfield-input"]'; + // each data-qa-alerts-panel contains a toggle button and a numeric input + cy.get('[data-qa-alerts-panel="true"]').each((panel) => { + cy.wrap(panel).within(() => { + // toggle button is enabled + ui.toggle + .find() + .should('have.attr', 'data-qa-toggle', 'true') + .should('be.visible') + .should('be.enabled'); + cy.get('label[data-qa-alert]') + .invoke('attr', 'data-qa-alert') + .then((lbl) => { + cy.get(strNumericInputSelector).clear(); + cy.get(strNumericInputSelector).blur(); + // error appears in numeric input + cy.get('p[data-qa-textfield-error-text]') + .should('be.visible') + .then(($err) => { + // use the toggle button's label to get the full error msg + expect($err).to.contain(`${lbl} is required.`); + }); + // toggle button is not disabled by the error + ui.toggle + .find() + .should('have.attr', 'data-qa-toggle', 'true') + .should('be.enabled'); + cy.get(strNumericInputSelector).click(); + cy.get(strNumericInputSelector).type('1'); + // error is removed + cy.get('p[data-qa-textfield-error-text]').should('not.exist'); + }); + }); + }); + }); + }); }); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-vm-host-maintenance.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-vm-host-maintenance.spec.ts new file mode 100644 index 00000000000..7f0e8989401 --- /dev/null +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-vm-host-maintenance.spec.ts @@ -0,0 +1,144 @@ +import { linodeFactory, regionFactory } from '@linode/utilities'; +import { mockGetAccountSettings } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockCreateLinode } from 'support/intercepts/linodes'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { ui } from 'support/ui'; +import { linodeCreatePage } from 'support/ui/pages'; +import { randomLabel, randomString } from 'support/util/random'; + +import { accountSettingsFactory } from 'src/factories'; +const mockEnabledRegion = regionFactory.build({ + capabilities: ['Linodes', 'Maintenance Policy'], +}); +const mockDisabledRegion = regionFactory.build({ + capabilities: ['Linodes'], +}); + +describe('vmHostMaintenance feature flag', () => { + beforeEach(() => { + mockGetAccountSettings( + accountSettingsFactory.build({ + maintenance_policy: 'linode/power_off_on', + }) + ).as('getAccountSettings'); + mockGetRegions([mockEnabledRegion, mockDisabledRegion]).as('getRegions'); + }); + + it('Create flow when vmHostMaintenance feature flag is enabled', () => { + mockAppendFeatureFlags({ + vmHostMaintenance: { + enabled: true, + }, + }).as('getFeatureFlags'); + const mockLinode = linodeFactory.build({ + label: randomLabel(), + region: mockEnabledRegion.id, + }); + mockCreateLinode(mockLinode).as('createLinode'); + + cy.visitWithLogin('/linodes/create'); + cy.wait(['@getAccountSettings', '@getFeatureFlags', '@getRegions']); + + // "Host Maintenance Policy" section is present under the "Additional Options" + cy.contains('Additional Options').should('be.visible'); + cy.get('[data-qa-panel="Host Maintenance Policy"]') + .should('be.visible') + .within(() => { + cy.get('[data-qa-panel-summary="Host Maintenance Policy"]').click(); + }); + ui.autocomplete + .findByLabel('Maintenance Policy') + .should('be.visible') + .should('be.disabled'); + cy.findByText('Select a region to choose a maintenance policy.').should( + 'be.visible' + ); + // user selects region that does not have the "Maintenance Policy" capability + ui.regionSelect.find().click(); + ui.regionSelect.find().type(`${mockDisabledRegion.label}{enter}`); + ui.autocomplete + .findByLabel('Maintenance Policy') + .should('be.visible') + .should('be.disabled'); + cy.findByText( + 'Maintenance policy is not available in the selected region.' + ).should('be.visible'); + + // user selects region that does have the "Maintenance Policy" capability + ui.regionSelect.find().click(); + ui.regionSelect.find().clear(); + ui.regionSelect.find().type(`${mockEnabledRegion.label}{enter}`); + ui.autocomplete + .findByLabel('Maintenance Policy') + .should('be.visible') + .should('be.enabled'); + + // form prerequisites + cy.get('[type="password"]').should('be.visible').scrollIntoView(); + cy.get('[id="root-password"]').type(randomString(12)); + const mockPlan = { + planType: 'Shared CPU', + planLabel: 'Nanode 1 GB', + }; + linodeCreatePage.selectPlan(mockPlan.planType, mockPlan.planLabel); + cy.scrollTo('bottom'); + ui.button + .findByTitle('View Code Snippets') + .should('be.visible') + .should('be.enabled') + .click(); + + // maintenance policy is included in the code snippets + ui.dialog + .findByTitle('Create Linode') + .should('be.visible') + .within(() => { + cy.get('pre code') + .should('be.visible') + .within(() => { + cy.contains('--maintenance_policy linode/migrate'); + }); + // cURL tab + ui.tabList.findTabByTitle('cURL').should('be.visible').click(); + cy.get('pre code') + .should('be.visible') + .within(() => { + cy.contains('"maintenance_policy": "linode/migrate"'); + }); + ui.button + .findByTitle('Close') + .should('be.visible') + .should('be.enabled') + .click(); + }); + // submit + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + // POST payload should include maintenance_policy + cy.wait('@createLinode').then((intercept) => { + expect(intercept.request.body['maintenance_policy']).to.eq( + 'linode/migrate' + ); + }); + }); + + it('Create flow when vmHostMaintenance feature flag is disabled', () => { + mockAppendFeatureFlags({ + vmHostMaintenance: { + enabled: false, + }, + }).as('getFeatureFlags'); + cy.visitWithLogin('/linodes/create'); + cy.wait(['@getAccountSettings', '@getFeatureFlags', '@getRegions']); + + ui.regionSelect.find().click(); + ui.regionSelect.find().type(`${mockEnabledRegion.label}{enter}`); + + // "Host Maintenance Policy" section is not present + cy.get('[data-qa-panel="Host Maintenance Policy"]').should('not.exist'); + }); +}); diff --git a/packages/manager/cypress/e2e/core/linodes/linode-settings-vm-host-maintenance.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-settings-vm-host-maintenance.spec.ts new file mode 100644 index 00000000000..416f1829b9f --- /dev/null +++ b/packages/manager/cypress/e2e/core/linodes/linode-settings-vm-host-maintenance.spec.ts @@ -0,0 +1,188 @@ +import { linodeFactory, regionFactory } from '@linode/utilities'; +import { mockGetAccountSettings } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockGetLinodeDetails, + mockGetLinodeDisks, + mockUpdateLinode, + mockUpdateLinodeError, +} from 'support/intercepts/linodes'; +import { mockGetMaintenancePolicies } from 'support/intercepts/maintenance'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { ui } from 'support/ui'; +import { randomLabel, randomNumber } from 'support/util/random'; + +import { accountSettingsFactory } from 'src/factories'; +import { maintenancePolicyFactory } from 'src/factories/maintenancePolicy'; + +import type { Disk } from '@linode/api-v4'; + +const mockEnabledRegion = regionFactory.build({ + capabilities: ['Linodes', 'Maintenance Policy'], +}); +const mockDisabledRegion = regionFactory.build({ + capabilities: ['Linodes'], +}); +const mockMaintenancePolicyMigrate = maintenancePolicyFactory.build({ + slug: 'linode/migrate', + label: 'Migrate', + type: 'linode_migrate', +}); +const mockMaintenancePolicyPowerOnOff = maintenancePolicyFactory.build({ + slug: 'linode/power_off_on', + label: 'Power Off / Power On', + type: 'linode_power_off_on', +}); +const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: mockEnabledRegion.id, +}); + +describe('vmHostMaintenance feature flag', () => { + beforeEach(() => { + mockGetAccountSettings( + accountSettingsFactory.build({ + maintenance_policy: 'linode/power_off_on', + }) + ).as('getAccountSettings'); + mockGetRegions([mockEnabledRegion, mockDisabledRegion]).as('getRegions'); + mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); + const mockDisk: Disk = { + created: '2020-08-21T17:26:14', + filesystem: 'ext4', + id: 44311273, + label: 'Debian 10 Disk', + size: 81408, + status: 'ready', + updated: '2020-08-21T17:26:30', + }; + mockGetLinodeDisks(mockLinode.id, [mockDisk]).as('getDisks'); + mockGetMaintenancePolicies([ + mockMaintenancePolicyMigrate, + mockMaintenancePolicyPowerOnOff, + ]).as('getMaintenancePolicies'); + // cy.wrap(mockMaintenancePolicyPowerOnOff).as('mockMaintenancePolicyPowerOnOff'); + }); + + it('VM host maintenance setting is editable when vmHostMaintenance feature flag is enabled. Mocked success.', () => { + mockAppendFeatureFlags({ + vmHostMaintenance: { + enabled: true, + }, + }).as('getFeatureFlags'); + cy.visitWithLogin(`/linodes/${mockLinode.id}/settings`); + cy.wait([ + '@getAccountSettings', + '@getFeatureFlags', + '@getMaintenancePolicies', + '@getDisks', + ]); + mockUpdateLinode(mockLinode.id, { + ...mockLinode, + maintenance_policy: mockMaintenancePolicyPowerOnOff.slug, + }).as('updateLinode'); + + cy.contains('Host Maintenance Policy').should('be.visible'); + cy.contains('Maintenance Policy').should('be.visible'); + ui.autocomplete.findByLabel('Maintenance Policy').as('maintenanceInput'); + cy.get('@maintenanceInput') + .should('be.visible') + .should('have.value', mockMaintenancePolicyMigrate.label); + cy.get('@maintenanceInput') + .closest('form') + .within(() => { + // save button for the host maintenance setting is disabled before edits + ui.button.findByTitle('Save').should('be.disabled'); + // make edit + cy.get('@maintenanceInput').click(); + cy.focused().type(`${mockMaintenancePolicyPowerOnOff.label}`); + ui.autocompletePopper + .findByTitle(mockMaintenancePolicyPowerOnOff.label) + .should('be.visible') + .click(); + // save button is enabled after edit + ui.button.findByTitle('Save').should('be.enabled').click(); + }); + + // POST payload should include maintenance_policy + cy.wait('@updateLinode').then((intercept) => { + expect(intercept.request.body['maintenance_policy']).to.eq( + mockMaintenancePolicyPowerOnOff.slug + ); + }); + + // toast notification + ui.toast.assertMessage('Host Maintenance Policy settings updated.'); + }); + + it('VM host maintenance setting is editable when vmHostMaintenance feature flag is enabled. Mocked failure.', () => { + mockAppendFeatureFlags({ + vmHostMaintenance: { + enabled: true, + }, + }).as('getFeatureFlags'); + cy.visitWithLogin(`/linodes/${mockLinode.id}/settings`); + cy.wait([ + '@getAccountSettings', + '@getFeatureFlags', + '@getMaintenancePolicies', + '@getDisks', + ]); + const linodeError = { + statusCode: 400, + errorMessage: 'Linode update failed', + }; + mockUpdateLinodeError( + mockLinode.id, + linodeError.errorMessage, + linodeError.statusCode + ); + + cy.contains('Host Maintenance Policy').should('be.visible'); + cy.contains('Maintenance Policy').should('be.visible'); + ui.autocomplete.findByLabel('Maintenance Policy').as('maintenanceInput'); + cy.get('@maintenanceInput') + .should('be.visible') + .should('have.value', mockMaintenancePolicyMigrate.label); + cy.get('@maintenanceInput') + .closest('form') + .within(() => { + // save button for the host maintenance setting is disabled before edits + ui.button.findByTitle('Save').should('be.disabled'); + // make edit + cy.get('@maintenanceInput').click(); + cy.focused().type(`${mockMaintenancePolicyPowerOnOff.label}`); + ui.autocompletePopper + .findByTitle(mockMaintenancePolicyPowerOnOff.label) + .should('be.visible') + .click(); + // save button is enabled after edit + ui.button.findByTitle('Save').should('be.enabled').click(); + }); + + cy.get('[data-qa-textfield-error-text="Maintenance Policy"]') + .should('be.visible') + .should('have.text', linodeError.errorMessage); + cy.get('[aria-errormessage]').should('be.visible'); + }); + + it('Maintenance policy setting is absent when feature flag is disabled', () => { + mockAppendFeatureFlags({ + vmHostMaintenance: { + enabled: false, + }, + }).as('getFeatureFlags'); + cy.visitWithLogin(`/linodes/${mockLinode.id}/settings`); + cy.wait(['@getAccountSettings', '@getFeatureFlags', '@getDisks']); + + // "Host Maintenance Policy" section is not present + cy.get('[data-reach-tab-panels]') + .should('be.visible') + .within(() => { + cy.findByText('Host Maintenance Policy').should('not.exist'); + cy.findByText('Maintenance Policy').should('not.exist'); + cy.get('[data-qa-panel="Host Maintenance Policy"]').should('not.exist'); + }); + }); +}); diff --git a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts index 7cf8942b4af..8814b12c700 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts @@ -160,13 +160,12 @@ describe('linode storage tab', () => { }); /* - * - Confirms UI flow end-to-end when a user deletes a Linode disk. - * - Confirms that user can successfully delete a disk from a Linode. - * - Confirms that Cloud Manager UI automatically updates to reflect deleted disk. - * TODO: Disk cannot be deleted if disk_encryption is 'enabled' - * TODO: edit result of this test if/when behavior of backend is updated. uncertain what expected behavior is for this disk config + * - Confirms UI flow end-to-end when a user attempts to delete a Linode disk with encryption enabled. + * - Confirms that disk deletion fails and toast notification appears. */ - it('delete disk fails', () => { + // TODO: Disk cannot be deleted if disk_encryption is 'enabled' + // TODO: edit result of this test if/when behavior of backend is updated. uncertain what expected behavior is for this disk config + it('delete disk fails when Linode uses disk encryption', () => { const diskName = randomLabel(); cy.defer(() => createTestLinode({ @@ -214,9 +213,11 @@ describe('linode storage tab', () => { }); /* - * - Same test as above, but uses different linode config for disk_encryption + * - Confirms UI flow end-to-end when a user deletes a Linode disk. + * - Confirms that disk is deleted successfully + * - Confirms that UI updates to reflect the deleted disk. */ - it('delete disk succeeds', () => { + it('deletes a disk', () => { const diskName = randomLabel(); cy.defer(() => createTestLinode({ @@ -244,9 +245,7 @@ describe('linode storage tab', () => { deleteDisk(diskName); cy.wait('@deleteDisk').its('response.statusCode').should('eq', 200); - cy.findByText('Deleting', { exact: false }).should('be.visible'); ui.button.findByTitle('Add a Disk').should('be.enabled'); - ui.toast.assertMessage( `Disk ${diskName} on Linode ${linode.label} has been deleted.` ); diff --git a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts index 1f6c3bdfa67..195357623a1 100644 --- a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts @@ -41,9 +41,10 @@ describe('resize linode', () => { // Click "Resize Linode". // The Resize Linode button remains disabled while the Linode is provisioning, // so we have to wait for that to complete before the button becomes enabled. + // Waiting longer (7.5 mins) for Linode to boot ui.button .findByTitle('Resize Linode') - .should('be.enabled', { timeout: LINODE_CREATE_TIMEOUT }) + .should('be.enabled', { timeout: 1.5 * LINODE_CREATE_TIMEOUT }) .click(); }); @@ -81,9 +82,10 @@ describe('resize linode', () => { // Click "Resize Linode". // The Resize Linode button remains disabled while the Linode is provisioning, // so we have to wait for that to complete before the button becomes enabled. + // Waiting longer (7.5 mins) for Linode to boot ui.button .findByTitle('Resize Linode') - .should('be.enabled', { timeout: LINODE_CREATE_TIMEOUT }) + .should('be.enabled', { timeout: 1.5 * LINODE_CREATE_TIMEOUT }) .click(); }); @@ -182,9 +184,10 @@ describe('resize linode', () => { // Click "Resize Linode". // The Resize Linode button remains disabled while the Linode is provisioning, // so we have to wait for that to complete before the button becomes enabled. + // Waiting longer (7.5 mins) for Linode to boot ui.button .findByTitle('Resize Linode') - .should('be.enabled', { timeout: LINODE_CREATE_TIMEOUT }) + .should('be.enabled', { timeout: 1.5 * LINODE_CREATE_TIMEOUT }) .click(); }); 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 6be3c0eb1b8..d02b019ee9a 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 @@ -105,7 +105,7 @@ describe('linode landing checks', () => { cy.findByTestId('menu-item-Marketplace').should('be.visible'); cy.findByTestId('menu-item-Billing').scrollIntoView(); cy.findByTestId('menu-item-Billing').should('be.visible'); - cy.findByTestId('menu-item-Settings').should('be.visible'); + cy.findByTestId('menu-item-Account Settings').should('be.visible'); cy.findByTestId('menu-item-Help & Support').should('be.visible'); }); diff --git a/packages/manager/cypress/support/constants/alert.ts b/packages/manager/cypress/support/constants/alert.ts index 2628cb1a6ff..398ff892ccf 100644 --- a/packages/manager/cypress/support/constants/alert.ts +++ b/packages/manager/cypress/support/constants/alert.ts @@ -32,10 +32,10 @@ export const severityMap: Record = { }; export const aggregationTypeMap: Record = { - avg: 'Average', + avg: 'Avg', count: 'Count', - max: 'Maximum', - min: 'Minimum', + max: 'Max', + min: 'Min', sum: 'Sum', }; diff --git a/packages/manager/cypress/support/constants/lke.ts b/packages/manager/cypress/support/constants/lke.ts index e8ad5e735f6..0f36b0e1462 100644 --- a/packages/manager/cypress/support/constants/lke.ts +++ b/packages/manager/cypress/support/constants/lke.ts @@ -121,3 +121,7 @@ export const clusterPlans: LkePlanDescription[] = [ type: 'standard', }, ]; + +// Warning that's shown when recommended minimum number of nodes is not met. +export const minimumNodeNotice = + 'We recommend a minimum of 3 nodes in each Node Pool to avoid downtime during upgrades and maintenance.'; diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index 60c387bcccc..28516154615 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -806,6 +806,49 @@ export const mockGetLinodeStatsError = ( * * @returns Cypress chainable. */ -export const mockUpdateLinode = (linodeId: number): Cypress.Chainable => { +export const interceptUpdateLinode = ( + linodeId: number +): Cypress.Chainable => { return cy.intercept('PUT', apiMatcher(`linode/instances/${linodeId}`)); }; + +/** + * Intercepts PUT request to edit details of a linode + * + * @param linodeId - ID of Linode for intercepted request. + * @param updatedLinode - a mock linode object + * + * @returns Cypress chainable. + */ +export const mockUpdateLinode = ( + linodeId: number, + updatedLinode: Linode +): Cypress.Chainable => { + return cy.intercept( + 'PUT', + apiMatcher(`linode/instances/${linodeId}`), + updatedLinode + ); +}; + +/** + * Intercepts PUT request to edit details of a linode and mocks an error response. + * + * @param linodeId - ID of Linode for intercepted request. + * @param updatedLinode - a mock linode object + * @param errorMessage - Error message to be included in the mocked HTTP response. + * @param statusCode - HTTP status code for mocked error response. Default is `400`. + * + * @returns Cypress chainable. + */ +export const mockUpdateLinodeError = ( + linodeId: number, + errorMessage: string, + statusCode: number = 400 +): Cypress.Chainable => { + return cy.intercept( + 'PUT', + apiMatcher(`linode/instances/${linodeId}`), + makeErrorResponse(errorMessage, statusCode) + ); +}; diff --git a/packages/manager/cypress/support/plugins/junit-report.ts b/packages/manager/cypress/support/plugins/junit-report.ts index 2249babea92..9eff60f4a4d 100644 --- a/packages/manager/cypress/support/plugins/junit-report.ts +++ b/packages/manager/cypress/support/plugins/junit-report.ts @@ -27,6 +27,8 @@ const getCommonJunitConfig = ( testSuite: string, config: Cypress.PluginConfigOptions ) => { + const runnerIndex = Number(config.env['CY_TEST_SPLIT_RUN_INDEX']) || 1; + if (config.env[envVarName]) { if (!config.reporterOptions) { config.reporterOptions = {}; @@ -38,6 +40,9 @@ const getCommonJunitConfig = ( testsuitesTitle: testSuiteName, jenkinsMode: true, suiteTitleSeparatedBy: '→', + properties: { + runner_index: runnerIndex, + }, }; } return config; diff --git a/packages/manager/cypress/support/ui/constants.ts b/packages/manager/cypress/support/ui/constants.ts index a39dd3183e1..3ca596c5182 100644 --- a/packages/manager/cypress/support/ui/constants.ts +++ b/packages/manager/cypress/support/ui/constants.ts @@ -258,6 +258,6 @@ export const pages: Page[] = [ }, ], name: 'Settings', - url: `/settings`, + url: '/account-settings', }, ]; diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index 174ed08ea9e..98ee8d6af74 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -108,7 +108,7 @@ export const createTestLinode = async ( let regionId = createRequestPayload?.region; if (!regionId) { - regionId = chooseRegion().id; + regionId = chooseRegion({ capabilities: ['Linodes', 'Vlans'] }).id; } const securityMethodPayload: Partial = diff --git a/packages/manager/cypress/support/util/lke.ts b/packages/manager/cypress/support/util/lke.ts index 63005b76c96..cbb17398451 100644 --- a/packages/manager/cypress/support/util/lke.ts +++ b/packages/manager/cypress/support/util/lke.ts @@ -1,4 +1,7 @@ import { sortByVersion } from '@linode/utilities'; +import { ui } from 'support/ui'; + +import { randomNumber } from './random'; /** * Returns the string of the highest semantic version. @@ -16,3 +19,46 @@ export const getLatestKubernetesVersion = (versions: string[]) => { } return latestVersion; }; + +/** + * Performs a click operation on Cypress subject a given number of times. + * + * @param subject - Cypress subject to click. + * @param count - Number of times to perform click. + * + * @returns Cypress chainable. + */ +const multipleClick = ( + subject: Cypress.Chainable, + count: number +): Cypress.Chainable => { + if (count == 1) { + return subject.click(); + } + return multipleClick(subject.click(), count - 1); +}; + +/** + * Adds a random-sized node pool of the given plan. + * + * @param plan Name of plan for which to add nodes. + */ +export const addNodes = (plan: string) => { + const defaultNodes = 3; + const extraNodes = randomNumber(1, 5); + + cy.get(`[data-qa-plan-row="${plan}"`).within(() => { + multipleClick(cy.get('[data-testid="increment-button"]'), extraNodes); + multipleClick(cy.get('[data-testid="decrement-button"]'), extraNodes + 1); + + cy.get('[data-testid="textfield-input"]') + .invoke('val') + .should('eq', `${defaultNodes - 1}`); + + ui.button + .findByTitle('Add') + .should('be.visible') + .should('be.enabled') + .click(); + }); +}; diff --git a/packages/manager/package.json b/packages/manager/package.json index ae508f7b6ca..a53d571a399 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.149.1", + "version": "1.150.0", "private": true, "type": "module", "bugs": { @@ -59,7 +59,7 @@ "he": "^1.2.0", "immer": "^9.0.6", "ipaddr.js": "^1.9.1", - "jspdf": "^3.0.1", + "jspdf": "^3.0.2", "jspdf-autotable": "^5.0.2", "launchdarkly-react-client-sdk": "3.0.10", "libphonenumber-js": "^1.10.6", @@ -189,4 +189,4 @@ "Firefox ESR", "not dead" ] -} +} \ No newline at end of file diff --git a/packages/manager/public/pendo/pendo-staging.js b/packages/manager/public/pendo/pendo-staging.js index 6703205b634..0b327092f3e 100644 --- a/packages/manager/public/pendo/pendo-staging.js +++ b/packages/manager/public/pendo/pendo-staging.js @@ -1,121 +1,122 @@ // Pendo Agent Wrapper // Copyright 2025 Pendo.io, Inc. // Environment: staging -// Agent Version: 2.285.2 -// Installed: 2025-07-18T19:07:45Z +// Agent Version: 2.291.3 +// Installed: 2025-09-05T18:20:46Z (function (PendoConfig) { -/* -@license https://agent.pendo.io/licenses -*/ -!function(D,G,E){{var H="undefined"!=typeof PendoConfig?PendoConfig:{};z="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".split("");var z,j,o,W,J,q,K,V,$={uint8ToBase64:function(e){var t,n,i,r=e.length%3,o="";for(t=0,i=e.length-r;t>18&63]+z[e>>12&63]+z[e>>6&63]+z[63&e]}(n);switch(r){case 1:n=e[e.length-1],o=(o+=z[n>>2])+z[n<<4&63];break;case 2:n=(e[e.length-2]<<8)+e[e.length-1],o=(o=(o+=z[n>>10])+z[n>>4&63])+z[n<<2&63]}return o}},Ut="undefined"!=typeof globalThis?globalThis:void 0!==D?D:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function Z(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e["default"]:e}function Y(e){e?(K[0]=K[16]=K[1]=K[2]=K[3]=K[4]=K[5]=K[6]=K[7]=K[8]=K[9]=K[10]=K[11]=K[12]=K[13]=K[14]=K[15]=0,this.blocks=K):this.blocks=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],this.h0=1732584193,this.h1=4023233417,this.h2=2562383102,this.h3=271733878,this.h4=3285377520,this.block=this.start=this.bytes=this.hBytes=0,this.finalized=this.hashed=!1,this.first=!0}a=Be={exports:{}},e=!(j="object"==typeof D?D:{}).JS_SHA1_NO_COMMON_JS&&a.exports,o="0123456789abcdef".split(""),W=[-2147483648,8388608,32768,128],J=[24,16,8,0],q=["hex","array","digest","arrayBuffer"],K=[],V=function(t){return function(e){return new Y(!0).update(e)[t]()}},Y.prototype.update=function(e){if(!this.finalized){for(var t,n,i="string"!=typeof e,r=0,o=(e=i&&e.constructor===j.ArrayBuffer?new Uint8Array(e):e).length||0,a=this.blocks;r>2]|=e[r]<>2]|=t<>2]|=(192|t>>6)<>2]|=(224|t>>12)<>2]|=(240|t>>18)<>2]|=(128|t>>12&63)<>2]|=(128|t>>6&63)<>2]|=(128|63&t)<>2]|=W[3&t],this.block=e[16],56<=t&&(this.hashed||this.hash(),e[0]=this.block,e[16]=e[1]=e[2]=e[3]=e[4]=e[5]=e[6]=e[7]=e[8]=e[9]=e[10]=e[11]=e[12]=e[13]=e[14]=e[15]=0),e[14]=this.hBytes<<3|this.bytes>>>29,e[15]=this.bytes<<3,this.hash())},Y.prototype.hash=function(){for(var e,t=this.h0,n=this.h1,i=this.h2,r=this.h3,o=this.h4,a=this.blocks,s=16;s<80;++s)e=a[s-3]^a[s-8]^a[s-14]^a[s-16],a[s]=e<<1|e>>>31;for(s=0;s<20;s+=5)t=(e=(n=(e=(i=(e=(r=(e=(o=(e=t<<5|t>>>27)+(n&i|~n&r)+o+1518500249+a[s]<<0)<<5|o>>>27)+(t&(n=n<<30|n>>>2)|~t&i)+r+1518500249+a[s+1]<<0)<<5|r>>>27)+(o&(t=t<<30|t>>>2)|~o&n)+i+1518500249+a[s+2]<<0)<<5|i>>>27)+(r&(o=o<<30|o>>>2)|~r&t)+n+1518500249+a[s+3]<<0)<<5|n>>>27)+(i&(r=r<<30|r>>>2)|~i&o)+t+1518500249+a[s+4]<<0,i=i<<30|i>>>2;for(;s<40;s+=5)t=(e=(n=(e=(i=(e=(r=(e=(o=(e=t<<5|t>>>27)+(n^i^r)+o+1859775393+a[s]<<0)<<5|o>>>27)+(t^(n=n<<30|n>>>2)^i)+r+1859775393+a[s+1]<<0)<<5|r>>>27)+(o^(t=t<<30|t>>>2)^n)+i+1859775393+a[s+2]<<0)<<5|i>>>27)+(r^(o=o<<30|o>>>2)^t)+n+1859775393+a[s+3]<<0)<<5|n>>>27)+(i^(r=r<<30|r>>>2)^o)+t+1859775393+a[s+4]<<0,i=i<<30|i>>>2;for(;s<60;s+=5)t=(e=(n=(e=(i=(e=(r=(e=(o=(e=t<<5|t>>>27)+(n&i|n&r|i&r)+o-1894007588+a[s]<<0)<<5|o>>>27)+(t&(n=n<<30|n>>>2)|t&i|n&i)+r-1894007588+a[s+1]<<0)<<5|r>>>27)+(o&(t=t<<30|t>>>2)|o&n|t&n)+i-1894007588+a[s+2]<<0)<<5|i>>>27)+(r&(o=o<<30|o>>>2)|r&t|o&t)+n-1894007588+a[s+3]<<0)<<5|n>>>27)+(i&(r=r<<30|r>>>2)|i&o|r&o)+t-1894007588+a[s+4]<<0,i=i<<30|i>>>2;for(;s<80;s+=5)t=(e=(n=(e=(i=(e=(r=(e=(o=(e=t<<5|t>>>27)+(n^i^r)+o-899497514+a[s]<<0)<<5|o>>>27)+(t^(n=n<<30|n>>>2)^i)+r-899497514+a[s+1]<<0)<<5|r>>>27)+(o^(t=t<<30|t>>>2)^n)+i-899497514+a[s+2]<<0)<<5|i>>>27)+(r^(o=o<<30|o>>>2)^t)+n-899497514+a[s+3]<<0)<<5|n>>>27)+(i^(r=r<<30|r>>>2)^o)+t-899497514+a[s+4]<<0,i=i<<30|i>>>2;this.h0=this.h0+t<<0,this.h1=this.h1+n<<0,this.h2=this.h2+i<<0,this.h3=this.h3+r<<0,this.h4=this.h4+o<<0},Y.prototype.toString=Y.prototype.hex=function(){this.finalize();var e=this.h0,t=this.h1,n=this.h2,i=this.h3,r=this.h4;return o[e>>28&15]+o[e>>24&15]+o[e>>20&15]+o[e>>16&15]+o[e>>12&15]+o[e>>8&15]+o[e>>4&15]+o[15&e]+o[t>>28&15]+o[t>>24&15]+o[t>>20&15]+o[t>>16&15]+o[t>>12&15]+o[t>>8&15]+o[t>>4&15]+o[15&t]+o[n>>28&15]+o[n>>24&15]+o[n>>20&15]+o[n>>16&15]+o[n>>12&15]+o[n>>8&15]+o[n>>4&15]+o[15&n]+o[i>>28&15]+o[i>>24&15]+o[i>>20&15]+o[i>>16&15]+o[i>>12&15]+o[i>>8&15]+o[i>>4&15]+o[15&i]+o[r>>28&15]+o[r>>24&15]+o[r>>20&15]+o[r>>16&15]+o[r>>12&15]+o[r>>8&15]+o[r>>4&15]+o[15&r]},Y.prototype.array=Y.prototype.digest=function(){this.finalize();var e=this.h0,t=this.h1,n=this.h2,i=this.h3,r=this.h4;return[e>>24&255,e>>16&255,e>>8&255,255&e,t>>24&255,t>>16&255,t>>8&255,255&t,n>>24&255,n>>16&255,n>>8&255,255&n,i>>24&255,i>>16&255,i>>8&255,255&i,r>>24&255,r>>16&255,r>>8&255,255&r]},Y.prototype.arrayBuffer=function(){this.finalize();var e=new ArrayBuffer(20),t=new DataView(e);return t.setUint32(0,this.h0),t.setUint32(4,this.h1),t.setUint32(8,this.h2),t.setUint32(12,this.h3),t.setUint32(16,this.h4),e},Ue=function(){var t=V("hex");t.create=function(){return new Y},t.update=function(e){return t.create().update(e)};for(var e=0;ee,createHTML:e=>e};function Q(e){return t||(t=e.trustedTypesPolicy||(D.trustedTypes&&"function"==typeof D.trustedTypes.createPolicy?D.trustedTypes.createPolicy("pendo",q0):q0),e.trustedTypesPolicy=t),t}var I,ee="stagingServerHashes",te={};function ne(e){return e.loadAsModule}function ie(e){return"staging"===e.environmentName}function re(e){return"extension"===e.installType}function oe(t=[],n){var i=/^https:\/\/[\w\-.]*cdn[\w\-.]*\.(pendo-dev\.com|pendo\.io)\/agent\/static\/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}|PENDO_API_KEY)\/pendo\.js$/g;for(let e=0;e":">",'"':""","'":"'","`":"`"},Ge=De(t),t=De(Ee(t)),Ue=y.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g},Be=/(.)^/,He={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},ze=/\\|'|\r|\n|\u2028|\u2029/g;function je(e){return"\\"+He[e]}var We=/^\s*(\w|\$)+\s*$/;var Je=0;function qe(e,t,n,i,r){return i instanceof t?(i=Te(e.prototype),o(t=e.apply(i,r))?t:i):e.apply(n,r)}var _=c(function(r,o){var a=_.placeholder,s=function(){for(var e=0,t=o.length,n=Array(t),i=0;iu(h,"name"),sources:{SNIPPET_SRC:d,PENDO_CONFIG_SRC:c,GLOBAL_SRC:l,DEFAULT_SRC:p},validate(t){t.groupCollapsed("Validate Config options"),r(S(),function(e){t.log(String(e.active)),0=e},isMobileUserAgent:x.memoize(function(){return/Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(Oe())}),isChromeExtension:a},je=function(){return!isNaN(_e)&&11!=_e&&"CSS1Compat"!==G.compatMode},We=function(e,t){var n,i=e.height,r=e.width;return"top"==e.arrowPosition||"bottom"==e.arrowPosition?(n=0,"top"==e.arrowPosition?(e.top=t.top+t.height,n=-1,e.arrow.top=3,_e<=9&&(e.arrow.top=6)):"bottom"==e.arrowPosition&&(e.top=t.top-(i+I.TOOLTIP_ARROW_SIZE),e.arrow.top=i-I.TOOLTIP_ARROW_SIZE,10==_e?e.arrow.top--:_e<=9&&(e.arrow.top+=4),n=1),"left"==e.arrow.hbias?(e.left=t.left+t.width/2-(10+2*I.TOOLTIP_ARROW_SIZE),e.arrow.left=10+I.TOOLTIP_ARROW_SIZE):"right"==e.arrow.hbias?(e.left=t.left-r+t.width/2+(10+2*I.TOOLTIP_ARROW_SIZE),e.arrow.left=r-3*I.TOOLTIP_ARROW_SIZE-10):(e.left=t.left+t.width/2-r/2,e.arrow.left=r/2-I.TOOLTIP_ARROW_SIZE),e.arrow.border.top=e.arrow.top+n,e.arrow.border.left=e.arrow.left):("left"==e.arrow.hbias?(e.left=t.left+t.width,e.arrow.left=1,e.arrow.left+=5,e.arrow.border.left=e.arrow.left-1):"right"==e.arrow.hbias&&(e.left=Math.max(0,t.left-r-I.TOOLTIP_ARROW_SIZE),e.arrow.left=r-I.TOOLTIP_ARROW_SIZE-1,e.arrow.left+=5,e.arrow.border.left=e.arrow.left+1),e.top=t.top+t.height/2-i/2,e.arrow.top=i/2-I.TOOLTIP_ARROW_SIZE,e.arrow.border.top=e.arrow.top),e},Je="prod",qe="https://app.pendo.io",Ke="cdn.pendo.io",Ve="agent/releases/2.285.2",Ue="https://app.pendo.io",$e="2.285.2_prod",Be="2.285.2",Ze="xhr",Ye=function(){return je()?$e+"+quirksmode":$e};function Xe(){return-1!==Je.indexOf("prod")}var Qe=/^\s+|\s+$/g;function et(e){for(var t=[],n=0;n>6,128|63&i):i<55296||57344<=i?t.push(224|i>>12,128|i>>6&63,128|63&i):(n++,i=65536+((1023&i)<<10|1023&e.charCodeAt(n)),t.push(240|i>>18,128|i>>12&63,128|i>>6&63,128|63&i))}return t}var tt=(tt=String.prototype.trim)||function(){return this.replace(Qe,"")},e={exports:{}},nt=(!function(){var X=void 0,Q=!0,o=this;function e(e,t){var n,i=e.split("."),r=o;i[0]in r||!r.execScript||r.execScript("var "+i[0]);for(;i.length&&(n=i.shift());)i.length||t===X?r=r[n]||(r[n]={}):r[n]=t}var ee="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array;function te(e,t){if(this.index="number"==typeof t?t:0,this.e=0,this.buffer=e instanceof(ee?Uint8Array:Array)?e:new(ee?Uint8Array:Array)(32768),2*this.buffer.length<=this.index)throw Error("invalid index");this.buffer.length<=this.index&&u(this)}function u(e){var t,n=e.buffer,i=n.length,r=new(ee?Uint8Array:Array)(i<<1);if(ee)r.set(n);else for(t=0;t>>8&255]<<16|d[e>>>16&255]<<8|d[e>>>24&255])>>32-t:d[e]>>8-t),t+a<8)s=s<>t-i-1&1,8==++a&&(a=0,r[o++]=d[s],s=0,o===r.length)&&(r=u(this));r[o]=s,this.buffer=r,this.e=a,this.index=o},te.prototype.finish=function(){var e=this.buffer,t=this.index;return 0>>1;a;a>>>=1)i=i<<1|1&a,--r;t[n]=(i<>>0}var d=t;function c(e){this.buffer=new(ee?Uint16Array:Array)(2*e),this.length=0}function s(e,t){this.d=ne,this.i=0,this.input=ee&&e instanceof Array?new Uint8Array(e):e,this.c=0,t&&(t.lazy&&(this.i=t.lazy),"number"==typeof t.compressionType&&(this.d=t.compressionType),t.outputBuffer&&(this.a=ee&&t.outputBuffer instanceof Array?new Uint8Array(t.outputBuffer):t.outputBuffer),"number"==typeof t.outputIndex)&&(this.c=t.outputIndex),this.a||(this.a=new(ee?Uint8Array:Array)(32768))}c.prototype.getParent=function(e){return 2*((e-2)/4|0)},c.prototype.push=function(e,t){var n,i,r=this.buffer,o=this.length;for(r[this.length++]=t,r[this.length++]=e;0r[n]);)i=r[o],r[o]=r[n],r[n]=i,i=r[o+1],r[o+1]=r[n+1],r[n+1]=i,o=n;return this.length},c.prototype.pop=function(){var e,t,n,i=this.buffer,r=i[0],o=i[1];for(this.length-=2,i[0]=i[this.length],i[1]=i[this.length+1],n=0;!((t=2*n+2)>=this.length)&&(t+2i[t]&&(t+=2),i[t]>i[n]);)e=i[n],i[n]=i[t],i[t]=e,e=i[n+1],i[n+1]=i[t+1],i[t+1]=e,n=t;return{index:o,value:r,length:this.length}};for(var ne=2,l={NONE:0,h:1,g:ne,n:3},ie=[],p=0;p<288;p++)switch(Q){case p<=143:ie.push([p+48,8]);break;case p<=255:ie.push([p-144+400,9]);break;case p<=279:ie.push([p-256,7]);break;case p<=287:ie.push([p-280+192,8]);break;default:throw"invalid literal: "+p}function y(e,t){this.length=e,this.k=t}s.prototype.f=function(){var e,t,F,n=this.input;switch(this.d){case 0:for(t=0,F=n.length;t>>8&255,a[s++]=255&D,a[s++]=D>>>8&255,ee)a.set(i,s),s+=i.length,a=a.subarray(0,s);else{for(o=0,G=i.length;o>16&255,a[s++]=u>>24,Q){case 1===o:n=[0,o-1,0];break;case 2===o:n=[1,o-2,0];break;case 3===o:n=[2,o-3,0];break;case 4===o:n=[3,o-4,0];break;case o<=6:n=[4,o-5,1];break;case o<=8:n=[5,o-7,1];break;case o<=12:n=[6,o-9,2];break;case o<=16:n=[7,o-13,2];break;case o<=24:n=[8,o-17,3];break;case o<=32:n=[9,o-25,3];break;case o<=48:n=[10,o-33,4];break;case o<=64:n=[11,o-49,4];break;case o<=96:n=[12,o-65,5];break;case o<=128:n=[13,o-97,5];break;case o<=192:n=[14,o-129,6];break;case o<=256:n=[15,o-193,6];break;case o<=384:n=[16,o-257,7];break;case o<=512:n=[17,o-385,7];break;case o<=768:n=[18,o-513,8];break;case o<=1024:n=[19,o-769,8];break;case o<=1536:n=[20,o-1025,9];break;case o<=2048:n=[21,o-1537,9];break;case o<=3072:n=[22,o-2049,10];break;case o<=4096:n=[23,o-3073,10];break;case o<=6144:n=[24,o-4097,11];break;case o<=8192:n=[25,o-6145,11];break;case o<=12288:n=[26,o-8193,12];break;case o<=16384:n=[27,o-12289,12];break;case o<=24576:n=[28,o-16385,13];break;case o<=32768:n=[29,o-24577,13];break;default:throw"invalid distance"}for(u=n,a[s++]=u[0],a[+s]=u[1],a[5]=u[2],i=0,r=a.length;i2*u[r-1]+d[r]&&(u[r]=2*u[r-1]+d[r]),l[r]=Array(u[r]),p[r]=Array(u[r]);for(i=0;ie[i]?(l[r][o]=a,p[r][o]=n,s+=2):(l[r][o]=e[i],p[r][o]=i,++i);h[r]=0,1===d[r]&&function m(e){var t=p[e][h[e]];t===n?(m(e+1),m(e+1)):--c[t],++h[e]}(r)}return c}(i,i.length,t),o=0,a=n.length;o>>=1;return i}function f(e,t){this.input=e,this.a=new(ee?Uint8Array:Array)(32768),this.d=S.g;var n,i={};for(n in(t?"number"==typeof t.compressionType:(t={},0))&&(this.d=t.compressionType),t)i[n]=t[n];i.outputBuffer=this.a,this.j=new s(this.input,i)}var g,m,v,b,S=l,E=(f.prototype.f=function(){var e,t,n=0,i=this.a,r=Math.LOG2E*Math.log(32768)-8<<4|8;switch(i[n++]=r,8,this.d){case S.NONE:t=0;break;case S.h:t=1;break;case S.g:t=2;break;default:throw Error("unsupported compression type")}i[+n]=(e=t<<6|0)|31-(256*r+e)%31;var o=this.input;if("string"==typeof o){for(var a=o.split(""),s=0,u=a.length;s>>0;o=a}for(var d,c=1,l=0,p=o.length,h=0;0>>0,this.j.c=2,n=(i=this.j.f()).length,ee&&((i=new Uint8Array(i.buffer)).length<=n+4&&(this.a=new Uint8Array(i.length+4),this.a.set(i),i=this.a),i=i.subarray(0,n+4)),i[n++]=r>>24&255,i[n++]=r>>16&255,i[n++]=r>>8&255,i[+n]=255&r,i},e("Zlib.Deflate",f),e("Zlib.Deflate.compress",function(e,t){return new f(e,t).f()}),e("Zlib.Deflate.prototype.compress",f.prototype.f),{NONE:S.NONE,FIXED:S.h,DYNAMIC:S.g});if(Object.keys)g=Object.keys(E);else for(m in g=[],v=0,E)g[v++]=m;for(v=0,b=g.length;v>>8^r[255&(t^e[n])];for(o=i>>3;o--;n+=8)t=(t=(t=(t=(t=(t=(t=(t=t>>>8^r[255&(t^e[n])])>>>8^r[255&(t^e[n+1])])>>>8^r[255&(t^e[n+2])])>>>8^r[255&(t^e[n+3])])>>>8^r[255&(t^e[n+4])])>>>8^r[255&(t^e[n+5])])>>>8^r[255&(t^e[n+6])])>>>8^r[255&(t^e[n+7])];return(4294967295^t)>>>0},d:function(e,t){return(a.a[255&(e^t)]^e>>>8)>>>0},b:[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,853044451,1172266101,3705015759,2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,4240017532,1658658271,366619977,2362670323,4224994405,1303535960,984961486,2747007092,3569037538,1256170817,1037604311,2765210733,3554079995,1131014506,879679996,2909243462,3663771856,1141124467,855842277,2852801631,3708648649,1342533948,654459306,3188396048,3373015174,1466479909,544179635,3110523913,3462522015,1591671054,702138776,2966460450,3352799412,1504918807,783551873,3082640443,3233442989,3988292384,2596254646,62317068,1957810842,3939845945,2647816111,81470997,1943803523,3814918930,2489596804,225274430,2053790376,3826175755,2466906013,167816743,2097651377,4027552580,2265490386,503444072,1762050814,4150417245,2154129355,426522225,1852507879,4275313526,2312317920,282753626,1742555852,4189708143,2394877945,397917763,1622183637,3604390888,2714866558,953729732,1340076626,3518719985,2797360999,1068828381,1219638859,3624741850,2936675148,906185462,1090812512,3747672003,2825379669,829329135,1181335161,3412177804,3160834842,628085408,1382605366,3423369109,3138078467,570562233,1426400815,3317316542,2998733608,733239954,1555261956,3268935591,3050360625,752459403,1541320221,2607071920,3965973030,1969922972,40735498,2617837225,3943577151,1913087877,83908371,2512341634,3803740692,2075208622,213261112,2463272603,3855990285,2094854071,198958881,2262029012,4057260610,1759359992,534414190,2176718541,4139329115,1873836001,414664567,2282248934,4279200368,1711684554,285281116,2405801727,4167216745,1634467795,376229701,2685067896,3608007406,1308918612,956543938,2808555105,3495958263,1231636301,1047427035,2932959818,3654703836,1088359270,936918e3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117]};a.a="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array?new Uint32Array(a.b):a.b,e("Zlib.CRC32",a),e("Zlib.CRC32.calc",a.c),e("Zlib.CRC32.update",a.update)}.call(a.exports);var ot={CRC32:Z(a.exports).Zlib.CRC32},at=function(e,n){var i;return 200<=(n=n||0)?e:x.isArray(e)?x.map(e,function(e){return at(e,n+1)}):!x.isObject(e)||x.isDate(e)||x.isRegExp(e)||x.isElement(e)?x.isString(e)?x.escape(e):e:(i={},x.each(e,function(e,t){i[t]=at(e,n+1)}),i)},st=function(e){e=et(e);return $.uint8ToBase64(e)},ut=function(e){if(void 0!==e)return e=et(e=x.isString(e)?e:JSON.stringify(e)),ot.CRC32.calc(e,0,e.length)};function dt(e){return e[Math.floor(Math.random()*e.length)]}function ct(e){for(var t="abcdefghijklmnopqrstuvwxyz",n="",i=(t+t.toUpperCase()+"1234567890").split(""),r=0;rt+"="+e)),e}hashCode(){return this.toString()}}(a=yt=yt||{}).Debug="debug",a.Info="info",a.Warn="warn",a.Error="error",a.Critical="critical";class V0 extends class{constructor(){this.listeners={}}addEventListener(e,t){let n=this.listeners[e];n||(n=[],this.listeners[e]=n),x.findIndex(n,e=>t===e)<0&&n.push(t)}removeEventListener(e,t){var n,i=this.listeners[e];i&&0<=(n=x.findIndex(i,e=>t===e))&&(i.splice(n,1),i.length||delete this.listeners[e])}dispatchEvent(t){var e=this.listeners[t.type];e&&x.each(e,e=>{e(t)})}}{write(e,t,n){this.dispatchEvent(new K0(t,e,n))}writeError(e,t,n){let i,r;x.isString(e)?(i=e,r={message:i}):(r=e,i=r.message),n&&n.error&&(r=n.error,delete n.error);e=new K0(t,i,n);e.error=r,this.dispatchEvent(e)}debug(e,t){this.write(e,yt.Debug,t)}info(e,t){this.write(e,yt.Info,t)}warn(e,t){this.writeError(e,yt.Warn,t)}error(e,t){this.writeError(e,yt.Error,t)}critical(e,t){this.writeError(e,yt.Critical,t)}}const B=new V0;function Et(e){if(e)return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const $0=function(){var e=f.get("storage.allowKeys")||"*";return x.isArray(e)?x.indexBy(e):e};function It(t,n,i){return function(){try{return n.apply(t,arguments)}catch(e){return i}}}function xt(n){x.forEach(x.keys(n),function(e){try{/^_?pendo_/.test(e)&&n.removeItem(e)}catch(t){}})}function Ct(e){var t=x.noop,t={getItem:()=>null,setItem:t,removeItem:t,clearPendo:t};try{var n=e();return n?{getItem:It(n,n.getItem,null),setItem:It(n,n.setItem),removeItem:It(n,n.removeItem),clearPendo:x.partial(xt,n)}:t}catch(i){return t}}var _t,Tt=Ct(function(){return D.localStorage}),At=Ct(function(){return D.sessionStorage}),Rt={},Ot=!0,kt=function(){return f.get("localStorageOnly")},Lt=function(){return!!f.get("disableCookies")};function Nt(e){Ot=e}var Mt=function(e){var t=Lt()||kt()?Rt[e]:G.cookie;return(e=new RegExp("(^|; )"+e+"=([^;]*)").exec(t))?wt(e[2]):null},Pt=function(e,t,n,i){var r,o;!Ot||f.get("preventCookieRefresh")&&Mt(e)===t||(o=X0(n),(r=new Date).setTime(r.getTime()+o),o=e+"="+St(t)+(n?";expires="+r.toUTCString():"")+"; path=/"+("https:"===G.location.protocol||i?";secure":"")+"; SameSite=Strict",_t&&(o+=";domain="+_t),Lt()||kt()?Rt[e]=o:G.cookie=o)};function Ft(e){_t?Pt(e,""):G.cookie=e+"=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"}var Dt=function(e,t){return`_pendo_${e}.`+(t||I.apiKey)},Bt=function(e,t){return Mt(Dt(e,t))};const Z0=864e5,Y0=100*Z0,X0=(e=Y0)=>{var t=f.get("maxCookieTTLDays"),t=tn,getSession:()=>t}}(),qt=(D.Promise,e=function(){var t=Gt;function d(e){return Boolean(e&&"undefined"!=typeof e.length)}function i(){}function a(e){if(!(this instanceof a))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=E,this._deferreds=[],l(e,this)}function r(i,r){for(;3===i._state;)i=i._value;0===i._state?i._deferreds.push(r):(i._handled=!0,a._immediateFn(function(){var e,t=1===i._state?r.onFulfilled:r.onRejected;if(null===t)(1===i._state?o:s)(r.promise,i._value);else{try{e=t(i._value)}catch(n){return void s(r.promise,n)}o(r.promise,e)}}))}function o(e,t){try{if(t===e)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"==typeof t||"function"==typeof t)){var n=t.then;if(t instanceof a)return e._state=3,e._value=t,void u(e);if("function"==typeof n)return void l((i=n,r=t,function(){i.apply(r,arguments)}),e)}e._state=1,e._value=t,u(e)}catch(o){s(e,o)}var i,r}function s(e,t){e._state=2,e._value=t,u(e)}function u(e){2===e._state&&0===e._deferreds.length&&a._immediateFn(function(){e._handled||a._unhandledRejectionFn(e._value)});for(var t=0,n=e._deferreds.length;t+~]|"+c+")"+c+"*"),Ri=new RegExp(c+"|>"),Oi=new RegExp(xi),ki=new RegExp("^"+e+"$"),Li={ID:new RegExp("^#("+e+")"),CLASS:new RegExp("^\\.("+e+")"),TAG:new RegExp("^("+e+"|[*])"),ATTR:new RegExp("^"+Yv),PSEUDO:new RegExp("^"+xi),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+c+"*(even|odd|(([+-]|)(\\d*)n|)"+c+"*(?:([+-]|)"+c+"*(\\d+)|))"+c+"*\\)|)","i"),bool:new RegExp("^(?:"+Ii+")$","i"),needsContext:new RegExp("^"+c+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+c+"*((?:-\\d)?\\d*)"+c+"*\\)|)(?=[^-]|$)","i")},Ni=/HTML$/i,Mi=/^(?:input|select|textarea|button)$/i,Pi=/^h\d$/i,Fi=/^[^{]+\{\s*\[native \w/,Di=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Gi=/[+~]/,Ui=new RegExp("\\\\[\\da-fA-F]{1,6}"+c+"?|\\\\([^\\r\\n\\f])","g"),Bi=function(e,t){e="0x"+e.slice(1)-65536;return t||(e<0?String.fromCharCode(65536+e):String.fromCharCode(e>>10|55296,1023&e|56320))},Hi=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,zi=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},ji=function(){ei()},Wi=tr(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{wi.apply(Kv=Si.call(di.childNodes),di.childNodes),Kv[di.childNodes.length].nodeType}catch(W0){wi={apply:Kv.length?function(e,t){yi.apply(e,Si.call(t))}:function(e,t){for(var n=e.length,i=0;e[n++]=t[i++];);e.length=n-1}}}function T(e,t,n,i){var r,o,a,s,u,d,c=t&&t.ownerDocument,l=t?t.nodeType:9;if(n=n||[],"string"!=typeof e||!e||1!==l&&9!==l&&11!==l)return n;if(!i&&(ei(t),t=t||_,ni)){if(11!==l&&(s=Di.exec(e)))if(r=s[1]){if(9===l){if(!(d=t.getElementById(r)))return n;if(d.id===r)return n.push(d),n}else if(c&&(d=c.getElementById(r))&&ai(t,d)&&d.id===r)return n.push(d),n}else{if(s[2])return wi.apply(n,t.getElementsByTagName(e)),n;if((r=s[3])&&S.getElementsByClassName&&t.getElementsByClassName)return wi.apply(n,t.getElementsByClassName(r)),n}if(S.qsa&&!gi[e+" "]&&(!ii||!ii.test(e))&&(1!==l||"object"!==t.nodeName.toLowerCase())){if(d=e,c=t,1===l&&(Ri.test(e)||Ai.test(e))){for((c=Gi.test(e)&&Xi(t.parentNode)||t)===t&&S.scope||((a=t.getAttribute("id"))?a=a.replace(Hi,zi):t.setAttribute("id",a=ui)),o=(u=Vn(e)).length;o--;)u[o]=(a?"#"+a:":scope")+" "+er(u[o]);d=u.join(",")}try{return wi.apply(n,c.querySelectorAll(d)),n}catch(p){gi(e,!0)}finally{a===ui&&t.removeAttribute("id")}}}return Zn(e.replace(_i,"$1"),t,n,i)}function Ji(){var n=[];function i(e,t){return n.push(e+" ")>C.cacheLength&&delete i[n.shift()],i[e+" "]=t}return i}function qi(e){return e[ui]=!0,e}function Ki(e){var t=_.createElement("fieldset");try{return!!e(t)}catch(W0){return!1}finally{t.parentNode&&t.parentNode.removeChild(t)}}function Vi(e,t){for(var n=e.split("|"),i=n.length;i--;)C.attrHandle[n[i]]=t}function $i(e,t){var n=t&&e,i=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(i)return i;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function Zi(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&Wi(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function Yi(a){return qi(function(o){return o=+o,qi(function(e,t){for(var n,i=a([],e.length,o),r=i.length;r--;)e[n=i[r]]&&(e[n]=!(t[n]=e[n]))})})}function Xi(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(Jn in S=T.support={},Kn=T.isXML=function(e){var t=e.namespaceURI,e=(e.ownerDocument||e).documentElement;return!Ni.test(t||e&&e.nodeName||"HTML")},ei=T.setDocument=function(e){var e=e?e.ownerDocument||e:di;return e!=_&&9===e.nodeType&&e.documentElement&&(ti=(_=e).documentElement,ni=!Kn(_),di!=_&&(e=_.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",ji,!1):e.attachEvent&&e.attachEvent("onunload",ji)),S.scope=Ki(function(e){return ti.appendChild(e).appendChild(_.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),S.attributes=Ki(function(e){return e.className="i",!e.getAttribute("className")}),S.getElementsByTagName=Ki(function(e){return e.appendChild(_.createComment("")),!e.getElementsByTagName("*").length}),S.getElementsByClassName=!!_.getElementsByClassName,S.getById=Ki(function(e){return ti.appendChild(e).id=ui,!_.getElementsByName||!_.getElementsByName(ui).length}),S.getById?(C.filter.ID=function(e){var t=e.replace(Ui,Bi);return function(e){return e.getAttribute("id")===t}},C.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&ni)return(e=t.getElementById(e))?[e]:[]}):(C.filter.ID=function(e){var t=e.replace(Ui,Bi);return function(e){e="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return e&&e.value===t}},C.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&ni){var n,i,r,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];for(r=t.getElementsByName(e),i=0;o=r[i++];)if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),C.find.TAG=S.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):S.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,i=[],r=0,o=t.getElementsByTagName(e);if("*"!==e)return o;for(;n=o[r++];)1===n.nodeType&&i.push(n);return i},C.find.CLASS=S.getElementsByClassName&&function(e,t){return"undefined"!=typeof t.getElementsByClassName&&ni?t.getElementsByClassName(e):S.qsa&&ni?t.querySelectorAll("."+e):void 0},ri=[],ii=[],(S.qsa=!!_.querySelectorAll)&&(Ki(function(e){var t;ti.appendChild(e).innerHTML=Q().createHTML(""),e.querySelectorAll("[msallowcapture^='']").length&&ii.push("[*^$]="+c+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||ii.push("\\["+c+"*(?:value|"+Ii+")"),e.querySelectorAll("[id~="+ui+"-]").length||ii.push("~="),(t=_.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||ii.push("\\["+c+"*name"+c+"*="+c+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||ii.push(":checked"),e.querySelectorAll("a#"+ui+"+*").length||ii.push(".#.+[+~]"),e.querySelectorAll("\\\f"),ii.push("[\\r\\n\\f]")}),Ki(function(e){e.innerHTML=Q().createHTML("");var t=_.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&ii.push("name"+c+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&ii.push(":enabled",":disabled"),ti.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&ii.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),ii.push(",.*:")})),(S.matchesSelector=Fi.test(oi=ti.matches||ti.webkitMatchesSelector||ti.mozMatchesSelector||ti.oMatchesSelector||ti.msMatchesSelector))&&Ki(function(e){S.disconnectedMatch=oi.call(e,"*"),oi.call(e,"[s!='']:x"),ri.push("!=",xi)}),ii=ii.length&&new RegExp(ii.join("|")),ri=ri.length&&new RegExp(ri.join("|")),e=!!ti.compareDocumentPosition,ai=e||ti.contains?function(e,t){var n=9===e.nodeType?e.documentElement:e,t=t&&t.parentNode;return e===t||!(!t||1!==t.nodeType||!(n.contains?n.contains(t):e.compareDocumentPosition&&16&e.compareDocumentPosition(t)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},mi=e?function(e,t){var n;return e===t?(Qn=!0,0):(n=!e.compareDocumentPosition-!t.compareDocumentPosition)||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!S.sortDetached&&t.compareDocumentPosition(e)===n?e==_||e.ownerDocument==di&&ai(di,e)?-1:t==_||t.ownerDocument==di&&ai(di,t)?1:Xn?Ei(Xn,e)-Ei(Xn,t):0:4&n?-1:1)}:function(e,t){if(e===t)return Qn=!0,0;var n,i=0,r=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!r||!o)return e==_?-1:t==_?1:r?-1:o?1:Xn?Ei(Xn,e)-Ei(Xn,t):0;if(r===o)return $i(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[i]===s[i];)i++;return i?$i(a[i],s[i]):a[i]==di?-1:s[i]==di?1:0}),_},T.matches=function(e,t){return T(e,null,null,t)},T.matchesSelector=function(e,t){if(ei(e),S.matchesSelector&&ni&&!gi[t+" "]&&(!ri||!ri.test(t))&&(!ii||!ii.test(t)))try{var n=oi.call(e,t);if(n||S.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(W0){gi(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Ui,Bi),e[3]=(e[3]||e[4]||e[5]||"").replace(Ui,Bi),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||T.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&T.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Li.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&Oi.test(n)&&(t=(t=Vn(n,!0))&&n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Ui,Bi).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=pi[e+" "];return t||(t=new RegExp("(^|"+c+")"+e+"("+c+"|$)"))&&pi(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(t,n,i){return function(e){e=T.attr(e,t);return null==e?"!="===n:!n||(e+="","="===n?e===i:"!="===n?e!==i:"^="===n?i&&0===e.indexOf(i):"*="===n?i&&-1"),"#"===e.firstChild.getAttribute("href")})||Vi("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),S.attributes&&Ki(function(e){return e.innerHTML=Q().createHTML(""),e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||Vi("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),Ki(function(e){return null==e.getAttribute("disabled")})||Vi(Ii,function(e,t,n){if(!n)return!0===e[t]?t.toLowerCase():(n=e.getAttributeNode(t))&&n.specified?n.value:null});var or=si.Sizzle;T.noConflict=function(){return si.Sizzle===T&&(si.Sizzle=or),T},d.exports?d.exports=T:si.Sizzle=T,ar=Z(a.exports),(sr=x.extend(function(){return sr.Sizzle.apply(this,arguments)},ar)).reset=function(){sr.Sizzle=ar,sr.matchesSelector=ar.matchesSelector,sr.matches=ar.matches},sr.intercept=function(e,t="Sizzle"){sr[t]=x.wrap(sr[t],e)},sr.reset();var ar,sr,ur=sr;function A(e,t){var n,i=this;if(e&&e instanceof A)return e;if(!(i instanceof A))return new A(e,t);if(e)if(e.nodeType)n=[e];else if(r=/^<(\w+)\/?>$/.exec(e))n=[G.createElement(r[1])];else if(/^<[\w\W]+>$/.test(e)){var r=G.createElement("div");r.innerHTML=e,n=x.toArray(r.childNodes)}else if(x.isString(e)){t instanceof A&&(t=0{fr(e,t,n,i)}):x.noop}function fr(e,t,n,i){e&&t&&n&&(i=i||!1,e.removeEventListener?ht("removeEventListener",e).call(e,t,n,i):e.detachEvent&&e.detachEvent("on"+t,n))}var gr=function(e){var t=zn.getComposedPath(e);return t&&0{A.event.remove(t,e.type,e.handler,e.capture)}},dispatch(r,o){var e,t,a;r&&(e=(cr.get(r,"captureEvents")||{})[o.type]||[],t=(cr.get(r,"bubbleEvents")||{})[o.type]||[],(t=e.concat(t)).length)&&!(a=cr.get(o)).ignore&&(a.handled=a.handled||{},x.each(t.slice(),function(e){var t=!!e.capture===pr(o),n=(n=o,x.isNumber(n.eventPhase)&&2===n.eventPhase),t=!t&&!n;if(!(gr(o)!==r&&t||a.handled[e.id])){a.handled[e.id]=!0;try{(!be(e.selector)||0{clearTimeout(Sr)}}var kr=function(e){var t;try{t=_r()}catch(n){}return t},Lr=[],Nr=!1,Mr=null;function Pr(){var t=xr().href;Mr!=t&&(Mr=t,x.map(Lr,function(e){e(t)}))}var Fr="queryStringWhitelist";function Dr(e){var t=f.get("sanitizeUrl");return x.isFunction(t)?t(e):e}function Gr(e){e=e||xr().href;var t=f.get("annotateUrl");if(t)if(x.isFunction(t))try{var n,i,r,o=t();return o&&(x.isObject(o)||x.isArray(o))?(n=o.exclude,i=o.include,r=o.fragment,delete o.fragment,(n&&x.isArray(n)||i&&(x.isArray(i)||x.isObject(i)))&&(n&&(e=Br(e,null,n,!0)),o=i||{}),vr.urlFor(e,o,r)):e}catch(a){B.error("customer-provided `annotateUrl` function threw an exception",{error:a})}else B.error("customer-provided `annotateUrl` must be of type: function");return e}function Ur(e){var t,n;return!e||(t=e.indexOf("?"))<0?"":(n=e.indexOf("#"))<0?e.substring(t):n{i[e]=t})}),n.push(hr(D,"popstate",Pr))),ze.supportsHashChange()&&n.push(hr(D,"hashchange",Pr)),!1!==t&&ze.supportsHashChange()||n.push(Or()),Nr=!0),Lr.push(e),()=>{x.each(n,function(e){e()}),Lr.length=0,Nr=!1}},get:kr,externalizeURL:function(e,t,n){n=n||f.get(Fr);return Dr(Br(e,t,n=x.isFunction(n)?n():n,!1))},startPoller:Or,getWindowLocation:xr,clear:function(){Lr=[]},isElectron:Er,electronUserDirectory:function(){return D.process.env.PWD||""},electronAppName:function(){return D.process.env.npm_package_name||""},electronUserHomeDirectory:function(){return D.process.env.HOME||""},electronResourcesPath:function(){return D.process.resourcesPath||""}};function Jr(){var e=f.getLocalConfig("dataHost");return e||((e=f.getHostedConfig("dataHost"))?-1===e.indexOf("://")?"https://"+e:e:qe)}function qr(){Hr=Jr()}function Kr(){var e=f.get("contentHost")||f.get("assetHost")||Ke;return e=e&&-1===e.indexOf("://")?"https://"+e:e}function Vr(e){var t=Kr(),n=0<=(n=t).indexOf("localhost")||0<=n.indexOf("local.pendo.io")?e.replace(".min.js",".js"):e;return t+"/"+(Ve?Ve+"/":"")+n}function $r(){var e=f.get("allowPartnerAnalyticsForwarding",!1)&&f.get("adoptAnalyticsForwarding",!1);return f.get("trainingPartner",!1)||e}var Zr=3,Yr=1,Xr=9,Qr=11,eo=4;function O(e){var t;if((t=e)&&t.nodeType===Yr)try{return D.getComputedStyle?getComputedStyle(e):e.currentStyle||void 0}catch(n){}}function to(e,t){var n;return!(!e||!x.isFunction(e.getPropertyValue))&&(n=[e.getPropertyValue("transform")],void 0!==t&&x.isString(t)&&n.push(e.getPropertyValue("-"+t.toLowerCase()+"-transform")),x.any(n,function(e){return e&&"none"!==e}))}function no(e){var t=(e=e||D).document.documentElement;return e.pageYOffset||t.scrollTop}function io(e){var t=(e=e||D).document.documentElement;return e.pageXOffset||t.scrollLeft}function ro(e){return x.isNumber(e)?e:0}function oo(e,t){e=e.offsetParent;return t=t||D,e=e&&e.parentElement===t.document.documentElement&&!so(e)?null:e}function ao(e){return to(O(e),ke)&&isNaN(_e)}function so(e){if(e)return(e=O(e))&&(x.contains(["relative","absolute","fixed"],e.position)||to(e,ke))}function uo(e,t,n){if(!e)return{width:0,height:0};n=n||D;var t=so(t)?t:oo(t,n),i=t?co(t):{top:0,left:0},e=co(e),i={top:e.top-i.top,left:e.left-i.left,width:e.width,height:e.height};return t?(t!==n.document.scrollingElement&&(i.top+=ro(t.scrollTop),i.left+=ro(t.scrollLeft)),i.top-=ro(t.clientTop),i.left-=ro(t.clientLeft)):(i.top+=no(n),i.left+=io(n)),i.bottom=i.top+i.height,i.right=i.left+i.width,i}function co(e){var t;return e?e.getBoundingClientRect?{top:(t=e.getBoundingClientRect()).top,left:t.left,bottom:t.bottom,right:t.right,width:t.width||Math.abs(t.right-t.left),height:t.height||Math.abs(t.bottom-t.top)}:{top:0,left:0,width:e.offsetWidth,height:e.offsetHeight,right:e.offsetWidth,bottom:e.offsetHeight}:{width:0,height:0}}var lo=void 0===(e=f.get("pendoCore"))||e,po=function(e,t,n){e=Hr+"/data/"+e+"/"+t,t=x.map(n,function(e,t){return t+"="+e});return 0=n&&e.left>=i&&e.top+e.height<=n+t.height&&e.left+e.width<=i+t.width};function No(t){return x.each(["left","top","width","height"],function(e){t[e]=Math.round(t[e])}),t}function Mo(e,t=D){var n;return function(e){var t,n=e;for(;n;){if(!(t=O(n)))return;if("fixed"===t.position)return!isNaN(_e)||!jo(n);n=n.parentNode}return}(e)?((n=co(e)).fixed=!0,No(n)):No(uo(e,Go(t.document),t))}var Po=function(e){e&&e.parentNode&&e.parentNode.removeChild(e)},Fo=x.compose(function(e){return Array.prototype.slice.call(e)},function(e,t){try{return ur(e,t)}catch(n){return fo("error using sizzle: "+n),t.getElementsByTagName(e)}}),Do=function(e,t){try{return t.children.length+t.offsetHeight+t.offsetWidth-(e.children.length+e.offsetHeight+e.offsetWidth)}catch(n){return B.info("error interrogating body elements: "+n),fo("error picking best body:"+n),0}},Go=function(e){e=e||G;try{var t=Fo("body",e);return t&&1=t.bottom||e.bottom<=t.top||e.left>=t.right||e.right<=t.left)};function jo(e){for(var t=e&&e.parentNode;t;){if(to(O(t),ke))return 1;t=t.parentNode}}var Wo=function(e,t,n){t=t||/(auto|scroll|hidden)/;var i,r=(n=n||D).document.documentElement;if(Bo(e))for(i=e;i;)if(zn.isElementShadowRoot(i))i=i.host;else{if(i===r)return null;if(!(o=O(i)))return null;var o,a=o.position;if(i!==e&&t.test(o.overflow+o.overflowY+o.overflowX))return i.parentNode!==r||(o=O(r))&&!x.contains([o.overflow,o.overflowY,o.overflowX],"visible")?i:null;if("absolute"===a||"fixed"===a&&jo(i))i=oo(i);else{if("fixed"===a)return null;i=i.assignedSlot||i.parentNode}}return null};function Jo(e,t){e=O(e);return t=t||/(auto|scroll|hidden)/,!e||"inline"===e.display?qo.NONE:t.test(e.overflowY)&&t.test(e.overflowX)?qo.BOTH:t.test(e.overflowY)?qo.Y:t.test(e.overflowX)?qo.X:t.test(e.overflow)?qo.BOTH:qo.NONE}var qo={X:"x",Y:"y",BOTH:"both",NONE:"none"};function Ko(e){return e&&e.nodeName&&"body"===e.nodeName.toLowerCase()&&Bo(e)}function Vo(e){var t=G.createElement("script"),n=G.head||G.getElementsByTagName("head")[0]||G.body;t.type="text/javascript",e.src?t.src=e.src:t.text=e.text||e.textContent||e.innerHTML||"",n.appendChild(t),n.removeChild(t)}function $o(e){if(e){if(x.isFunction(e.getRootNode))return e.getRootNode();if(null!=e.ownerDocument)return e.ownerDocument}return G}function Zo(e,t,n){const i=[];var r=$o(G.documentElement);let o=$o(Wo(t)),a=0;for(;o!==r&&a<20;)i.push(e(o,"scroll",n,!0)),o=$o(Wo(o)),a++;return()=>{x.each(x.compact(i),function(e){e()}),i.length=0}}const aw=['a[href]:not([disabled]):not([tabindex="-1"])','button:not([disabled]):not([tabindex="-1"])','textarea:not([disabled]):not([tabindex="-1"])','input:not([disabled]):not([tabindex="-1"])','select:not([disabled]):not([tabindex="-1"])','[tabindex]:not([tabindex="-1"])',"iframe"].join(", ");function Yo(e,t,n){var i=Ho(t),t=Jo(t,n);if(t!==qo.BOTH||zo(e,i)){if(t===qo.Y){if(e.top>=i.bottom)return;if(e.bottom<=i.top)return}if(t===qo.X){if(e.left>=i.right)return;if(e.right<=i.left)return}return 1}}function Xo(e){if(e){if(Ko(e))return 1;var t=Ho(e);if(0!==t.width&&0!==t.height){var n=O(e);if(!n||"hidden"!==n.visibility){for(var i=e;i&&n;){if("none"===n.display)return;if(parseFloat(n.opacity)<=0)return;n=O(i=i.parentNode)}return 1}}}}function Qo(e,t){if(!Xo(e))return!1;if(!Ko(e)){for(var n=Ho(e),i=Wo(e,t=t||/hidden/),r=null;i&&i!==G&&i!==r;){if(!Yo(n,i,t))return!1;i=Wo(r=i,t)}if(e.getBoundingClientRect){var e=e.getBoundingClientRect(),o=e.right,e=e.bottom;if(n.fixed||(o+=io(),e+=no()),o<=0||e<=0)return!1}}return!0}function ea(e){var t,n,i,r,o,a=/(auto|scroll)/,s=/(auto|scroll|hidden)/,u=Ho(e),d=Wo(e,s);if(!Xo(e))return!1;for(;d;){if(t=Ho(d),(o=Jo(d,a))!==qo.NONE&&(i=n=0,o!==qo.Y&&o!==qo.BOTH||(u.bottom>t.bottom&&(n+=u.bottom-t.bottom,u.top-=n,u.bottom-=n),u.topt.right&&(i+=u.right-t.right,u.left-=i,u.right-=i),u.leftn.bottom&&(i+=t.bottom-n.bottom,t.top-=i,t.bottom-=i),t.topn.right&&(r+=t.right-n.right,t.left-=r,t.right-=r),t.left{},ze.MutationObserver){const n=new(ht("MutationObserver"))((e,t)=>{this.signal()});n.observe(e,t),this._teardown=()=>n.disconnect}else{const i=Gt(()=>{this.signal()},500);this._teardown=()=>{clearTimeout(i)}}}signal(){x.each(this.listeners,e=>{e.get()})}addObservers(...e){this.listeners=[].concat(this.listeners,e)}teardown(){this._teardown()}}Kv=function(){function e(e){this._object=e}return e.prototype.deref=function(){return this._object},e};var na="function"==typeof(d=D.WeakRef)&&/native/.test(d)?d:Kv();function ia(e,t){var n,i;return e.tagName&&-1<["textarea","input"].indexOf(e.tagName.toLowerCase())?(n=e.value,i=t,n.length<=i?n:sa(n.substring(0,i))):ra(e,t)}function ra(e,t=128){var n,i="",r=e.nodeType;if(r===Zr||r===eo)return e.nodeValue;if((n=e).tagName&&"textarea"!=n.tagName.toLowerCase()&&(r===Yr||r===Xr||r===Qr)){if(!e.childNodes)return i;for(var o,a=0;a{t.addEventListener(e,e=>this.onEvent(e))}),this.elRef=new na(t)}return t}getText(e=1024){return ia(this.get(),e)}addEventListener(e,t){var n=this.get();this.events.indexOf(e)<0&&(this.events.push(e),n)&&n.addEventListener(e,e=>this.onEvent(e)),this.listeners[e]=this.listeners[e]||[],this.listeners[e].push(t)}onEvent(t){var e=t.type;x.each(this.listeners[e],e=>e(t))}teardown(t=this.get()){t&&x.each(this.events,e=>t.removeEventListener(e,this.onEvent))}}function ua(t){if(!t)return!1;if(t===D.location.origin)return!0;if(t===Jr())return!0;if(t===Kr())return!0;var e=[/^https:\/\/(app|via|adopt)(\.eu|\.us|\.gov|\.jpn|\.hsbc|\.au)?\.pendo\.io$/,/^https:\/\/((adopt\.)?us1\.)?(app|via|adopt)\.pendo\.io$/,/^https:\/\/([0-9]{8}t[0-9]{4}-dot-)pendo-(io|eu|us1|govramp|jp-prod|hsbc|au)\.appspot\.com$/,/^https:\/\/hotfix-(ops|app)-([0-9]+-dot-)pendo-(io|eu|us1|govramp|jp-prod|hsbc|au)\.appspot\.com$/,/^https:\/\/pendo-(io|eu|us1|govramp|jp-prod|hsbc|au)-static\.storage\.googleapis\.com$/,/^https:\/\/(us1\.)?cdn(\.eu|\.jpn|\.gov|\.hsbc|\.au)?\.pendo\.io$/],n=(Xe()||(e=e.concat([/^https:\/\/([a-zA-Z0-9-]+\.)*pendo-dev\.com$/,/^https:\/\/([a-zA-Z0-9-]+-dot-)?pendo-(dev|test|io|us1|govramp|jp-prod|hsbc|au|batman|magic|atlas|wildlings|ionchef|mobile-guides|mobile-hummus|mobile-fbi|mobile-plat|eu|eu-dev|apollo|security|perfserf|freeze|armada|voc|mcfly|calypso|dap|scrum-ops|ml|helix|uat)\.appspot\.com$/,/^https:\/\/via\.pendo\.local:\d{4}$/,/^https:\/\/adopt\.pendo\.local:\d{4}$/,/^https:\/\/local\.pendo\.io:\d{4}$/,new RegExp("^https://pendo-"+Je+"-static\\.storage\\.googleapis\\.com$")])),f.get("adoptHost"));if(n&&t==="https://"+n)return!0;return!!x.contains(f.get("allowedOriginServers",[]),t)||x.any(e,function(e){return e.test(t)})}function da(e){var t;if(x.isString(e))return t=(t=Ur(e).substring(1))&&t.length?jr(t):{},e=x.last(x.first(e.split("?")).split("/")).split("."),{filename:x.first(e),extension:e.slice(1).join("."),query:t}}function ca(e,t){var n;f.get("guideValidation")&&ze.sri&&(n=da(t),t=x.find(["sha512","sha384","sha256"],function(e){return!!n.query[e]}))&&(e.integrity=t+"-"+(t=n.query[t],x.isString?t.replace(/-/g,"+").replace(/_/g,"/"):t),e.setAttribute("crossorigin","anonymous"))}x.extend(A,{data:cr,event:mr,removeNode:Po,getClass:_o,hasClass:Eo,addClass:function(e,t){var n;"string"==typeof e?(n=A(e),x.map(n,function(e){Io(e,t)})):Io(e,t)},removeClass:function(e,t){var n;"string"==typeof e?(n=A(e),x.map(n,function(e){xo(e,t)})):xo(e,t)},getBody:Go,getComputedStyle:O,getClientRect:Ho,intersectRect:zo,getScrollParent:Wo,isElementVisible:Qo,Observer:sw,Element:uw,scrollIntoView:ta,getRootNode:$o}),x.extend(A.prototype,mr.$,Yv.$);var la,pa=function(e){var t=0===f.get("allowedOriginServers",[]).length,n=$r();return!(!t&&!n&&(t=Je,n=te,!/prod/.test(t)||se(n)))||ua(e)},ha=function(e,t,n=!1){try{var i,r="text/css",o="text/javascript";if(x.isString(e)&&(e={url:e}),!pa((d=e.url,new Dn(d).origin)))throw new Error;e.type=e.type||/\.css/.test(e.url)?r:o;var a=null,s=G.getElementsByTagName("head")[0]||G.getElementsByTagName("body")[0];if(e.type===r){var u=G.createElement("link");u.type=r,u.rel="stylesheet",u.href=e.url,ca(u,e.url),a=u}else{if(pt())return(i=G.createElement("script")).addEventListener("load",function(){t(),Po(i)}),i.type=o,i.src=Q(I).createScriptURL(e.url),ca(i,e.url),G.body.appendChild(i),{};(i=G.createElement("script")).type=o,i["async"]=!0,i.src=Q(I).createScriptURL(e.url),ca(i,e.url),a=i,t=x.wrap(t,function(e,t){A.removeNode(i),t?n&&e(t):e.apply(this,x.toArray(arguments).slice(1))})}return s.appendChild(a),fa(a,e.url,t),a}catch(c){return{}}var d},fa=function(e,t,n){var i=!1;be(n)&&(e.onload=function(){!0!==i&&(i=!0,n(null,t))},e.onerror=function(){!0!==i&&(i=!0,n(new Error("Failed to load script"),t))},e.onreadystatechange=function(){i||e.readyState&&"loaded"!=e.readyState&&"complete"!=e.readyState||(i=!0,n(null,t))},"link"===e.tagName.toLowerCase())&&(Gt(function(){var e;i||((e=new Image).onload=e.onerror=function(){!0!==i&&(i=!0,n(null,t))},e.src=t)},500),Gt(function(){i||fo("Failed to load "+t+" within 10 seconds")},1e4))},ga=function(e){var t=JSON.parse(e.data),n=e.origin;B.debug(I.app_name+": Message: "+JSON.stringify(t)+" from "+n),Oa(e.source,{status:"success",msg:"ack",originator:"messageLogger"},n)},ma=function(e){ba(Ea)(va(e))},va=function(e){if(e.data)try{var t="string"==typeof e.data?JSON.parse(e.data):e.data,n=e.origin,i=e.source;if(!t.action&&!t.mutation){if(t.type&&"string"==typeof t.type)return{data:t,origin:n,source:i};B.debug("Invalid Message: Missing 'type' in data format")}}catch(r){}};function ba(t){return function(e){if(e&&ua(e.origin))return t.apply(this,arguments)}}var ya={disconnect(e){},module:function(e){Ra(e.moduleURL)},debug:function(e){Na(ga)}},wa=function(e,t){ya[e]=t},Sa=function(e){delete ya[e]},Ea=function(e){var t;e&&(t=e.data)&&be(ya[t.type])&&ya[t.type](t,e)},Ia={},xa=function(e){if(Ia[e]={},"undefined"!=typeof CKEDITOR)try{CKEDITOR.config.customConfig=""}catch(t){}},Ca=function(e){return be(Ia[e])},_a=function(e){if(Ia)for(var t in Ia)if(0<=t.indexOf(e))return t;return null},Ta=[],Aa=function(){var e;Ta.length<1||(e=Ta.shift(),Ca(e))||ha(e,function(){xa(e),Aa()})},Ra=function(e){!function(e){var t={"/js/lib/ckeditor/ckeditor.js":1};x.each(["depres.js","tether.js","sortable.js","selection.js","selection.css","html2canvas.js","ckeditor/ckeditor.js"],function(e){t["/modules/pendo.designer/plugins/"+e]=1,t["/engage-app-ui/assets/classic-designer/plugins/"+e]=1});try{var n=new Dn(e);return ua(n.origin)&&t[n.pathname]}catch(i){B.debug("Invalid module URL: "+e)}}(e)||(Ta.push(e),1e.codePointAt(0))))):JSON.parse(atob(e.split(".")[1]))}catch(n){return null}var t}function Qa(e,t){return t=t?t+": ":"",e.jwt||e.signingKeyName?e.jwt&&!e.signingKeyName?(B.debug(t+"The jwt is supplied but missing signingKeyName."),!1):e.signingKeyName&&!e.jwt?(B.debug(t+"The signingKeyName is supplied but missing jwt."),!1):(e=e.jwt,!(!x.isString(e)||!/^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/.test(e))||(B.debug(t+"The jwt is invalid."),!1)):(B.debug(t+"Missing jwt and signingKeyName."),!1)}es=null;var es,ts={set:function(e){es=JSON.parse(JSON.stringify(e||{}))},get:function(){return null!==es?es:{}},getJwtOptions:function(e,t){var n;return t=t||"",!!f.get("enableSignedMetadata")&&(n=Qa(e,t),f.get("requireSignedMetadata")&&!n?(B.debug("Pendo will not "+t+"."),!1):n?Xa(e.jwt):void B.debug("JWT is enabled but not being used, falling back to unsigned metadata."))}};class dw{constructor(e,t=100){this.queue=[],this.unloads=new Set,this.pending=new Set,this.failures=new Map,this.sendFn=e,this.maxFailures=t}isEmpty(){return this.queue.length<=0}stop(){this.queue.length=0,this.unloads.clear(),this.pending.clear(),this.failures.clear(),this.stopped=!0,clearTimeout(this.timer),delete this.timer}start(){this.stopped=!1}push(...e){this.queue.push(...e),this.next()}next(){var e;if(this.queue.length&&this.pending.size<1)return e=this.queue[0],this.send(e,!1,!0)}incrementFailure(e){var t=(this.failures.get(e)||0)+1;return this.failures.set(e,t),t}pass(e,t=!0){this.unloads["delete"](e),this.pending["delete"](e),this.failures.clear();e=this.queue.indexOf(e);0<=e&&this.queue.splice(e,1),!this.stopped&&t&&this.next()}fail(e,t=!0){this.unloads["delete"](e),this.pending["delete"](e);var n=this.incrementFailure(e);!this.stopped&&t&&(n>=this.maxFailures&&(this.onTimeout&&this.onTimeout(),this.pass(e,!1)),this.retryLater(1e3*Math.pow(2,Math.min(n-1,6))))}retryLater(e){this.timer=Gt(()=>{delete this.timer,this.next()},e)}failed(){return 0this.pass(e,n),()=>this.fail(e,n))}drain(e,t=!0){if(this.queue.push(...e),this.failed())return qt.reject();var n=[];for(const i of this.queue)this.pending.has(i)?this.retryPending&&t&&!this.unloads.has(i)&&(this.incrementFailure(i),n.push(this.send(i,t,!1))):n.push(this.send(i,t,!1));return qt.all(n)}}const cw="unsentEvents";class lw{constructor(){this.events={}}send(t,n){var i=this.events[t];if(i){let e=i.shift();for(;e;)n.push(e),e=i.shift();delete this.events[t]}}push(e,t){var n=this.events[e]||[];n.push(t),this.events[e]=n}read(e){e.registry.addLocal(cw),this.events=JSON.parse(e.read(cw)||"{}"),e.clear(cw)}write(e){0t.upper)return o;i+=n}if(!(0t.lower)return o;i+=n}return-1}function ys(){var e=ts.get();return x.isEmpty(e)?0:e.jwt.length+e.signingKeyName.length}function ws(e,t){var n;if(0!==e.length)return e.JZB||(e.JZB=I.squeezeAndCompress(e.slice()),e.JZB.length<=Ba)||1===e.length?t(e):(n=e.length/2,ws(e.slice(0,n),t),void ws(e.slice(n),t))}function Ss(e,t){So()&&t(e)}function Es(){return function(e,t){1===e.length&&e.JZB.length>Ba?(B.debug("Couldn't write event"),fo("Single item is: "+e.JZB.length+". Dropping."),go(e.JZB)):t(e)}}function Is(e,t){return po(t.beacon+".gif",e,x.extend({v:$e,ct:v(),jzb:t.JZB},t.params,t.auth))}function xs(e,t){return po(t.beacon+".gif",e,x.extend({v:$e,ct:v(),s:t.JZB.length},t.params))}function Cs(i){return function(e,t){e.params=x.extend({},e.params,i.params),e.beacon=i.beacon,e.eventLength=e.JZB.length;var n=ts.get();x.isEmpty(n)||(e.auth=n,e.eventLength+=n.jwt.length,e.eventLength+=n.signingKeyName.length),t(e)}}function _s(e,t){var n=$r(),i=x.first(e),i=x.get(i,"account_id");n&&i&&(e.params=x.extend({},e.params,{acc:st(i)})),t(e)}function Ts(e,t){var n=x.first(e),n=x.get(n,"props.source");n&&(e.params=x.extend({},e.params,{source:n})),t(e)}function As(e){return JSON.stringify(x.extend({events:e.JZB},e.auth))}function Rs(e){return e.status<200||300<=e.status?b.reject(new Error(`received status code ${e.status}: `+e.statusText)):b.resolve()}function Os(e,t){return vo(Is(e,t)).then(Rs)}function ks(e,t){return fetch(xs(e,t),{method:"POST",keepalive:!0,body:As(t),headers:{"Content-Type":"application/json"}}).then(Rs)}function Ls(e,t){var n=As(t);return bo(xs(e,t),n)?b.resolve():b.reject()}function Ns(n){return function(e,t){return t.JZB?t.eventLength<=Ua&&!f.get("sendEventsWithPostOnly")?n.preferFetch&&!t.auth&&vo.supported()?Os(e,t):t.auth?vr({method:"GET",url:Is(e,t)}):mo(Is(e,t)):n.allowPost&&t.eventLength<=Ba?vo.supported()?ks(e,t):bo.supported()?Ls(e,t):vr({method:"POST",url:xs(e,e=t),data:As(e),headers:{"Content-Type":"application/json"}}):b.resolve():b.resolve()}}function Ms(n){return function(e,t){if(t.JZB){if(t.eventLength<=Ua&&!f.get("sendEventsWithPostOnly",!1)){if(!t.auth&&vo.supported())return Os(e,t);if(ze.msie<=11)return vr({method:"GET",url:Is(e,t),sync:!0})}if(t.eventLength<=Ba&&n.allowPost){if(vo.supported())return ks(e,t);if(bo.supported())return Ls(e,t);if(ze.msie<=11)return vr({method:"POST",url:xs(e,e=t),data:As(e),sync:!0,headers:{"Content-Type":"application/json"}})}}return b.resolve()}}function Ps(e,t){e.length=0;var n,i={};for(n in e)e.hasOwnProperty(n)&&(i[n]=e[n]);t(i)}function Fs(e){return ls(Ss,fs,ws,(t=e.shorten,t=x.defaults(t||{},{fields:[],siloMaxLength:Ba}),function(n,e){var i;1===n.length&&n.JZB.length>t.siloMaxLength&&(i=n[0],B.debug("Max length exceeded for an event"),x.each(t.fields,function(e){var t=i[e];t&&2e3e.isEmpty())}stop(){x.each(this.queues,e=>e.stop())}push(){const t=x.toArray(arguments);x.each(this.queues,e=>e.push.apply(e,t))}drain(){const t=x.toArray(arguments);return b.all(x.map(this.queues,e=>e.drain.apply(e,t)))}}function Ds(o,a,s){a=a||Ns(o),s=s||Ms(o);e=o;var e=x.isFunction(e.apiKey)?[].concat(e.apiKey()):[].concat(e.apiKey),e=x.map(e,(i,r)=>{var e=new dw(function(e,t,n){return n&&(e.params=x.extend({},e.params,{rt:n})),o.localStorageUnload&&t?(0===r&&ns.push(o.beacon,e),b.resolve()):(t?s:a)(i,e)});return e.onTimeout=function(){y.commit("monitoring/incrementCounter",o.beacon+"GifFailures")},e.retryPending=!0,e}),e=new pw(e);return ns.send(o.beacon,e),e}function Gs(e,t){var n=f.get("analytics.excludeEvents");0<=x.indexOf(n,e.type)||t(e)}class hw{constructor(e){this.locks={},this.cache=e.cache||[],this.silos=e.silos||[],this.packageSilos=e.packageSilos,this.processSilos=e.processSilos,this.sendQueue=Ds(e)}pause(e=1e4){var t=x.uniqueId();const n=this["locks"];n[t]=1;var i=()=>{n[t]&&(clearTimeout(r),delete n[t],this.flush())},r=Gt(i,e);return i}push(e){this.packageSilos(e,e=>{this.silos.push(e)})}clear(){this.cache.length=0,this.silos.length=0,this.sendQueue.stop()}flush({unload:e=!1,hidden:t=!1}={}){var{cache:n,silos:i}=this;if((0!==n.length||0!==i.length||!this.sendQueue.isEmpty())&&x.isEmpty(this.locks)){i.push(n.slice()),n.length=0;n=i.slice();i.length=0;const r=[];x.each(n,function(e){this.processSilos(e,function(e){r.push(e)})},this),e||t?this.sendQueue.drain(r,e):this.sendQueue.push(...r)}}}function Us(e){var i,r,t=Fs(e),n=ls((r=e.beacon,function(e,t){var n=f.get("excludeNonGuideAnalytics");"ptm"===r&&n||t(e)}),Gs,ps,hs(e.cache),(i={overhead:ys,lower:f.get("sendEventsWithPostOnly")?Ba:Ua,upper:Ba,compressionRatio:[.5*rs,.75*rs,rs]},function(e,t){for(var n=bs(e,i);0<=n;)t(e.splice(0,Math.max(n,1))),n=bs(e,i)}));return new hw(x.extend({processSilos:t,packageSilos:n},e))}var Bs=Fn(function(e){var t,n,i;if((e=e||R.get())&&e!==Bs.lastUrl)return Bs.lastUrl=e,t=-1,Ma()||ka()&&(i=La(),Oa(i,{type:"load",url:location.toString()},"*")),B.debug("sending load event for url "+e),t={load_time:t="undefined"!=typeof performance&&x.isFunction(performance.getEntriesByType)&&!x.isEmpty(performance.getEntriesByType("navigation"))?(i=performance.getEntriesByType("navigation")[0]).loadEventStart-i.fetchStart:t},ka()&&(t.is_frame=!0),"*"!==(n=$0())&&(t.allowed_storage_keys=x.keys(n)),os("load",t,e),Va(),m.urlChanged.trigger(),!0});function Hs(e){return"hidden"===e.visibilityState}Bs.reset=function(){Bs.lastUrl=null};const fw="visibilitychange",gw="pagehide",mw="unload";function zs(){this.serializers=x.toArray(arguments)}function js(e,t){return e.tag=zn.isElementShadowRoot(t)?"#shadow-root":t.nodeName||"",e}function Ws(e){return be(e)?""+e:""}function Js(e,t){return e.id=Ws(t.id),e}function qs(e,t){return e.cls=Ws(A.getClass(t)),e}x.extend(zs.prototype,{add(e){this.serializers.push(e)},remove(e){e=x.indexOf(this.serializers,e);0<=e&&this.serializers.splice(e,1)},serialize(n,i){return n?(i=i||n,x.reduce(this.serializers,function(e,t){return t.call(this,e,n,i)},{},this)):{}}});var Ks=256,Vs=64,$s={a:{events:["click"],attr:["href"]},button:{events:["click"],attr:["value","name"]},img:{events:["click"],attr:["src","alt"]},select:{events:["mouseup"],attr:["name","type","selectedIndex"]},textarea:{events:["mouseup"],attr:["name"]},'input[type="submit"]':{events:["click"],attr:["name","type","value"]},'input[type="button"]':{events:["click"],attr:["name","type","value"]},'input[type="radio"]':{events:["click"],attr:["name","type"]},'input[type="checkbox"]':{events:["click"],attr:["name","type"]},'input[type="password"]':{events:["click"],attr:["name","type"]},'input[type="text"]':{events:["click"],attr:["name","type"]}},Zs=function(e,t,n){var i;return e&&e.nodeName?"img"==(i=e.nodeName.toLowerCase())&&"src"==t||"a"==i&&"href"==t?(i=e.getAttribute(t),Dr((i=i)&&0===i.indexOf("data:")?(B.debug("Embedded data provided in URI."),i.substring(0,i.indexOf(","))):i+"")):(i=t,e=(t=e).getAttribute?t.getAttribute(i):t[i],(!n||typeof e===n)&&e?x.isString(e)?tt.call(e).substring(0,Ks):e:null):null};function Ys(t){var e,n,i;return x.isRegExp(t)&&x.isFunction(t.test)?function(e){return t.test(e)}:x.isArray(t)?(e=x.map(x.filter(t,x.isObject),function(e){var t;return e.regexp?(t=(t=/\/([a-z]*)$/.exec(e.value))&&t[1]||"",new RegExp(e.value.replace(/^\//,"").replace(/\/[a-z]*$/,""),t)):new RegExp("^"+e.value+"$","i")}),function(t){return x.any(e,function(e){return e.test(t)})}):x.isObject(t)&&t.regexp?(n=(n=/\/([a-z]*)$/.exec(t.value))&&n[1]||"",i=new RegExp(t.value.replace(/^\//,"").replace(/\/[a-z]*$/,""),n),function(e){return i.test(e)}):x.constant(!1)}function Xs(e,t,n,i){try{var r,o=x.indexBy(t),a=x.filter(x.filter(e,function(e){return n(e.nodeName)||o[e.nodeName]}),function(e){return!i(e.nodeName)});return a.length<=Vs?x.pluck(a,"nodeName"):(r=x.groupBy(e,function(e){return o[e.nodeName]?"defaults":x.isString(e.value)&&e.value.length>Ks?"large":"small"}),x.pluck([].concat(x.sortBy(r.defaults,"nodeName")).concat(x.sortBy(r.small,"nodeName")).concat(x.sortBy(r.large,"nodeName")).slice(0,Vs),"nodeName"))}catch(s){return B.error("Error collecting DOM Node attributes: "+s),[]}}function Qs(t,n){var e=Ys(f.get("htmlAttributes")),i=Ys(f.get("htmlAttributeBlacklist")),r=(i("title")||(t.title=Zs(n,"title","string")),(t.tag||"").toLowerCase()),r=("input"===r&&(r+='[type="'+n.type+'"]'),t.attrs={},Xs(n.attributes,$s[r]&&$s[r].attr,e,i));return x.each(r,function(e){t.attrs[e.toLowerCase()]=Zs(n,e)}),t}function eu(e,t){var n;return t.parentNode&&t.parentNode.childNodes&&(n=x.chain(t.parentNode.childNodes),e.myIndex=n.indexOf(t).value(),e.childIndex=n.filter(function(e){return e.nodeType==Yr}).indexOf(t).value()),e}function tu(i,e){var r;return f.get("siblingSelectors")&&e.previousElementSibling&&(r="_pendo_sibling_",this.remove(tu),e=this.serialize(e.previousElementSibling),this.add(tu),i.attrs=i.attrs||{},x.each(e,function(e,t){var n={cls:"class",txt:"pendo_text"}[t]||t;x.isEmpty(e)||(x.isObject(e)?x.each(e,function(e,t){e&&!x.isEmpty(e)&&(i.attrs[r+n+"_"+t]=e)}):i.attrs[r+n]=e)})),i}var nu=new zs(js,Js,qs,Qs,eu,tu),iu=function(e){return"BODY"===e.nodeName&&e===Go()||null===e.parentNode&&!zn.isElementShadowRoot(e)},ru="pendo-ignore",ou="pendo-analytics-ignore",au=function(e){var t={},n=t,i=e,r=!1;if(!e)return t;do{var o=i,a=nu.serialize(o,e)}while(r||!lt(a.cls,ru)&&!lt(a.cls,ou)||(r=!0),n.parentElem=a,n=a,(i=zn.getParent(o))&&!iu(o));return r&&(t.parentElem.ignore=!0),t.parentElem},su=["","left","right","middle"],uu=[["button",function(e){return e.which||e.button},function(){return!0},function(e,t){return su[t]}],["altKey",e=function(e,t){return e[t]},a=function(e){return e},a],["ctrlKey",e,a,a],["metaKey",e,a,a],["shiftKey",e,a,a]],du={click:function(e,t){for(var n=[],i=0;i{lu.cancel()}),t.push(hr(G,"change",lu,!0)));var n,i=f.get("interceptElementRemoval")||f.get("syntheticClicks.elementRemoval"),r=f.get("syntheticClicks.targetChanged"),r=(t.push(function(t,e,n,i){var r,o,a=[],s=ze.hasEvent("pointerdown"),u=s?"pointerdown":"mousedown",s=s?"pointerup":"mouseup",d=[],c={cloneEvent:function(e){e=A.event.clone(e);return e.type="click",e.from=u,e.bubbles=!0,e},down:function(e){o=!1,e&&(r=c.cloneEvent(e),n)&&c.intercept(e)},up:function(e){o=!1,e&&r&&i&&gr(r)!==gr(e)&&(o=!0,t(r))},click:function(e){r=null,o&&A.data.set(e,"ignore",!0),o=!1,n&&c.unwrap()},intercept:function(e){e=function(e){var t=[];for(;e&&!iu(e);)t.push(e),e=e.parentNode;return t}(gr(e));x.each(e,function(e){e=hu(e,c.remove);a.push(e)})},remove:function(){r&&(t(r),r=null),c.unwrap()},unwrap:function(){0{x.each(n,function(e){e()})})),t.push(function(e,t){if(t){const n=[];return n.push(hr(G,fw,()=>{Hs(G)&&e(!0,!1)})),n.push(hr(D,gw,x.partial(e,!1,!0))),()=>x.each(n,function(e){e()})}return hr(D,mw,x.partial(e,!0,!0))}(function(e,t){t&&m.appUnloaded.trigger(),e&&m.appHidden.trigger()},f.get("preventUnloadListener"))),f.get("interceptStopPropagation",!0)),i=f.get("interceptPreventDefault",!0);return r&&t.push(fu(D.Event,e)),i&&t.push(gu(D.Event,["touchend"])),()=>{x.each(t,function(e){e()})}};function hu(n,i){var e=["remove","removeChild"];try{if(!n)return x.noop;x.each(e,function(e){var t=n[e];if(!t)return x.noop;n[e]=x.wrap(t,function(e){return i&&i(),e.apply(this,x.toArray(arguments).slice(1))}),n[e]._pendoUnwrap=function(){if(!n)return x.noop;n[e]=t,delete n[e]._pendoUnwrap}})}catch(t){B.critical("ERROR in interceptRemove",{error:t})}return function(){x.each(e,function(e){if(!n[e])return x.noop;e=n[e]._pendoUnwrap;x.isFunction(e)&&e()})}}function fu(n,e){var t=["stopPropagation","stopImmediatePropagation"];try{if(!n||!n.prototype)return x.noop;var i=x.indexBy(e);x.each(t,function(e){var t=n.prototype[e];t&&(n.prototype[e]=x.wrap(t,function(e){var t=e.apply(this,arguments);return i[this.type]&&(A.data.set(this,"stopped",!0),A.event.trigger(this)),t}),n.prototype[e]._pendoUnwrap=function(){n.prototype[e]=t,delete n.prototype[e]._pendoUnwrap})})}catch(r){B.critical("ERROR in interceptStopPropagation",{error:r})}return function(){x.each(t,function(e){e=n.prototype[e]._pendoUnwrap;x.isFunction(e)&&e()})}}function gu(t,e){try{if(!t||!t.prototype)return x.noop;var i=x.indexBy(e),n=t.prototype.preventDefault;if(!n)return x.noop;t.prototype.preventDefault=x.wrap(n,function(e){var t,n=e.apply(this,arguments);return i[this.type]&&((t=A.event.clone(this)).type="click",t.from=this.type,t.bubbles=!0,t.eventPhase=lr,A.event.trigger(t)),n}),t.prototype.preventDefault._pendoUnwrap=function(){t.prototype.preventDefault=n,delete t.prototype.preventDefault._pendoUnwrap}}catch(r){B.critical("ERROR in interceptPreventDefault",{error:r})}return function(){var e=t.prototype.preventDefault._pendoUnwrap;x.isFunction(e)&&e()}}function l(e,t,n,i){return e&&t&&n?(i&&!ze.addEventListener&&(i=!1),A.event.add(e,{type:t,handler:n,capture:i})):x.noop}function mu(e,t,n,i){e&&t&&n&&(i&&!ze.addEventListener&&(i=!1),A.event.remove(e,t,n,i))}var vu=function(e){A.data.set(e,"pendoStopped",!0),e.stopPropagation?e.stopPropagation():e.cancelBubble=!0,e.preventDefault?e.preventDefault():e.returnValue=!1},bu=function(e,t){return"complete"!==(t=t||D).document.readyState?hr(t,"load",e):(e(),x.noop)},k=[],yu=[],wu={};let h={};function Su(){return k}function Eu(){return Iu(k)}function Iu(e){return x.filter(e,function(e){return!e.isFrameProxy})}function xu(e){x.isArray(e)?(k=e,m.guideListChanged.trigger({guideIds:x.pluck(e,"id")})):B.info("bad guide array input to `setActiveGuides`")}var Cu=function(){let n=[];return{addGuide:e=>{var t;x.isEmpty(e)||(n=n.concat(e),x.each(n,e=>e.hide&&e.hide()),e=k,(t=x.difference(e,n)).length