diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md index 0e605925df5..e7cb34aec3f 100644 --- a/docs/PULL_REQUEST_TEMPLATE.md +++ b/docs/PULL_REQUEST_TEMPLATE.md @@ -6,6 +6,9 @@ List any change relevant to the reviewer. - ... - ... +## Target release date 🗓️ +Please specify a release date to guarantee timely review of this PR. If exact date is not known, please approximate and update it as needed. + ## Preview 📷 **Include a screenshot or screen recording of the change** @@ -27,7 +30,7 @@ List any change relevant to the reviewer. - ... - ... -### Verification steps +### Verification steps (How to verify changes) - ... - ... diff --git a/docs/development-guide/05-fetching-data.md b/docs/development-guide/05-fetching-data.md index 5d44295ad67..ec26c115cbd 100644 --- a/docs/development-guide/05-fetching-data.md +++ b/docs/development-guide/05-fetching-data.md @@ -132,11 +132,11 @@ const UsernameDisplay = () => { ## When to use React Query or an api-v4 method directly Because **api-v4** methods don't commit data to a cache, it is acceptable to use **api-v4** methods directly -when performing ***one-time actions*** that do not require any immediate state change in Cloud Manager's UI. +when performing ***one-time actions*** that do not require any immediate state change in Cloud Manager's UI. While use of **api-v4** methods directly are acceptable, use of **React Query** Queries or Mutations are **still prefered** for the benefits described above. -A minimal example of accepable direct **api-v4** use: +A minimal example of acceptable direct **api-v4** use: ```ts resetKubeConfig({ id }).then(() => { @@ -229,3 +229,8 @@ console.log(errorMap); none: 'a linode_id error or similar' } ``` + +### Toast / Event Message Punctuation +**Best practice:** +- If a message is a sentence or a sentence fragment with a subject and a verb, add punctuation. Otherwise, leave punctuation off. +- If a developer notices inconsistencies within files they are already working in, they can progressively fix them. In this case, be prepared to fix any Cypress test failures. \ No newline at end of file diff --git a/docs/development-guide/08-testing.md b/docs/development-guide/08-testing.md index 91df0ddcad1..caf87e86060 100644 --- a/docs/development-guide/08-testing.md +++ b/docs/development-guide/08-testing.md @@ -45,7 +45,7 @@ Test execution will stop at the debugger statement, and you will be able to use ### React Testing Library -We have some older tests that still use the Enzyme framework, but for new tests we generally use [React Testing Library](https://testing-library.com/docs/react-testing-library/intro). This library provides a set of tools to render React components from within the Vitest environment. The library's philosophy is that components should be tested as closely as possible to how they are used. +This library provides a set of tools to render React components from within the Vitest environment. The library's philosophy is that components should be tested as closely as possible to how they are used. A simple test using this library will look something like this: diff --git a/package.json b/package.json index 67848f39cb2..9ed95835e54 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "build:sdk": "yarn workspace @linode/api-v4 build", "build:validation": "yarn workspace @linode/validation build", "build": "yarn build:validation && yarn build:sdk && yarn workspace linode-manager build", - "build:analyze": "yarn build --bundle-analyze", + "build:analyze": "yarn workspace linode-manager build:analyze", "up": "yarn install:all && yarn build:validation && yarn build:sdk && yarn start:all", "up:expose": "yarn install:all && yarn build:validation && yarn build:sdk && yarn start:all:expose", "dev": "yarn install:all && yarn start:all", diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index e2c188b5c88..7e09e127e93 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,5 +1,10 @@ -## [2024-02-05] - v0.109.0 +## [2024-02-20] - v0.110.0 + +### Upcoming Features: +- Update /account and /profile UserType from `null` to `"default"` ([#10176](https://github.com/linode/manager/pull/10176)) + +## [2024-02-05] - v0.109.0 ### Fixed: @@ -13,7 +18,6 @@ ## [2024-01-22] - v0.108.0 - ### Added: - AGLB endpoint health endpoints ([#10008](https://github.com/linode/manager/pull/10008)) @@ -25,10 +29,9 @@ ## [2024-01-08] - v0.107.0 - ### Added: -- Optional `headers` to `getProfile` function ([#9987](https://github.com/linode/manager/pull/9987)) +- Optional `headers` to `getProfile` function ([#9987](https://github.com/linode/manager/pull/9987)) ### Tech Stories: @@ -36,7 +39,6 @@ ## [2023-12-11] - v0.106.0 - ### Added: - Beta flag DC Get Well endpoints ([#9904](https://github.com/linode/manager/pull/9904)) @@ -55,7 +57,6 @@ ## [2023-11-13] - v0.105.0 - ### Upcoming Features: - Add `UpdateConfigurationPayload` ([#9853](https://github.com/linode/manager/pull/9853)) diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index a96528a1c27..4d5475a0cfe 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.109.0", + "version": "0.110.0", "homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4", "bugs": { "url": "https://github.com/linode/manager/issues" diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 6ad2d55d9a7..3800001d242 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -1,7 +1,7 @@ import type { APIWarning, RequestOptions } from '../types'; import type { Capabilities, Region } from '../regions'; -export type UserType = 'child' | 'parent' | 'proxy'; +export type UserType = 'child' | 'parent' | 'proxy' | 'default'; export interface User { email: string; @@ -30,7 +30,7 @@ export interface User { ssh_keys: string[]; tfa_enabled: boolean; username: string; - user_type: UserType | null; + user_type: UserType; verified_phone_number: string | null; } @@ -71,6 +71,7 @@ export type AccountCapability = | 'NodeBalancers' | 'Object Storage Access Key Regions' | 'Object Storage' + | 'Placement Group' | 'Vlans' | 'VPCs'; diff --git a/packages/api-v4/src/aglb/certificates.ts b/packages/api-v4/src/aclb/certificates.ts similarity index 88% rename from packages/api-v4/src/aglb/certificates.ts rename to packages/api-v4/src/aclb/certificates.ts index 10a9538687b..38af6d3f0a9 100644 --- a/packages/api-v4/src/aglb/certificates.ts +++ b/packages/api-v4/src/aclb/certificates.ts @@ -29,7 +29,7 @@ export const getLoadbalancerCertificates = ( ) => Request>( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent(loadbalancerId)}/certificates` + `${BETA_API_ROOT}/aclb/${encodeURIComponent(loadbalancerId)}/certificates` ), setMethod('GET'), setParams(params), @@ -47,7 +47,7 @@ export const getLoadbalancerCertificate = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/certificates/${encodeURIComponent(certificateId)}` ), @@ -65,7 +65,7 @@ export const createLoadbalancerCertificate = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent(loadbalancerId)}/certificates` + `${BETA_API_ROOT}/aclb/${encodeURIComponent(loadbalancerId)}/certificates` ), setMethod('POST'), setData(data, CreateCertificateSchema) @@ -83,7 +83,7 @@ export const updateLoadbalancerCertificate = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/certificates/${encodeURIComponent(certificateId)}` ), @@ -102,7 +102,7 @@ export const deleteLoadbalancerCertificate = ( ) => Request<{}>( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/certificates/${encodeURIComponent(certificateId)}` ), diff --git a/packages/api-v4/src/aglb/configurations.ts b/packages/api-v4/src/aclb/configurations.ts similarity index 90% rename from packages/api-v4/src/aglb/configurations.ts rename to packages/api-v4/src/aclb/configurations.ts index 82feaa83afc..8fe0d9ca30b 100644 --- a/packages/api-v4/src/aglb/configurations.ts +++ b/packages/api-v4/src/aclb/configurations.ts @@ -30,7 +30,7 @@ export const getLoadbalancerConfigurations = ( ) => Request>( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/configurations` ), @@ -50,7 +50,7 @@ export const getLoadbalancerConfiguration = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/configurations/${encodeURIComponent(configurationId)}` ), @@ -67,7 +67,7 @@ export const getLoadbalancerConfigurationsEndpointHealth = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/configurations/endpoints-health` ), @@ -85,7 +85,7 @@ export const createLoadbalancerConfiguration = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/configurations` ), @@ -105,7 +105,7 @@ export const updateLoadbalancerConfiguration = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/configurations/${encodeURIComponent(configurationId)}` ), @@ -124,7 +124,7 @@ export const deleteLoadbalancerConfiguration = ( ) => Request<{}>( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/configurations/${encodeURIComponent(configurationId)}` ), diff --git a/packages/api-v4/src/aglb/index.ts b/packages/api-v4/src/aclb/index.ts similarity index 100% rename from packages/api-v4/src/aglb/index.ts rename to packages/api-v4/src/aclb/index.ts diff --git a/packages/api-v4/src/aglb/loadbalancers.ts b/packages/api-v4/src/aclb/loadbalancers.ts similarity index 84% rename from packages/api-v4/src/aglb/loadbalancers.ts rename to packages/api-v4/src/aclb/loadbalancers.ts index 56a50288acd..2176bb3e7e5 100644 --- a/packages/api-v4/src/aglb/loadbalancers.ts +++ b/packages/api-v4/src/aclb/loadbalancers.ts @@ -23,7 +23,7 @@ import { CreateBasicLoadbalancerSchema } from '@linode/validation'; */ export const getLoadbalancers = (params?: Params, filter?: Filter) => Request>( - setURL(`${BETA_API_ROOT}/aglb`), + setURL(`${BETA_API_ROOT}/aclb`), setMethod('GET'), setParams(params), setXFilter(filter) @@ -36,7 +36,7 @@ export const getLoadbalancers = (params?: Params, filter?: Filter) => */ export const getLoadbalancer = (id: number) => Request( - setURL(`${BETA_API_ROOT}/aglb/${encodeURIComponent(id)}`), + setURL(`${BETA_API_ROOT}/aclb/${encodeURIComponent(id)}`), setMethod('GET') ); @@ -47,7 +47,7 @@ export const getLoadbalancer = (id: number) => */ export const getLoadbalancerEndpointHealth = (id: number) => Request( - setURL(`${BETA_API_ROOT}/aglb/${encodeURIComponent(id)}/endpoints-health`), + setURL(`${BETA_API_ROOT}/aclb/${encodeURIComponent(id)}/endpoints-health`), setMethod('GET') ); @@ -58,7 +58,7 @@ export const getLoadbalancerEndpointHealth = (id: number) => */ export const createLoadbalancer = (data: CreateLoadbalancerPayload) => Request( - setURL(`${BETA_API_ROOT}/aglb`), + setURL(`${BETA_API_ROOT}/aclb`), setData(data), setMethod('POST') ); @@ -70,7 +70,7 @@ export const createLoadbalancer = (data: CreateLoadbalancerPayload) => */ export const createBasicLoadbalancer = (data: CreateBasicLoadbalancerPayload) => Request( - setURL(`${BETA_API_ROOT}/aglb`), + setURL(`${BETA_API_ROOT}/aclb`), setData(data, CreateBasicLoadbalancerSchema), setMethod('POST') ); @@ -85,7 +85,7 @@ export const updateLoadbalancer = ( data: UpdateLoadbalancerPayload ) => Request( - setURL(`${BETA_API_ROOT}/aglb/${encodeURIComponent(id)}`), + setURL(`${BETA_API_ROOT}/aclb/${encodeURIComponent(id)}`), setData(data), setMethod('PUT') ); @@ -97,6 +97,6 @@ export const updateLoadbalancer = ( */ export const deleteLoadbalancer = (id: number) => Request<{}>( - setURL(`${BETA_API_ROOT}/aglb/${encodeURIComponent(id)}`), + setURL(`${BETA_API_ROOT}/aclb/${encodeURIComponent(id)}`), setMethod('DELETE') ); diff --git a/packages/api-v4/src/aglb/routes.ts b/packages/api-v4/src/aclb/routes.ts similarity index 87% rename from packages/api-v4/src/aglb/routes.ts rename to packages/api-v4/src/aclb/routes.ts index 0fcb196b0a5..91a7aa42686 100644 --- a/packages/api-v4/src/aglb/routes.ts +++ b/packages/api-v4/src/aclb/routes.ts @@ -22,7 +22,7 @@ export const getLoadbalancerRoutes = ( ) => Request>( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent(loadbalancerId)}/routes` + `${BETA_API_ROOT}/aclb/${encodeURIComponent(loadbalancerId)}/routes` ), setMethod('GET'), setParams(params), @@ -37,7 +37,7 @@ export const getLoadbalancerRoutes = ( export const getLoadbalancerRoute = (loadbalancerId: number, routeId: number) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/routes/${encodeURIComponent(routeId)}` ), @@ -55,7 +55,7 @@ export const createLoadbalancerRoute = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent(loadbalancerId)}/routes` + `${BETA_API_ROOT}/aclb/${encodeURIComponent(loadbalancerId)}/routes` ), setData(data, CreateRouteSchema), setMethod('POST') @@ -73,7 +73,7 @@ export const updateLoadbalancerRoute = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/routes/${encodeURIComponent(routeId)}` ), @@ -92,7 +92,7 @@ export const deleteLoadbalancerRoute = ( ) => Request<{}>( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/routes/${encodeURIComponent(routeId)}` ), diff --git a/packages/api-v4/src/aglb/service-targets.ts b/packages/api-v4/src/aclb/service-targets.ts similarity index 89% rename from packages/api-v4/src/aglb/service-targets.ts rename to packages/api-v4/src/aclb/service-targets.ts index c8caddf0cfe..40ef68a8939 100644 --- a/packages/api-v4/src/aglb/service-targets.ts +++ b/packages/api-v4/src/aclb/service-targets.ts @@ -29,7 +29,7 @@ export const getLoadbalancerServiceTargets = ( ) => Request>( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/service-targets` ), @@ -49,7 +49,7 @@ export const getServiceTarget = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/service-targets/${encodeURIComponent(serviceTargetId)}` ), @@ -64,7 +64,7 @@ export const getServiceTarget = ( export const getServiceTargetsEndpointHealth = (loadbalancerId: number) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/service-targets/endpoints-health` ), @@ -82,7 +82,7 @@ export const createLoadbalancerServiceTarget = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/service-targets` ), @@ -102,7 +102,7 @@ export const updateLoadbalancerServiceTarget = ( ) => Request( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/service-targets/${encodeURIComponent(serviceTargetId)}` ), @@ -121,7 +121,7 @@ export const deleteLoadbalancerServiceTarget = ( ) => Request<{}>( setURL( - `${BETA_API_ROOT}/aglb/${encodeURIComponent( + `${BETA_API_ROOT}/aclb/${encodeURIComponent( loadbalancerId )}/service-targets/${encodeURIComponent(serviceTargetId)}` ), diff --git a/packages/api-v4/src/aglb/types.ts b/packages/api-v4/src/aclb/types.ts similarity index 100% rename from packages/api-v4/src/aglb/types.ts rename to packages/api-v4/src/aclb/types.ts diff --git a/packages/api-v4/src/index.ts b/packages/api-v4/src/index.ts index 7d407750435..b62ab50d817 100644 --- a/packages/api-v4/src/index.ts +++ b/packages/api-v4/src/index.ts @@ -1,6 +1,6 @@ export * from './account'; -export * from './aglb'; +export * from './aclb'; export * from './databases'; diff --git a/packages/api-v4/src/object-storage/objectStorageKeys.ts b/packages/api-v4/src/object-storage/objectStorageKeys.ts index 6dd4d417adb..df8f3b0ce48 100644 --- a/packages/api-v4/src/object-storage/objectStorageKeys.ts +++ b/packages/api-v4/src/object-storage/objectStorageKeys.ts @@ -1,4 +1,7 @@ -import { createObjectStorageKeysSchema } from '@linode/validation/lib/objectStorageKeys.schema'; +import { + createObjectStorageKeysSchema, + updateObjectStorageKeysSchema, +} from '@linode/validation/lib/objectStorageKeys.schema'; import { API_ROOT } from '../constants'; import Request, { setData, @@ -51,7 +54,7 @@ export const updateObjectStorageKey = ( Request( setMethod('PUT'), setURL(`${API_ROOT}/object-storage/keys/${encodeURIComponent(id)}`), - setData(data, createObjectStorageKeysSchema) + setData(data, updateObjectStorageKeysSchema) ); /** diff --git a/packages/api-v4/src/placement-groups/placement-groups.ts b/packages/api-v4/src/placement-groups/placement-groups.ts index 834cf10ead2..16cd63182fb 100644 --- a/packages/api-v4/src/placement-groups/placement-groups.ts +++ b/packages/api-v4/src/placement-groups/placement-groups.ts @@ -1,7 +1,5 @@ import { - assignVMsToPlacementGroupSchema, createPlacementGroupSchema, - unassignVMsFromPlacementGroupSchema, renamePlacementGroupSchema, } from '@linode/validation'; import { API_ROOT } from '../constants'; @@ -15,10 +13,10 @@ import Request, { } from '../request'; import type { Filter, Params, ResourcePage as Page } from '../types'; import type { - AssignVMsToPlacementGroupPayload, + AssignLinodesToPlacementGroupPayload, CreatePlacementGroupPayload, PlacementGroup, - UnassignVMsFromPlacementGroupPayload, + UnassignLinodesFromPlacementGroupPayload, RenamePlacementGroupPayload, } from './types'; @@ -109,9 +107,9 @@ export const deletePlacementGroup = (placementGroupId: number) => * * @note While this accepts an array of Linode ids (future proofing), only one Linode id is supported at this time. */ -export const assignVMsToPlacementGroup = ( +export const assignLinodesToPlacementGroup = ( placementGroupId: number, - linodeIds: AssignVMsToPlacementGroupPayload + payload: AssignLinodesToPlacementGroupPayload ) => Request( setURL( @@ -120,7 +118,7 @@ export const assignVMsToPlacementGroup = ( )}/assign` ), setMethod('POST'), - setData(linodeIds, assignVMsToPlacementGroupSchema) + setData(payload) ); /** @@ -133,9 +131,9 @@ export const assignVMsToPlacementGroup = ( * * @note While this accepts an array of Linode ids (future proofing), only one Linode id is supported at this time. */ -export const unassignVMsFromPlacementGroup = ( +export const unassignLinodesFromPlacementGroup = ( placementGroupId: number, - linodeIds: UnassignVMsFromPlacementGroupPayload + payload: UnassignLinodesFromPlacementGroupPayload ) => Request( setURL( @@ -144,5 +142,5 @@ export const unassignVMsFromPlacementGroup = ( )}/unassign` ), setMethod('POST'), - setData(linodeIds, unassignVMsFromPlacementGroupSchema) + setData(payload) ); diff --git a/packages/api-v4/src/placement-groups/types.ts b/packages/api-v4/src/placement-groups/types.ts index 1952653081b..63f64584004 100644 --- a/packages/api-v4/src/placement-groups/types.ts +++ b/packages/api-v4/src/placement-groups/types.ts @@ -12,20 +12,26 @@ export interface PlacementGroup { label: string; region: Region['id']; affinity_type: AffinityType; - compliant: boolean; + is_compliant: boolean; linode_ids: number[]; - capacity: number; } +// The `strict` parameter specifies whether placement groups should be ignored when looking for a host. +// TODO VM_Placement: figure out the values for each create flow (create, clone, migrate etc) export type CreatePlacementGroupPayload = Pick< PlacementGroup, 'label' | 'affinity_type' | 'region' ->; +> & { strict: boolean }; export type RenamePlacementGroupPayload = Pick; /** * Since the API expects an array of ONE linode id, we'll use a tuple here. */ -export type AssignVMsToPlacementGroupPayload = [number]; -export type UnassignVMsFromPlacementGroupPayload = [number]; +export type AssignLinodesToPlacementGroupPayload = { + linodes: [number]; + strict: boolean; +}; +export type UnassignLinodesFromPlacementGroupPayload = { + linodes: [number]; +}; diff --git a/packages/api-v4/src/profile/types.ts b/packages/api-v4/src/profile/types.ts index 0d7ea3347f3..aed94d75fda 100644 --- a/packages/api-v4/src/profile/types.ts +++ b/packages/api-v4/src/profile/types.ts @@ -24,7 +24,7 @@ export interface Profile { two_factor_auth: boolean; restricted: boolean; verified_phone_number: string | null; - user_type: UserType | null; + user_type: UserType; } export interface TokenRequest { diff --git a/packages/api-v4/src/regions/types.ts b/packages/api-v4/src/regions/types.ts index f03c852531c..23204247879 100644 --- a/packages/api-v4/src/regions/types.ts +++ b/packages/api-v4/src/regions/types.ts @@ -3,6 +3,7 @@ import { COUNTRY_CODE_TO_CONTINENT_CODE } from './constants'; export type Capabilities = | 'Bare Metal' | 'Block Storage' + | 'Block Storage Migrations' | 'Cloud Firewall' | 'GPU Linodes' | 'Kubernetes' @@ -11,6 +12,7 @@ export type Capabilities = | 'Metadata' | 'NodeBalancers' | 'Object Storage' + | 'Placement Group' | 'Premium Plans' | 'Vlans' | 'VPCs'; @@ -27,6 +29,8 @@ export interface Region { label: string; country: Country; capabilities: Capabilities[]; + maximum_pgs_per_customer: number; + maximum_vms_per_pg: number; status: RegionStatus; resolvers: DNSResolvers; } diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 35ff819b993..88934156e35 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,6 +4,58 @@ 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/). +## [2024-02-20] - v1.113.0 + +### Added: + +- Feb 2024 Marketplace apps ([#10149](https://github.com/linode/manager/pull/10149)) + +### Changed: + +- Improve Linode Graph X Axis Labels when viewing historic data ([#10186](https://github.com/linode/manager/pull/10186)) + +### Fixed: + +- EditableText interaction styling ([#10132](https://github.com/linode/manager/pull/10132)) +- Inability to transfer IPv6 ranges ([#10156](https://github.com/linode/manager/pull/10156)) +- Incorrect `X-Filter` on the Account Maintenance "Pending" Table (#10196) +- Bundle analyzer script ([#10175](https://github.com/linode/manager/pull/10175)) + +### Tech Stories: + +- Clean up DC Get Well feature flag logic ([#10146](https://github.com/linode/manager/pull/10146)) +- Clean up `regionDropdown` feature flag ([#10148](https://github.com/linode/manager/pull/10148)) +- Update `react-router-dom` in preparation for React 18 ([#10154](https://github.com/linode/manager/pull/10154)) +- Remove Enzyme ([#10160](https://github.com/linode/manager/pull/10160)) +- Update Luxon ([#10163](https://github.com/linode/manager/pull/10163)) +- Update `launchdarkly-react-client-sdk` ([#10165](https://github.com/linode/manager/pull/10165)) +- Add analytics event for breadcrumb label edit icon on Linode details page ([#10183](https://github.com/linode/manager/pull/10183)) + +### Tests: + +- Add integration test coverage for Account Login History ([#10125](https://github.com/linode/manager/pull/10125)) +- Add integration test to check proxy user disabled username/email field ([#10139](https://github.com/linode/manager/pull/10139)) +- Add Cypress tests for OBJ Multicluster access key operations ([#10144](https://github.com/linode/manager/pull/10144)) +- Fix billing contact Cypress test by narrowing element selection scope ([#10150](https://github.com/linode/manager/pull/10150)) +- Update Cypress tests to use `"default"` `user_type` for non-parent/child/proxy users ([#10176](https://github.com/linode/manager/pull/10176)) +- Fix Button enabled assertions ([#10142](https://github.com/linode/manager/pull/10142)) + +### Upcoming Features: + +- Disable "Save" button in Edit Access Key drawer unless field values are changed ([#10118](https://github.com/linode/manager/pull/10118)) +- Add Placement Groups Select component (#10100) +- Update Placement Groups limits ([#10191](https://github.com/linode/manager/pull/10191)) +- Add Placement Group Linodes List ([#10123](https://github.com/linode/manager/pull/10123)) +- Add AssignLinodesToPlacementGroup drawer ([#10140](https://github.com/linode/manager/pull/10140)) +- Add PlacementGroups Summary component ([#10164](https://github.com/linode/manager/pull/10164)) +- Add unassign linode from Placement Group modal (#10172) +- Improve restricted access Login History experience for child and restricted users ([#10125](https://github.com/linode/manager/pull/10125)) +- Add session expiry confirmation dialog for proxy to parent user account switching ([#10152](https://github.com/linode/manager/pull/10152)) +- Clean up files to use profile to get `user_type` ([#10102](https://github.com/linode/manager/pull/10102)) +- Update components and unit tests to use `"default"` `user_type` for non-parent/child/proxy users ([#10176](https://github.com/linode/manager/pull/10176)) +- Use infinite query for fetching child accounts ([#10179](https://github.com/linode/manager/pull/10179)) +- Use API filtering on user_type to populate the two Users & Grants tables ([#10192](https://github.com/linode/manager/pull/10192)) + ## [2024-02-13] - v1.112.0 ### Added: @@ -26,6 +78,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Display $0.00 prices in Linode Migration dialog ([#10166](https://github.com/linode/manager/pull/10166)) ## [2024-02-05] - v1.111.0 + ### Changed: - Table CollapsibleRow icon orientation ([#10119](https://github.com/linode/manager/pull/10119)) @@ -82,25 +135,26 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Add Placement Groups Landing Page ([#10068](https://github.com/linode/manager/pull/10068)) - Add Placement Groups Landing Page empty state ([#10075](https://github.com/linode/manager/pull/10075)) - ## [2024-01-31] - v1.110.3 ### Fix: + - Enable `Can add VPCs to this account` for user permissions ## [2024-01-31] - v1.110.2 ### Changed: + - Remove VPC beta feedback link ## [2024-01-31] - v1.110.1 ### Changed: + - Updated VPC flag for primary navigation ## [2024-01-22] - v1.110.0 - ### Added: - Subnet IPv4 range recommendation in VPC Create flow and Subnet Create Drawer ([#10010](https://github.com/linode/manager/pull/10010)) @@ -152,7 +206,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Add AGLB Endpoint Health ([#10008](https://github.com/linode/manager/pull/10008)) - Add child account access column and disable delete account button when account has child accounts ([#10025](https://github.com/linode/manager/pull/10025)) -- Add parent/proxy 'Switch Account' button and drawer to user profile dropdown menu ([#10031](https://github.com/linode/manager/pull/10031)) +- Add parent/proxy 'Switch Account' button and drawer to user profile dropdown menu ([#10031](https://github.com/linode/manager/pull/10031)) - Disable Contact / Billing Info for Restricted Users ([#10036](https://github.com/linode/manager/pull/10036)) - Disable Billing Access user permission for child accounts ([#10045](https://github.com/linode/manager/pull/10045)) - Fix AGLB Configuration "Save" button remaining disabled when trying to remove a route ([#10048](https://github.com/linode/manager/pull/10048)) @@ -175,10 +229,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Remove obsolete VPC disabled state tests ([#10047](https://github.com/linode/manager/pull/10047)) - ## [2024-01-08] - v1.109.0 - ### Changed: - Improve layout of User Permissions page ([#10005](https://github.com/linode/manager/pull/10005)) @@ -231,7 +283,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Add `child_account` oauth scope to Personal Access Token drawers ([#9992](https://github.com/linode/manager/pull/9992)) - Add AGLB Routes section of full create page ([#9997](https://github.com/linode/manager/pull/9997)) - ## [2023-12-11] - v1.108.0 ### Added: @@ -244,10 +295,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Improve MainConcept Transcoders Marketplace app name, description, and website ([#9858](https://github.com/linode/manager/pull/9858)) - Move Linode Details Add/Edit Config button alignment to the right ([#9925](https://github.com/linode/manager/pull/9925)) -- Add pricing Docs Link to create/clone flows and remove DC-specific pricing warning notice ([#9946](https://github.com/linode/manager/pull/9946)) +- Add pricing Docs Link to create/clone flows and remove DC-specific pricing warning notice ([#9946](https://github.com/linode/manager/pull/9946)) - Update MainConcept app names to include “Demo” ([#9950](https://github.com/linode/manager/pull/9950)) - ### Fixed: - Overflow for VPC and StackScript detail descriptions and cut off placeholder text in VPC search bar ([#9887](https://github.com/linode/manager/pull/9887)) @@ -324,7 +374,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [2023-11-13] - v1.107.0 - ### Changed: - Logic governing inclusion of public interfaces in Linode Create payload ([#9834](https://github.com/linode/manager/pull/9834)) @@ -332,7 +381,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Logic governing display of Network Interfaces/Networking section in Linode Config dialog ([#9868](https://github.com/linode/manager/pull/9868)) - Temporarily remove region sorting on DBaaS landing page ([#9861](https://github.com/linode/manager/pull/9861)) - ### Fixed: - Linodes Landing flickering ([#9836](https://github.com/linode/manager/pull/9836)) @@ -356,7 +404,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Add integration tests for AGLB certificate edit flow ([#9880](https://github.com/linode/manager/pull/9880)) - Add integration tests for AGLB certificate delete flow ([#9846](https://github.com/linode/manager/pull/9846)) - ### Upcoming Features: - Fix Unassign multiple Linodes from Subnet ([#9820](https://github.com/linode/manager/pull/9820)) @@ -380,7 +427,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - AGLB Configurations Add Route Drawer and other refinements ([#9853](https://github.com/linode/manager/pull/9853)) - Add missing label field validation in AGLB Edit Certificate drawer ([#9880](https://github.com/linode/manager/pull/9880)) - ## [2023-10-30] - v1.106.0 ### Added: diff --git a/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts b/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts new file mode 100644 index 00000000000..6b213f48bf4 --- /dev/null +++ b/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts @@ -0,0 +1,171 @@ +/** + * @file Integration tests for Cloud Manager account login history flows. + */ + +import { profileFactory } from 'src/factories'; +import { accountLoginFactory } from 'src/factories/accountLogin'; +import { formatDate } from 'src/utilities/formatDate'; +import { mockGetAccountLogins } from 'support/intercepts/account'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { mockGetProfile } from 'support/intercepts/profile'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; + +describe('Account login history', () => { + /* + * - Confirms that a user can navigate to and view the login history page. + * - Confirms that login table displays the expected column headers. + * - Confirms that the login table displays a mocked failed restricted user login. + * - Confirm that the login table displays a mocked successful unrestricted user login. + */ + it('users can view the login history table', () => { + const mockProfile = profileFactory.build({ + username: 'mock-user', + restricted: false, + user_type: 'default', + }); + const mockFailedLogin = accountLoginFactory.build({ + status: 'failed', + username: 'mock-restricted-user', + restricted: true, + }); + const mockSuccessfulLogin = accountLoginFactory.build({ + status: 'successful', + restricted: false, + }); + + mockGetProfile(mockProfile).as('getProfile'); + mockGetAccountLogins([mockFailedLogin, mockSuccessfulLogin]).as( + 'getAccountLogins' + ); + + // TODO: Parent/Child - M3-7559 clean up when feature is live in prod and feature flag is removed. + mockAppendFeatureFlags({ + parentChildAccountAccess: makeFeatureFlagData(false), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + + // Navigate to Account Login History page. + cy.visitWithLogin('/account/login-history'); + cy.wait(['@getClientStream', '@getFeatureFlags', '@getProfile']); + + // Confirm helper text above table is visible. + cy.findByText( + 'Logins across all users on your account over the last 90 days.' + ).should('be.visible'); + + // Confirm the login table includes the expected column headers and mocked logins are visible in table. + cy.findByLabelText('Account Logins').within(() => { + cy.get('thead').findByText('Date').should('be.visible'); + cy.get('thead').findByText('Username').should('be.visible'); + cy.get('thead').findByText('IP').should('be.visible'); + cy.get('thead').findByText('Permission Level').should('be.visible'); + cy.get('thead').findByText('Access').should('be.visible'); + + // Confirm that restricted user's failed login and status icon display in table. + cy.findByText(mockFailedLogin.username) + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByText(mockFailedLogin.status, { exact: false }).should( + 'be.visible' + ); + cy.findAllByLabelText(`Status is ${mockFailedLogin.status}`); + cy.findByText('Restricted').should('be.visible'); + }); + + // Confirm that unrestricted user login displays in table. + cy.findByText(mockSuccessfulLogin.username) + .should('be.visible') + .closest('tr') + .within(() => { + // Confirm that successful login and status icon display in table. + cy.findByText(mockSuccessfulLogin.status, { exact: false }).should( + 'be.visible' + ); + cy.findAllByLabelText(`Status is ${mockSuccessfulLogin.status}`); + + // Confirm all other fields display in table. + cy.findByText( + formatDate(mockSuccessfulLogin.datetime, { + timezone: mockProfile.timezone, + }) + ).should('be.visible'); + cy.findByText(mockSuccessfulLogin.ip).should('be.visible'); + cy.findByText('Unrestricted').should('be.visible'); + }); + }); + }); + + /** + * - Confirms that a child user can navigate to the Login History page. + * - Confirms that a child user cannot see login history data. + * - Confirms that a child user sees a notice instead. + */ + it('child users cannot view login history', () => { + const mockProfile = profileFactory.build({ + username: 'mock-child-user', + restricted: false, + user_type: 'child', + }); + + mockGetProfile(mockProfile).as('getProfile'); + + // TODO: Parent/Child - M3-7559 clean up when feature is live in prod and feature flag is removed. + mockAppendFeatureFlags({ + parentChildAccountAccess: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + + // Navigate to Account Login History page. + cy.visitWithLogin('/account/login-history'); + cy.wait(['@getClientStream', '@getFeatureFlags', '@getProfile']); + + // Confirm helper text above table and table are not visible. + cy.findByText( + 'Logins across all users on your account over the last 90 days.' + ).should('not.exist'); + cy.findByLabelText('Account Logins').should('not.exist'); + + cy.findByText( + 'Access restricted. Please contact your business partner to request the necessary permission.' + ); + }); + + /** + * - Confirms that a restricted user can navigate to the Login History page. + * - Confirms that a restricted user cannot see login history data. + * - Confirms that a restricted user sees a notice instead. + */ + it('restricted users cannot view login history', () => { + const mockProfile = profileFactory.build({ + username: 'mock-restricted-user', + restricted: true, + user_type: 'default', + }); + + mockGetProfile(mockProfile).as('getProfile'); + + // TODO: Parent/Child - M3-7559 clean up when feature is live in prod and feature flag is removed. + mockAppendFeatureFlags({ + parentChildAccountAccess: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + + // Navigate to Account Login History page. + cy.visitWithLogin('/account/login-history'); + cy.wait(['@getClientStream', '@getFeatureFlags', '@getProfile']); + + // Confirm helper text above table and table are not visible. + cy.findByText( + 'Logins across all users on your account over the last 90 days.' + ).should('not.exist'); + cy.findByLabelText('Account Logins').should('not.exist'); + + cy.findByText( + 'Access restricted. Please contact your account administrator to request the necessary permission.' + ); + }); +}); diff --git a/packages/manager/cypress/e2e/core/account/change-username.spec.ts b/packages/manager/cypress/e2e/core/account/change-username.spec.ts index a2323877ade..a0374fc698f 100644 --- a/packages/manager/cypress/e2e/core/account/change-username.spec.ts +++ b/packages/manager/cypress/e2e/core/account/change-username.spec.ts @@ -1,3 +1,11 @@ +import { Profile } from '@linode/api-v4'; +import { profileFactory } from '@src/factories'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { mockGetProfile } from 'support/intercepts/profile'; import { getProfile } from 'support/api/account'; import { interceptGetProfile } from 'support/intercepts/profile'; import { @@ -7,6 +15,50 @@ import { import { ui } from 'support/ui'; import { randomString } from 'support/util/random'; +const verifyUsernameAndEmail = ( + mockRestrictedProxyProfile: Profile, + tooltip: string, + checkEmail: boolean +) => { + // TODO: Parent/Child - M3-7559 clean up when feature is live in prod and feature flag is removed. + mockAppendFeatureFlags({ + parentChildAccountAccess: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + + mockGetProfile(mockRestrictedProxyProfile); + + // Navigate to User Profile page + cy.visitWithLogin('/profile/display'); + + // Confirm the username and email address fields are disabled, as well their respective save buttons + cy.get('[id="username"]').should('be.disabled'); + ui.button + .findByTitle('Update Username') + .should('be.visible') + .should('be.disabled') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown + ui.tooltip.findByText(tooltip).should('be.visible'); + + // Refresh the page + mockGetProfile(mockRestrictedProxyProfile); + cy.reload(); + + if (checkEmail) { + cy.get('[id="email"]').should('be.disabled'); + ui.button + .findByTitle('Update Email') + .should('be.visible') + .should('be.disabled') + .trigger('mouseover'); + // Click the button first, then confirm the tooltip is shown + ui.tooltip + .findByText('This account type cannot update this field.') + .should('be.visible'); + } +}; + describe('username', () => { /* * - Validates username update flow via the user profile page using mocked data. @@ -96,4 +148,45 @@ describe('username', () => { cy.findByText('Username updated successfully.').should('be.visible'); }); }); + + it('disables username/email fields for restricted proxy user', () => { + const mockRestrictedProxyProfile = profileFactory.build({ + username: 'restricted-proxy-user', + user_type: 'proxy', + restricted: true, + }); + + verifyUsernameAndEmail( + mockRestrictedProxyProfile, + 'This account type cannot update this field.', + true + ); + }); + + it('disables username/email fields for unrestricted proxy user', () => { + const mockUnrestrictedProxyProfile = profileFactory.build({ + username: 'unrestricted-proxy-user', + user_type: 'proxy', + }); + + verifyUsernameAndEmail( + mockUnrestrictedProxyProfile, + 'This account type cannot update this field.', + true + ); + }); + + it('disables username/email fields for regular restricted user', () => { + const mockRegularRestrictedProfile = profileFactory.build({ + username: 'regular-restricted-user', + user_type: 'default', + restricted: true, + }); + + verifyUsernameAndEmail( + mockRegularRestrictedProfile, + 'Restricted users cannot update their username. Please contact an account administrator.', + false + ); + }); }); diff --git a/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts b/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts index be8fd90997c..0db765463af 100644 --- a/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts @@ -1,6 +1,7 @@ import { mockGetAccount, mockUpdateAccount } from 'support/intercepts/account'; import { accountFactory } from 'src/factories/account'; import type { Account } from '@linode/api-v4'; +import { ui } from 'support/ui'; /* eslint-disable sonarjs/no-duplicate-string */ const accountData = accountFactory.build({ @@ -66,75 +67,82 @@ describe('Billing Contact', () => { // mock the user's account data and confirm that it is displayed correctly upon page load mockGetAccount(accountData).as('getAccount'); cy.visitWithLogin('/account/billing'); - checkAccountContactDisplay(accountData); // edit the billing contact information mockUpdateAccount(newAccountData).as('updateAccount'); cy.get('[data-qa-contact-summary]').within((_contact) => { + checkAccountContactDisplay(accountData); cy.findByText('Edit').should('be.visible').click(); }); - // check drawer is visible - cy.findByLabelText('First Name') - .should('be.visible') - .click() - .clear() - .type(newAccountData['first_name']); - cy.findByLabelText('Last Name') - .should('be.visible') - .click() - .clear() - .type(newAccountData['last_name']); - cy.findByLabelText('Company Name') - .should('be.visible') - .click() - .clear() - .type(newAccountData['company']); - cy.findByLabelText('Address') - .should('be.visible') - .click() - .clear() - .type(newAccountData['address_1']); - cy.findByLabelText('Address 2') - .should('be.visible') - .click() - .clear() - .type(newAccountData['address_2']); - cy.findByLabelText('Email (required)') - .should('be.visible') - .click() - .clear() - .type(newAccountData['email']); - cy.findByLabelText('City') - .should('be.visible') - .click() - .clear() - .type(newAccountData['city']); - cy.findByLabelText('Postal Code') - .should('be.visible') - .click() - .clear() - .type(newAccountData['zip']); - cy.findByLabelText('Phone') - .should('be.visible') - .click() - .clear() - .type(newAccountData['phone']); - cy.get('[data-qa-contact-country]').click().type('United States{enter}'); - cy.get('[data-qa-contact-state-province]') - .should('be.visible') - .click() - .type(`${newAccountData['state']}{enter}`); - cy.findByLabelText('Tax ID') + + ui.drawer + .findByTitle('Edit Billing Contact Info') .should('be.visible') - .click() - .clear() - .type(newAccountData['tax_id']); - cy.get('[data-qa-save-contact-info="true"]') - .click() - .then(() => { - cy.wait('@updateAccount').then((xhr) => { - expect(xhr.response?.body).to.eql(newAccountData); - }); + .within(() => { + cy.findByLabelText('First Name') + .should('be.visible') + .click() + .clear() + .type(newAccountData['first_name']); + cy.findByLabelText('Last Name') + .should('be.visible') + .click() + .clear() + .type(newAccountData['last_name']); + cy.findByLabelText('Company Name') + .should('be.visible') + .click() + .clear() + .type(newAccountData['company']); + cy.findByLabelText('Address') + .should('be.visible') + .click() + .clear() + .type(newAccountData['address_1']); + cy.findByLabelText('Address 2') + .should('be.visible') + .click() + .clear() + .type(newAccountData['address_2']); + cy.findByLabelText('Email (required)') + .should('be.visible') + .click() + .clear() + .type(newAccountData['email']); + cy.findByLabelText('City') + .should('be.visible') + .click() + .clear() + .type(newAccountData['city']); + cy.findByLabelText('Postal Code') + .should('be.visible') + .click() + .clear() + .type(newAccountData['zip']); + cy.findByLabelText('Phone') + .should('be.visible') + .click() + .clear() + .type(newAccountData['phone']); + cy.get('[data-qa-contact-country]') + .click() + .type('United States{enter}'); + cy.get('[data-qa-contact-state-province]') + .should('be.visible') + .click() + .type(`${newAccountData['state']}{enter}`); + cy.findByLabelText('Tax ID') + .should('be.visible') + .click() + .clear() + .type(newAccountData['tax_id']); + cy.get('[data-qa-save-contact-info="true"]') + .click() + .then(() => { + cy.wait('@updateAccount').then((xhr) => { + expect(xhr.response?.body).to.eql(newAccountData); + }); + }); }); // check the page updates to reflect the edits diff --git a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts index 5a17dd052d0..8c6594b7f46 100644 --- a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts @@ -208,7 +208,7 @@ describe('restricted user billing flows', () => { const mockUser = accountUserFactory.build({ username: mockProfile.username, - user_type: null, + user_type: 'default', restricted: false, }); @@ -248,7 +248,7 @@ describe('restricted user billing flows', () => { const mockUser = accountUserFactory.build({ username: mockProfile.username, restricted: true, - user_type: null, + user_type: 'default', }); const mockGrants = grantsFactory.build({ @@ -304,7 +304,7 @@ describe('restricted user billing flows', () => { const mockUserRegular = accountUserFactory.build({ username: mockProfileRegular.username, - user_type: null, + user_type: 'default', restricted: false, }); diff --git a/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-summary.spec.ts b/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-summary.spec.ts index 1cb60363985..4eb6a35be71 100644 --- a/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-summary.spec.ts +++ b/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-summary.spec.ts @@ -2,7 +2,7 @@ * @file Integration tests for Akamai Cloud Load Balancer summary page. */ -import { loadbalancerFactory, configurationFactory } from '@src/factories/aglb'; +import { loadbalancerFactory, configurationFactory } from '@src/factories/aclb'; import { mockAppendFeatureFlags, mockGetFeatureFlagClientstream, diff --git a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts index ac17fe65c95..cc1f74b33b7 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts @@ -2,7 +2,10 @@ * @file Smoke tests for crucial Object Storage Access Keys operations. */ -import { objectStorageKeyFactory } from 'src/factories/objectStorage'; +import { + objectStorageKeyFactory, + objectStorageBucketFactory, +} from 'src/factories/objectStorage'; import { mockAppendFeatureFlags, mockGetFeatureFlagClientstream, @@ -11,10 +14,21 @@ import { mockCreateAccessKey, mockDeleteAccessKey, mockGetAccessKeys, + mockGetBucketsForRegion, + mockUpdateAccessKey, } from 'support/intercepts/object-storage'; import { makeFeatureFlagData } from 'support/util/feature-flags'; -import { randomLabel, randomNumber, randomString } from 'support/util/random'; +import { + randomDomainName, + randomLabel, + randomNumber, + randomString, +} from 'support/util/random'; import { ui } from 'support/ui'; +import { regionFactory } from 'src/factories'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { buildArray } from 'support/util/arrays'; +import { Scope } from '@linode/api-v4'; describe('object storage access keys smoke tests', () => { /* @@ -132,4 +146,386 @@ describe('object storage access keys smoke tests', () => { cy.wait(['@deleteKey', '@getKeys']); cy.findByText('No items to display.').should('be.visible'); }); + + describe('Object Storage Multicluster feature enabled', () => { + const mockRegionsObj = buildArray(3, () => { + return regionFactory.build({ + id: `us-${randomString(5)}`, + label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], + }); + }); + + const mockRegionsNoObj = regionFactory.buildList(3, { + capabilities: [], + }); + + const mockRegions = [...mockRegionsObj, ...mockRegionsNoObj]; + + beforeEach(() => { + mockAppendFeatureFlags({ + objMultiCluster: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + + /* + * - Confirms user can create access keys with unlimited access when OBJ Multicluster is enabled. + * - Confirms multiple regions can be selected when creating an access key. + * - Confirms that UI updates to reflect created access key. + */ + it('can create unlimited access keys with OBJ Multicluster', () => { + const mockAccessKey = objectStorageKeyFactory.build({ + id: randomNumber(10000, 99999), + label: randomLabel(), + access_key: randomString(20), + secret_key: randomString(39), + regions: mockRegionsObj.map((mockObjRegion) => ({ + id: mockObjRegion.id, + s3_endpoint: randomDomainName(), + })), + }); + + mockGetAccessKeys([]); + mockCreateAccessKey(mockAccessKey).as('createAccessKey'); + mockGetRegions(mockRegions); + mockRegions.forEach((region) => { + mockGetBucketsForRegion(region.id, []); + }); + + cy.visitWithLogin('/object-storage/access-keys'); + + ui.button + .findByTitle('Create Access Key') + .should('be.visible') + .should('be.enabled') + .click(); + + mockGetAccessKeys([mockAccessKey]); + ui.drawer + .findByTitle('Create Access Key') + .should('be.visible') + .within(() => { + cy.contains('Label (required)') + .should('be.visible') + .click() + .type(mockAccessKey.label); + + cy.contains('Regions (required)').should('be.visible').click(); + + // Select each region with the OBJ capability. + mockRegionsObj.forEach((mockRegion) => { + cy.contains('Regions (required)').type(mockRegion.label); + ui.autocompletePopper + .findByTitle(`${mockRegion.label} (${mockRegion.id})`) + .should('be.visible') + .click(); + }); + + // Close the regions drop-down. + cy.contains('Regions (required)') + .should('be.visible') + .click() + .type('{esc}'); + + // TODO Confirm expected regions are shown. + ui.buttonGroup + .findButtonByTitle('Create Access Key') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@createAccessKey'); + ui.dialog + .findByTitle('Access Keys') + .should('be.visible') + .within(() => { + // TODO Add assertions for S3 hostnames + cy.get('input[id="access-key"]') + .should('be.visible') + .should('have.value', mockAccessKey.access_key); + cy.get('input[id="secret-key"]') + .should('be.visible') + .should('have.value', mockAccessKey.secret_key); + + ui.button + .findByTitle('I Have Saved My Secret Key') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.findByText(mockAccessKey.label) + .should('be.visible') + .closest('tr') + .within(() => { + // TODO Add assertions for regions/S3 hostnames + cy.findByText(mockAccessKey.access_key).should('be.visible'); + }); + }); + + /* + * - COnfirms user can create access keys with limited access when OBJ Multicluster is enabled. + * - Confirms that UI updates to reflect created access key. + * - Confirms that "Permissions" drawer contains expected scope and permission data. + */ + it('can create limited access keys with OBJ Multicluster', () => { + const mockRegion = regionFactory.build({ + id: `us-${randomString(5)}`, + label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], + }); + + const mockBuckets = objectStorageBucketFactory.buildList(2, { + region: mockRegion.id, + cluster: undefined, + }); + + const mockAccessKey = objectStorageKeyFactory.build({ + id: randomNumber(10000, 99999), + label: randomLabel(), + access_key: randomString(20), + secret_key: randomString(39), + regions: [ + { + id: mockRegion.id, + s3_endpoint: randomDomainName(), + }, + ], + limited: true, + bucket_access: mockBuckets.map( + (bucket): Scope => ({ + bucket_name: bucket.label, + cluster: '', + permissions: 'read_only', + region: mockRegion.id, + }) + ), + }); + + mockGetAccessKeys([]); + mockCreateAccessKey(mockAccessKey).as('createAccessKey'); + mockGetRegions([mockRegion]); + mockGetBucketsForRegion(mockRegion.id, mockBuckets); + + // Navigate to access keys page, click "Create Access Key" button. + cy.visitWithLogin('/object-storage/access-keys'); + ui.button + .findByTitle('Create Access Key') + .should('be.visible') + .should('be.enabled') + .click(); + + // Fill out form in "Create Access Key" drawer. + ui.drawer + .findByTitle('Create Access Key') + .should('be.visible') + .within(() => { + cy.contains('Label (required)') + .should('be.visible') + .click() + .type(mockAccessKey.label); + + cy.contains('Regions (required)') + .should('be.visible') + .click() + .type(`${mockRegion.label}{enter}`); + + ui.autocompletePopper + .findByTitle(`${mockRegion.label} (${mockRegion.id})`) + .should('be.visible'); + + // Dismiss region drop-down. + cy.contains('Regions (required)') + .should('be.visible') + .click() + .type('{esc}'); + + // Enable "Limited Access" toggle for access key, and select access rules. + cy.findByText('Limited Access').should('be.visible').click(); + + mockBuckets.forEach((mockBucket) => { + cy.findByText(mockBucket.label) + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByLabelText( + `read-only for ${mockRegion.id}-${mockBucket.label}` + ) + .should('be.enabled') + .click(); + }); + }); + + mockGetAccessKeys([mockAccessKey]); + ui.buttonGroup + .findButtonByTitle('Create Access Key') + .should('be.enabled') + .click(); + }); + + // Dismiss secrets dialog. + cy.wait('@createAccessKey'); + ui.buttonGroup + .findButtonByTitle('I Have Saved My Secret Key') + .should('be.visible') + .should('be.enabled') + .click(); + + // Open "Permissions" drawer for new access key. + cy.findByText(mockAccessKey.label) + .should('be.visible') + .closest('tr') + .within(() => { + ui.actionMenu + .findByTitle( + `Action menu for Object Storage Key ${mockAccessKey.label}` + ) + .should('be.visible') + .click(); + }); + + ui.actionMenuItem.findByTitle('Permissions').click(); + ui.drawer + .findByTitle(`Permissions for ${mockAccessKey.label}`) + .should('be.visible') + .within(() => { + mockBuckets.forEach((mockBucket) => { + // TODO M3-7733 Update this selector when ARIA label is fixed. + cy.findByLabelText( + `This token has read-only access for -${mockBucket.label}` + ); + }); + }); + }); + + /* + * - Confirms user can edit access key labels and regions when OBJ Multicluster is enabled. + * - Confirms that user can deselect regions via the region selection list. + * - Confirms that access keys landing page automatically updates to reflect edited access key. + */ + it('can update access keys with OBJ Multicluster', () => { + const mockInitialRegion = regionFactory.build({ + id: `us-${randomString(5)}`, + label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], + }); + + const mockUpdatedRegion = regionFactory.build({ + id: `us-${randomString(5)}`, + label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], + }); + + const mockRegions = [mockInitialRegion, mockUpdatedRegion]; + + const mockAccessKey = objectStorageKeyFactory.build({ + id: randomNumber(10000, 99999), + label: randomLabel(), + access_key: randomString(20), + secret_key: randomString(39), + regions: [ + { + id: mockInitialRegion.id, + s3_endpoint: randomDomainName(), + }, + ], + }); + + const mockUpdatedAccessKeyEndpoint = randomDomainName(); + + const mockUpdatedAccessKey = { + ...mockAccessKey, + label: randomLabel(), + regions: [ + { + id: mockUpdatedRegion.id, + s3_endpoint: mockUpdatedAccessKeyEndpoint, + }, + ], + }; + + mockGetAccessKeys([mockAccessKey]); + mockGetRegions(mockRegions); + cy.visitWithLogin('/object-storage/access-keys'); + + cy.findByText(mockAccessKey.label) + .should('be.visible') + .closest('tr') + .within(() => { + ui.actionMenu + .findByTitle( + `Action menu for Object Storage Key ${mockAccessKey.label}` + ) + .should('be.visible') + .click(); + }); + + ui.actionMenuItem + .findByTitle('Edit') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle('Edit Access Key') + .should('be.visible') + .within(() => { + cy.contains('Label (required)') + .should('be.visible') + .click() + .type('{selectall}{backspace}') + .type(mockUpdatedAccessKey.label); + + cy.contains('Regions (required)') + .should('be.visible') + .click() + .type(`${mockUpdatedRegion.label}{enter}{esc}`); + + cy.get('[data-qa-selection-list]') + .should('be.visible') + .within(() => { + // Confirm both regions are selected and present in selection list. + mockRegions.forEach((mockRegion) => { + cy.findByText(`${mockRegion.label} (${mockRegion.id})`).should( + 'be.visible' + ); + }); + + // Deselect initial region and confirm it's removed from list. + cy.findByLabelText( + `remove ${mockInitialRegion.label} (${mockInitialRegion.id})` + ) + .should('be.visible') + .should('be.enabled') + .click(); + + cy.findByText( + `${mockInitialRegion.label} (${mockInitialRegion.id})` + ).should('not.exist'); + }); + + mockUpdateAccessKey(mockUpdatedAccessKey).as('updateAccessKey'); + mockGetAccessKeys([mockUpdatedAccessKey]); + ui.button + .findByTitle('Save Changes') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@updateAccessKey'); + + // Confirm that access key landing page reflects updated key. + cy.findByText(mockAccessKey.label).should('not.exist'); + cy.findByText(mockUpdatedAccessKey.label) + .should('be.visible') + .closest('tr') + .within(() => { + cy.contains(mockUpdatedRegion.label).should('be.visible'); + cy.contains(mockUpdatedAccessKeyEndpoint).should('be.visible'); + }); + }); + }); }); diff --git a/packages/manager/cypress/support/intercepts/account.ts b/packages/manager/cypress/support/intercepts/account.ts index f8721963252..5eb29242169 100644 --- a/packages/manager/cypress/support/intercepts/account.ts +++ b/packages/manager/cypress/support/intercepts/account.ts @@ -10,6 +10,7 @@ import { makeResponse } from 'support/util/response'; import type { Account, + AccountLogin, AccountSettings, Agreements, CancelAccount, @@ -514,3 +515,19 @@ export const mockGetAccountAgreements = ( makeResponse(agreements) ); }; + +/** + * Intercepts GET request to fetch the account logins and mocks the response. + * + * + * @returns Cypress chainable. + */ +export const mockGetAccountLogins = ( + accountLogins: AccountLogin[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher(`account/logins*`), + paginateResponse(accountLogins) + ); +}; diff --git a/packages/manager/cypress/support/intercepts/load-balancers.ts b/packages/manager/cypress/support/intercepts/load-balancers.ts index 2ce86a625c8..3a33cd1849b 100644 --- a/packages/manager/cypress/support/intercepts/load-balancers.ts +++ b/packages/manager/cypress/support/intercepts/load-balancers.ts @@ -22,7 +22,7 @@ import type { export const mockGetLoadBalancer = (loadBalancer: Loadbalancer) => { return cy.intercept( 'GET', - apiMatcher(`/aglb/${loadBalancer.id}`), + apiMatcher(`/aclb/${loadBalancer.id}`), makeResponse(loadBalancer) ); }; @@ -37,7 +37,7 @@ export const mockGetLoadBalancer = (loadBalancer: Loadbalancer) => { export const mockGetLoadBalancers = (loadBalancers: Loadbalancer[]) => { return cy.intercept( 'GET', - apiMatcher('/aglb*'), + apiMatcher('/aclb*'), paginateResponse(loadBalancers) ); }; @@ -50,7 +50,7 @@ export const mockGetLoadBalancers = (loadBalancers: Loadbalancer[]) => { * @returns Cypress chainable. */ export const mockDeleteLoadBalancer = (loadBalancerId: number) => { - return cy.intercept('DELETE', apiMatcher(`/aglb/${loadBalancerId}`), {}); + return cy.intercept('DELETE', apiMatcher(`/aclb/${loadBalancerId}`), {}); }; /** @@ -68,7 +68,7 @@ export const mockDeleteLoadBalancerError = ( const defaultMessage = 'An error occurred while deleting Load Balancer.'; return cy.intercept( 'DELETE', - apiMatcher(`/aglb/${loadBalancerId}`), + apiMatcher(`/aclb/${loadBalancerId}`), makeErrorResponse(message ?? defaultMessage, 500) ); }; @@ -87,7 +87,7 @@ export const mockGetLoadBalancerConfigurations = ( ) => { return cy.intercept( 'GET', - apiMatcher(`/aglb/${loadBalancerId}/configurations*`), + apiMatcher(`/aclb/${loadBalancerId}/configurations*`), paginateResponse(configurations) ); }; @@ -106,7 +106,7 @@ export const mockDeleteLoadBalancerConfiguration = ( ) => { return cy.intercept( 'DELETE', - apiMatcher(`/aglb/${loadBalancerId}/configurations/${configId}`), + apiMatcher(`/aclb/${loadBalancerId}/configurations/${configId}`), {} ); }; @@ -126,7 +126,7 @@ export const mockDeleteLoadBalancerConfigurationError = ( ) => { return cy.intercept( 'DELETE', - apiMatcher(`/aglb/${loadBalancerId}/configurations/${configId}`), + apiMatcher(`/aclb/${loadBalancerId}/configurations/${configId}`), makeResponse({ errors: [{ reason: error }] }, 500) ); }; @@ -145,7 +145,7 @@ export const mockCreateLoadBalancerConfiguration = ( ) => { return cy.intercept( 'POST', - apiMatcher(`/aglb/${loadBalancerId}/configurations`), + apiMatcher(`/aclb/${loadBalancerId}/configurations`), makeResponse(configuration) ); }; @@ -164,7 +164,7 @@ export const mockUpdateLoadBalancerConfiguration = ( ) => { return cy.intercept( 'PUT', - apiMatcher(`/aglb/${loadBalancerId}/configurations/${configuration.id}`), + apiMatcher(`/aclb/${loadBalancerId}/configurations/${configuration.id}`), makeResponse(configuration) ); }; @@ -184,7 +184,7 @@ export const mockUpdateLoadBalancerConfigurationError = ( ) => { return cy.intercept( 'PUT', - apiMatcher(`/aglb/${loadBalancerId}/configurations/${configurationId}`), + apiMatcher(`/aclb/${loadBalancerId}/configurations/${configurationId}`), makeResponse({ errors }, 400) ); }; @@ -203,7 +203,7 @@ export const mockCreateLoadBalancerConfigurationError = ( ) => { return cy.intercept( 'POST', - apiMatcher(`/aglb/${loadBalancerId}/configurations`), + apiMatcher(`/aclb/${loadBalancerId}/configurations`), makeResponse({ errors }, 500) ); }; @@ -222,7 +222,7 @@ export const mockGetLoadBalancerCertificates = ( ) => { return cy.intercept( 'GET', - apiMatcher(`/aglb/${loadBalancerId}/certificates*`), + apiMatcher(`/aclb/${loadBalancerId}/certificates*`), paginateResponse(certificates) ); }; @@ -240,7 +240,7 @@ export const mockUploadLoadBalancerCertificate = ( ) => { return cy.intercept( 'POST', - apiMatcher(`/aglb/${loadBalancerId}/certificates`), + apiMatcher(`/aclb/${loadBalancerId}/certificates`), makeResponse(certificate) ); }; @@ -259,7 +259,7 @@ export const mockDeleteLoadBalancerCertificate = ( ) => { return cy.intercept( 'DELETE', - apiMatcher(`/aglb/${loadBalancerId}/certificates/${certificateId}`), + apiMatcher(`/aclb/${loadBalancerId}/certificates/${certificateId}`), makeResponse() ); }; @@ -282,7 +282,7 @@ export const mockDeleteLoadBalancerCertificateError = ( 'An error occurred while deleting Load Balancer certificate.'; return cy.intercept( 'DELETE', - apiMatcher(`/aglb/${loadBalancerId}/certificates/${certificateId}`), + apiMatcher(`/aclb/${loadBalancerId}/certificates/${certificateId}`), makeErrorResponse(message ?? defaultMessage, 500) ); }; @@ -300,7 +300,7 @@ export const mockUpdateLoadBalancerCertificate = ( ) => { return cy.intercept( 'PUT', - apiMatcher(`/aglb/${loadBalancerId}/certificates/${certificate.id}`), + apiMatcher(`/aclb/${loadBalancerId}/certificates/${certificate.id}`), makeResponse(certificate) ); }; @@ -319,7 +319,7 @@ export const mockGetServiceTargets = ( ) => { return cy.intercept( 'GET', - apiMatcher(`/aglb/${loadBalancer.id}/service-targets*`), + apiMatcher(`/aclb/${loadBalancer.id}/service-targets*`), paginateResponse(serviceTargets) ); }; @@ -335,7 +335,7 @@ export const mockGetServiceTargetsError = (message?: string) => { const defaultMessage = 'An error occurred while retrieving service targets'; return cy.intercept( 'GET', - apiMatcher('/aglb/service-targets*'), + apiMatcher('/aclb/service-targets*'), makeErrorResponse(message ?? defaultMessage, 500) ); }; @@ -354,7 +354,7 @@ export const mockCreateServiceTarget = ( ) => { return cy.intercept( 'POST', - apiMatcher(`/aglb/${loadBalancer.id}/service-targets`), + apiMatcher(`/aclb/${loadBalancer.id}/service-targets`), makeResponse(serviceTarget) ); }; @@ -373,7 +373,7 @@ export const mockUpdateServiceTarget = ( ) => { return cy.intercept( 'PUT', - apiMatcher(`/aglb/${loadBalancer.id}/service-targets/${serviceTarget.id}`), + apiMatcher(`/aclb/${loadBalancer.id}/service-targets/${serviceTarget.id}`), makeResponse(serviceTarget) ); }; @@ -389,7 +389,7 @@ export const mockUpdateServiceTarget = ( export const mockCreateRoute = (loadBalancer: Loadbalancer, route: Route) => { return cy.intercept( 'POST', - apiMatcher(`/aglb/${loadBalancer.id}/routes`), + apiMatcher(`/aclb/${loadBalancer.id}/routes`), makeResponse(route) ); }; @@ -408,7 +408,7 @@ export const mockGetLoadBalancerRoutes = ( ) => { return cy.intercept( 'GET', - apiMatcher(`/aglb/${loadBalancerId}/routes*`), + apiMatcher(`/aclb/${loadBalancerId}/routes*`), paginateResponse(routes) ); }; @@ -427,7 +427,7 @@ export const mockGetLoadBalancerServiceTargets = ( ) => { return cy.intercept( 'GET', - apiMatcher(`/aglb/${loadBalancerId}/service-targets*`), + apiMatcher(`/aclb/${loadBalancerId}/service-targets*`), paginateResponse(serviceTargets) ); }; @@ -443,7 +443,7 @@ export const mockGetLoadBalancerServiceTargets = ( export const mockUpdateRoute = (loadBalancer: Loadbalancer, route: Route) => { return cy.intercept( 'PUT', - apiMatcher(`/aglb/${loadBalancer.id}/routes/${route.id}`), + apiMatcher(`/aclb/${loadBalancer.id}/routes/${route.id}`), makeResponse(route) ); }; @@ -462,7 +462,7 @@ export const mockUpdateRouteError = ( ) => { return cy.intercept( 'PUT', - apiMatcher(`/aglb/${loadBalancer.id}/routes/${route.id}`), + apiMatcher(`/aclb/${loadBalancer.id}/routes/${route.id}`), makeResponse( { errors: [ diff --git a/packages/manager/cypress/support/intercepts/object-storage.ts b/packages/manager/cypress/support/intercepts/object-storage.ts index 3996a18a3e3..f993da79d73 100644 --- a/packages/manager/cypress/support/intercepts/object-storage.ts +++ b/packages/manager/cypress/support/intercepts/object-storage.ts @@ -51,6 +51,25 @@ export const mockGetBuckets = ( ); }; +/** + * Intercepts GET request to fetch buckets for a region and mocks response. + * + * @param regionId - ID of region for which to mock buckets. + * @param buckets - Array of Bucket objects with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetBucketsForRegion = ( + regionId: string, + buckets: ObjectStorageBucket[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher(`object-storage/buckets/${regionId}*`), + paginateResponse(buckets) + ); +}; + /** * Intercepts POST request to create bucket. * @@ -352,6 +371,23 @@ export const mockCreateAccessKey = ( ); }; +/** + * Intercepts request to update an Object Storage Access Key and mocks response. + * + * @param updatedAccessKey - Access key with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockUpdateAccessKey = ( + updatedAccessKey: ObjectStorageKey +): Cypress.Chainable => { + return cy.intercept( + 'PUT', + apiMatcher(`object-storage/keys/${updatedAccessKey.id}`), + makeResponse(updatedAccessKey) + ); +}; + /** * Intercepts object storage access key DELETE request and mocks success response. * diff --git a/packages/manager/package.json b/packages/manager/package.json index 619861d2364..1f86cacdb8d 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.112.0", + "version": "1.113.0", "private": true, "type": "module", "bugs": { @@ -27,7 +27,6 @@ "axios": "~1.6.5", "braintree-web": "^3.92.2", "chart.js": "~2.9.4", - "chartjs-adapter-luxon": "^0.2.1", "copy-to-clipboard": "^3.0.8", "country-region-data": "^1.4.5", "flag-icons": "^6.6.5", @@ -39,11 +38,11 @@ "ipaddr.js": "^1.9.1", "jspdf": "^2.3.1", "jspdf-autotable": "^3.5.14", - "launchdarkly-react-client-sdk": "^3.0.6", + "launchdarkly-react-client-sdk": "3.0.10", "libphonenumber-js": "^1.10.6", "lodash": "^4.17.21", "logic-query-parser": "^0.0.5", - "luxon": "^3.2.1", + "luxon": "3.4.4", "markdown-it": "^12.3.2", "md5": "^2.2.1", "notistack": "^2.0.5", @@ -58,7 +57,7 @@ "react-number-format": "^3.5.0", "react-query": "^3.3.2", "react-redux": "~7.1.3", - "react-router-dom": "~5.1.2", + "react-router-dom": "~5.3.4", "react-router-hash-link": "^2.3.1", "react-select": "~3.1.0", "react-vnc": "^0.5.3", @@ -85,6 +84,7 @@ "start:ci": "yarn serve ./build -p 3000 -s --cors", "lint": "yarn run eslint . --ext .js,.ts,.tsx --quiet", "build": "node scripts/prebuild.mjs && vite build", + "build:analyze": "bunx vite-bundle-visualizer", "precommit": "lint-staged && yarn typecheck", "test": "vitest run", "test:debug": "node --inspect-brk scripts/test.js --runInBand", @@ -129,13 +129,12 @@ "@types/chai-string": "^1.4.5", "@types/chart.js": "^2.9.21", "@types/css-mediaquery": "^0.1.1", - "@types/enzyme": "^3.9.3", "@types/he": "^1.1.0", "@types/highlight.js": "~10.1.0", "@types/jest-axe": "^3.5.7", "@types/jsdom": "^21.1.4", "@types/jspdf": "^1.3.3", - "@types/luxon": "^3.2.0", + "@types/luxon": "3.4.2", "@types/markdown-it": "^10.0.2", "@types/md5": "^2.1.32", "@types/mocha": "^10.0.2", @@ -147,7 +146,7 @@ "@types/react-csv": "^1.1.3", "@types/react-dom": "^17.0.9", "@types/react-redux": "~7.1.7", - "@types/react-router-dom": "~5.1.2", + "@types/react-router-dom": "~5.3.3", "@types/react-router-hash-link": "^1.2.1", "@types/react-select": "^3.0.11", "@types/recompose": "^0.30.0", @@ -172,8 +171,6 @@ "cypress-real-events": "^1.11.0", "cypress-vite": "^1.5.0", "dotenv": "^16.0.3", - "enzyme": "^3.10.0", - "enzyme-adapter-react-16": "^1.14.0", "eslint": "^6.8.0", "eslint-config-prettier": "~8.1.0", "eslint-plugin-cypress": "^2.11.3", @@ -189,7 +186,6 @@ "eslint-plugin-storybook": "^0.6.15", "eslint-plugin-testing-library": "^3.1.2", "eslint-plugin-xss": "^0.1.10", - "simple-git": "^3.19.0", "factory.ts": "^0.5.1", "glob": "^10.3.1", "jest-axe": "^8.0.0", @@ -199,9 +195,11 @@ "mocha-junit-reporter": "^2.2.1", "msw": "~1.3.2", "prettier": "~2.2.1", + "react-test-renderer": "16.14.0", "redux-mock-store": "^1.5.3", "reselect-tools": "^0.0.7", "serve": "^14.0.1", + "simple-git": "^3.19.0", "storybook": "^7.6.10", "storybook-dark-mode": "^3.0.3", "ts-node": "^10.9.2", diff --git a/packages/manager/public/assets/jupyter.svg b/packages/manager/public/assets/jupyter.svg new file mode 100644 index 00000000000..959b69e5299 --- /dev/null +++ b/packages/manager/public/assets/jupyter.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/nats.svg b/packages/manager/public/assets/nats.svg new file mode 100644 index 00000000000..d9f01a213ac --- /dev/null +++ b/packages/manager/public/assets/nats.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/white/jupyter.svg b/packages/manager/public/assets/white/jupyter.svg new file mode 100644 index 00000000000..bf2c5ca816d --- /dev/null +++ b/packages/manager/public/assets/white/jupyter.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/white/nats.svg b/packages/manager/public/assets/white/nats.svg new file mode 100644 index 00000000000..246e6d1e4cf --- /dev/null +++ b/packages/manager/public/assets/white/nats.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/packages/manager/src/App.tsx b/packages/manager/src/App.tsx index 1247e6972c0..04c31a7aff0 100644 --- a/packages/manager/src/App.tsx +++ b/packages/manager/src/App.tsx @@ -6,7 +6,6 @@ import { DocumentTitleSegment, withDocumentTitleProvider, } from 'src/components/DocumentTitle'; -import withFeatureFlagConsumer from 'src/containers/withFeatureFlagConsumer.container'; import withFeatureFlagProvider from 'src/containers/withFeatureFlagProvider.container'; import TheApplicationIsOnFire from 'src/features/TheApplicationIsOnFire'; @@ -23,37 +22,35 @@ import { useSetupFeatureFlags } from './useSetupFeatureFlags'; export const App = () => ; const BaseApp = withDocumentTitleProvider( - withFeatureFlagProvider( - withFeatureFlagConsumer(() => { - const { isLoading } = useInitialRequests(); - - const { areFeatureFlagsLoading } = useSetupFeatureFlags(); - - if (isLoading || areFeatureFlagsLoading) { - return ; - } - - return ( - }> - {/** Accessibility helper */} - - Skip to main content - - - - - - - - ); - }) - ) + withFeatureFlagProvider(() => { + const { isLoading } = useInitialRequests(); + + const { areFeatureFlagsLoading } = useSetupFeatureFlags(); + + if (isLoading || areFeatureFlagsLoading) { + return ; + } + + return ( + }> + {/** Accessibility helper */} + + Skip to main content + + + + + + + + ); + }) ); const GlobalListeners = () => { diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 5d11ba2b26d..0d762133050 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -29,6 +29,7 @@ import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { ENABLE_MAINTENANCE_MODE } from './constants'; import { complianceUpdateContext } from './context/complianceUpdateContext'; +import { switchAccountSessionContext } from './context/switchAccountSessionContext'; import { FlagSet } from './featureFlags'; import { useIsACLBEnabled } from './features/LoadBalancers/utils'; import { useGlobalErrors } from './hooks/useGlobalErrors'; @@ -192,6 +193,11 @@ export const MainContent = () => { const ComplianceUpdateProvider = complianceUpdateContext.Provider; const complianceUpdateContextValue = useDialogContext(); + const SwitchAccountSessionProvider = switchAccountSessionContext.Provider; + const switchAccountSessionContextValue = useDialogContext({ + isOpen: false, + }); + const [menuIsOpen, toggleMenu] = React.useState(false); const { _isManagedAccount, @@ -294,100 +300,105 @@ export const MainContent = () => { */ return (
- - - <> - {shouldDisplayMainContentBanner ? ( - setBannerDismissed(true)} - url={flags.mainContentBanner?.link?.url ?? ''} - /> - ) : null} - toggleMenu(false)} - collapse={desktopMenuIsOpen || false} - open={menuIsOpen} - /> -
- toggleMenu(true)} - username={username} + + + + <> + {shouldDisplayMainContentBanner ? ( + setBannerDismissed(true)} + url={flags.mainContentBanner?.link?.url ?? ''} + /> + ) : null} + toggleMenu(false)} + collapse={desktopMenuIsOpen || false} + open={menuIsOpen} /> -
- - - - }> - - - - - - {isACLBEnabled && ( + toggleMenu(true)} + username={username} + /> +
+ + + + }> + + + + + + {isACLBEnabled && ( + + )} + + + + + + - )} - - - - - - - - - - - - - - - {showDatabases && ( - - )} - {flags.selfServeBetas && ( - - )} - {showVPCs && } - - {/** We don't want to break any bookmarks. This can probably be removed eventually. */} - - - - + + + + + + + + {showDatabases && ( + + )} + {flags.selfServeBetas && ( + + )} + {showVPCs && } + + {/** We don't want to break any bookmarks. This can probably be removed eventually. */} + + + + + - -
-
- -
-
- + +
+ + +