diff --git a/packages/manager/.mocks/entities/linodes.ts b/packages/manager/.mocks/entities/linodes.ts new file mode 100644 index 00000000000..1dc7ecc9181 --- /dev/null +++ b/packages/manager/.mocks/entities/linodes.ts @@ -0,0 +1,91 @@ +import { defineMockEntity } from '@akamai/compute-ui-mocks'; + +import type { Linode } from '@linode/api-v4'; + +export const linodeList = defineMockEntity({ + name: 'linodes', + endpoint: '/v4beta/linode/instances', + method: 'GET', + factory: (overrides = {}) => ({ + id: overrides.id ?? 1, + label: overrides.label ?? 'my-linode', + status: overrides.status ?? 'running', + region: overrides.region ?? 'us-east', + alerts: overrides.alerts ?? { + cpu: 0, + io: 0, + network_in: 0, + network_out: 0, + transfer_quota: 0, + system_alerts: [], + user_alerts: [], + }, + backups: overrides.backups ?? { + enabled: false, + last_successful: null, + schedule: { + day: 'Monday', + window: 'W0', + }, + }, + capabilities: overrides.capabilities ?? ['Block Storage Encryption'], + created: overrides.created ?? new Date().toISOString(), + disk_encryption: overrides.disk_encryption ?? undefined, + group: overrides.group ?? '', + has_user_data: overrides.has_user_data ?? false, + hypervisor: overrides.hypervisor ?? 'kvm', + image: overrides.image ?? null, + interface_generation: overrides.interface_generation ?? undefined, + ipv4: overrides.ipv4 ?? ['192.0.2.1'], + ipv6: overrides.ipv6 ?? null, + lke_cluster_id: overrides.lke_cluster_id ?? null, + locks: overrides.locks ?? [], + maintenance_policy: overrides.maintenance_policy ?? null, + placement_group: overrides.placement_group ?? null, + site_type: overrides.site_type ?? 'core', + specs: overrides.specs ?? { + accelerated_devices: 0, + disk: 51200, + gpus: 0, + memory: 2048, + transfer: 2000, + vcpus: 1, + }, + tags: overrides.tags ?? [], + type: overrides.type ?? null, + updated: overrides.updated ?? new Date().toISOString(), + watchdog_enabled: overrides.watchdog_enabled ?? true, + }), +}); + +// GET single linode +export const linodeGet = defineMockEntity({ + name: 'linodes', + endpoint: '/v4beta/linode/instances/:id', + method: 'GET', + factory: linodeList.factory, +}); + +// PUT (update) a linode — echoes body back, writes to store if storeKey set +export const linodePut = defineMockEntity({ + name: 'linodes', + endpoint: '/v4beta/linode/instances/:id', + method: 'PUT', + factory: linodeList.factory, +}); + +// DELETE a linode — always returns 204, no factory needed +export const linodeDelete = defineMockEntity({ + name: 'linodes', + endpoint: '/v4/linode/instances/:id', + method: 'DELETE', + factory: () => ({}), +}); + +// POST (create) a linode — echoes body back +export const linodeCreate = defineMockEntity({ + name: 'linodes', + endpoint: '/v4beta/linode/instances', + method: 'POST', + factory: linodeList.factory, +}); diff --git a/packages/manager/.mocks/presets/linodes.json b/packages/manager/.mocks/presets/linodes.json new file mode 100644 index 00000000000..7c5e9ae772b --- /dev/null +++ b/packages/manager/.mocks/presets/linodes.json @@ -0,0 +1,1069 @@ +[ + { + "endpoint": "/v4beta/linode/instances", + "method": "POST" + }, + { + "endpoint": "/v4/linode/instances/:id", + "method": "DELETE", + "storeKey": "linodes" + }, + { + "endpoint": "/v4beta/linode/instances/:id", + "method": "GET", + "storeKey": "linodes" + }, + { + "endpoint": "/v4beta/linode/instances", + "method": "GET", + "paginated": true, + "data": [ + { + "id": 1, + "label": "my-linode-1", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.181Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.181Z", + "watchdog_enabled": true + }, + { + "id": 2, + "label": "my-linode-2", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 3, + "label": "my-linode-3", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 4, + "label": "my-linode-4", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 5, + "label": "my-linode-5", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 6, + "label": "my-linode-6", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 7, + "label": "my-linode-7", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 8, + "label": "my-linode-8", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 9, + "label": "my-linode-9", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 10, + "label": "my-linode-10", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 11, + "label": "my-linode-11", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 12, + "label": "my-linode-12", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 13, + "label": "my-linode-13", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 14, + "label": "my-linode-14", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 15, + "label": "my-linode-15", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 16, + "label": "my-linode-16", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 17, + "label": "my-linode-17", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 18, + "label": "my-linode-18", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 19, + "label": "my-linode-19", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + }, + { + "id": 20, + "label": "my-linode-20", + "status": "running", + "region": "us-east", + "alerts": { + "cpu": 0, + "io": 0, + "network_in": 0, + "network_out": 0, + "transfer_quota": 0, + "system_alerts": [], + "user_alerts": [] + }, + "backups": { + "enabled": false, + "last_successful": null, + "schedule": { + "day": "Monday", + "window": "W0" + } + }, + "capabilities": [ + "Block Storage Encryption" + ], + "created": "2026-04-07T10:06:13.182Z", + "group": "", + "has_user_data": false, + "hypervisor": "kvm", + "image": null, + "ipv4": [ + "192.0.2.1" + ], + "ipv6": null, + "lke_cluster_id": null, + "locks": [], + "maintenance_policy": null, + "placement_group": null, + "site_type": "core", + "specs": { + "accelerated_devices": 0, + "disk": 51200, + "gpus": 0, + "memory": 2048, + "transfer": 2000, + "vcpus": 1 + }, + "tags": [], + "type": null, + "updated": "2026-04-07T10:06:13.182Z", + "watchdog_enabled": true + } + ], + "storeKey": "linodes" + }, + { + "endpoint": "/v4beta/linode/instances/:id", + "method": "PUT", + "storeKey": "linodes" + } +] \ No newline at end of file diff --git a/packages/manager/package.json b/packages/manager/package.json index 676546b1543..d35ee5d7daf 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -89,7 +89,8 @@ "typescript-fsa": "^3.0.0", "typescript-fsa-reducers": "^1.2.0", "yup": "^1.4.0", - "zxcvbn": "^4.4.2" + "zxcvbn": "^4.4.2", + "@akamai/compute-ui-core": "link:../../../compute-ui/packages/compute-ui-core" }, "scripts": { "start": "concurrently --raw \"NODE_OPTIONS='--max-old-space-size=4096' vite\" \"NODE_OPTIONS='--max-old-space-size=4096' tsc --watch --preserveWatchOutput\"", @@ -180,7 +181,8 @@ "redux-mock-store": "^1.5.3", "storybook": "^10.3.3", "vite": "^7.3.2", - "vite-plugin-svgr": "^4.5.0" + "vite-plugin-svgr": "^4.5.0", + "@akamai/compute-ui-mocks": "link:../../../compute-ui/packages/compute-ui-mocks" }, "browserslist": [ ">1%", diff --git a/packages/manager/public/mockServiceWorker.js b/packages/manager/public/mockServiceWorker.js index 792e45b19da..daa58d0f120 100644 --- a/packages/manager/public/mockServiceWorker.js +++ b/packages/manager/public/mockServiceWorker.js @@ -2,26 +2,26 @@ /* tslint:disable */ /** - * Mock Service Worker (2.2.3). + * Mock Service Worker. * @see https://github.com/mswjs/msw * - Please do NOT modify this file. - * - Please do NOT serve this file on production. */ -const INTEGRITY_CHECKSUM = '223d191a56023cd36aa88c802961b911' +const PACKAGE_VERSION = '2.12.10' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() -self.addEventListener('install', function () { +addEventListener('install', function () { self.skipWaiting() }) -self.addEventListener('activate', function (event) { +addEventListener('activate', function (event) { event.waitUntil(self.clients.claim()) }) -self.addEventListener('message', async function (event) { - const clientId = event.source.id +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') if (!clientId || !self.clients) { return @@ -48,7 +48,10 @@ self.addEventListener('message', async function (event) { case 'INTEGRITY_CHECK_REQUEST': { sendToClient(client, { type: 'INTEGRITY_CHECK_RESPONSE', - payload: INTEGRITY_CHECKSUM, + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, }) break } @@ -58,16 +61,16 @@ self.addEventListener('message', async function (event) { sendToClient(client, { type: 'MOCKING_ENABLED', - payload: true, + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, }) break } - case 'MOCK_DEACTIVATE': { - activeClientIds.delete(clientId) - break - } - case 'CLIENT_CLOSED': { activeClientIds.delete(clientId) @@ -85,77 +88,99 @@ self.addEventListener('message', async function (event) { } }) -self.addEventListener('fetch', function (event) { - const { request } = event - - // Bypass launch darkly - if (request.url.includes("launchdarkly.com")) { - return - } +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() // Bypass navigation requests. - if (request.mode === 'navigate') { + if (event.request.mode === 'navigate') { return } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. - if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { return } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests - // after it's been deleted (still remains active until the next reload). + // after it's been terminated (still remains active until the next reload). if (activeClientIds.size === 0) { return } - // Generate unique request ID. const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId)) + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) }) -async function handleRequest(event, requestId) { +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { const client = await resolveMainClient(event) - const response = await getResponse(event, client, requestId) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { - ;(async function () { - const responseClone = response.clone() - - sendToClient( - client, - { - type: 'RESPONSE', - payload: { - requestId, - isMockedResponse: IS_MOCKED_RESPONSE in response, + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { type: responseClone.type, status: responseClone.status, statusText: responseClone.statusText, - body: responseClone.body, headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, }, }, - [responseClone.body], - ) - })() + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) } return response } -// Resolve the main client for the given event. -// Client that issues a request doesn't necessarily equal the client -// that registered the worker. It's with the latter the worker should -// communicate with during the response resolving phase. +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ async function resolveMainClient(event) { const client = await self.clients.get(event.clientId) + if (activeClientIds.has(event.clientId)) { + return client + } + if (client?.frameType === 'top-level') { return client } @@ -176,20 +201,39 @@ async function resolveMainClient(event) { }) } -async function getResponse(event, client, requestId) { - const { request } = event - +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). - const requestClone = request.clone() + const requestClone = event.request.clone() function passthrough() { - const headers = Object.fromEntries(requestClone.headers.entries()) + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) - // Remove internal MSW request header so the passthrough request - // complies with any potential CORS preflight checks on the server. - // Some servers forbid unknown request headers. - delete headers['x-msw-intention'] + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } return fetch(requestClone, { headers }) } @@ -207,37 +251,19 @@ async function getResponse(event, client, requestId) { return passthrough() } - // Bypass requests with the explicit bypass header. - // Such requests can be issued by "ctx.fetch()". - const mswIntention = request.headers.get('x-msw-intention') - if (['bypass', 'passthrough'].includes(mswIntention)) { - return passthrough() - } - // Notify the client that a request has been intercepted. - const requestBuffer = await request.arrayBuffer() + const serializedRequest = await serializeRequest(event.request) const clientMessage = await sendToClient( client, { type: 'REQUEST', payload: { id: requestId, - url: request.url, - mode: request.mode, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: requestBuffer, - keepalive: request.keepalive, + interceptedAt: requestInterceptedAt, + ...serializedRequest, }, }, - [requestBuffer], + [serializedRequest.body], ) switch (clientMessage.type) { @@ -245,7 +271,7 @@ async function getResponse(event, client, requestId) { return respondWithMock(clientMessage.data) } - case 'MOCK_NOT_FOUND': { + case 'PASSTHROUGH': { return passthrough() } } @@ -253,6 +279,12 @@ async function getResponse(event, client, requestId) { return passthrough() } +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { const channel = new MessageChannel() @@ -265,14 +297,18 @@ function sendToClient(client, message, transferrables = []) { resolve(event.data) } - client.postMessage( - message, - [channel.port2].concat(transferrables.filter(Boolean)), - ) + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) }) } -async function respondWithMock(response) { +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { // Setting response status code to 0 is a no-op. // However, when responding with a "Response.error()", the produced Response // instance will have status code set to 0. Since it's not possible to create @@ -290,3 +326,24 @@ async function respondWithMock(response) { return mockedResponse } + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/packages/manager/src/constants.ts b/packages/manager/src/constants.ts index 5d7a2f8fdb3..543e1e04e92 100644 --- a/packages/manager/src/constants.ts +++ b/packages/manager/src/constants.ts @@ -11,6 +11,11 @@ export const ENABLE_DEV_TOOLS = ? import.meta.env.DEV : getBooleanEnv(import.meta.env.REACT_APP_ENABLE_DEV_TOOLS); +export const VITE_MOCKS_ENABLED = + import.meta.env.VITE_MOCKS_ENABLED === undefined + ? import.meta.env.DEV + : getBooleanEnv(import.meta.env.VITE_MOCKS_ENABLED); + // allow us to explicity enable maintenance mode export const ENABLE_MAINTENANCE_MODE = getBooleanEnv( import.meta.env.REACT_APP_ENABLE_MAINTENANCE_MODE diff --git a/packages/manager/src/index.tsx b/packages/manager/src/index.tsx index b9088a9dc22..0dfe5dab3b4 100644 --- a/packages/manager/src/index.tsx +++ b/packages/manager/src/index.tsx @@ -13,7 +13,7 @@ import { storeFactory } from 'src/store'; import './index.css'; import { App } from './App'; -import { ENABLE_DEV_TOOLS } from './constants'; +import { VITE_MOCKS_ENABLED } from './constants'; import { LinodeThemeWrapper } from './LinodeThemeWrapper'; const queryClient = queryClientFactory('longLived'); @@ -41,19 +41,24 @@ const Main = () => { }; async function loadApp() { - if (ENABLE_DEV_TOOLS && !window.location.pathname.includes('/lish/')) { - const devTools = await import('./dev-tools/load'); - await devTools.loadDevTools(); + // if (ENABLE_DEV_TOOLS && !window.location.pathname.includes('/lish/')) { + // const devTools = await import('./dev-tools/load'); + // await devTools.loadDevTools(); - const { DevTools } = await import('./dev-tools/DevTools'); + // const { DevTools } = await import('./dev-tools/DevTools'); - const devToolsRootContainer = document.createElement('div'); - devToolsRootContainer.id = 'dev-tools-root'; - document.body.appendChild(devToolsRootContainer); + // const devToolsRootContainer = document.createElement('div'); + // devToolsRootContainer.id = 'dev-tools-root'; + // document.body.appendChild(devToolsRootContainer); - const root = createRoot(devToolsRootContainer); + // const root = createRoot(devToolsRootContainer); - root.render(); + // root.render(); + // } + + if (VITE_MOCKS_ENABLED) { + const { initMocks } = await import('@akamai/compute-ui-mocks'); + await initMocks(); } const container = document.getElementById('root'); diff --git a/packages/manager/vite.config.ts b/packages/manager/vite.config.ts index e09e6b07809..f4f67317026 100644 --- a/packages/manager/vite.config.ts +++ b/packages/manager/vite.config.ts @@ -1,3 +1,4 @@ +import { computeUiMocksPlugin } from '@akamai/compute-ui-mocks/vite-plugin'; import react from '@vitejs/plugin-react-swc'; import { URL } from 'url'; import svgr from 'vite-plugin-svgr'; @@ -17,6 +18,7 @@ export default defineConfig({ react(), svgr({ svgrOptions: { exportType: 'default' }, include: '**/*.svg' }), urlCanParsePolyfill(), + computeUiMocksPlugin() as any, ], resolve: { alias: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ae58ab823d..60260cc4a32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,6 +123,9 @@ importers: packages/manager: dependencies: + '@akamai/compute-ui-core': + specifier: link:../../../compute-ui/packages/compute-ui-core + version: link:../../../compute-ui/packages/compute-ui-core '@braintree/sanitize-url': specifier: ^7.1.0 version: 7.1.0 @@ -355,6 +358,9 @@ importers: '@4tw/cypress-drag-drop': specifier: ^2.3.1 version: 2.3.1(cypress@15.13.0) + '@akamai/compute-ui-mocks': + specifier: link:../../../compute-ui/packages/compute-ui-mocks + version: link:../../../compute-ui/packages/compute-ui-mocks '@storybook/addon-a11y': specifier: ^10.3.3 version: 10.3.3(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.5.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))