Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [M3-7083 & M3-7175] MarketPlace regex fix + end to end coverage #9704

Merged
merged 16 commits into from
Sep 28, 2023
Merged

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/manager/cypress/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"moduleResolution": "node",
"lib": ["es6", "dom"],
"baseUrl": "..",
"jsx": "react",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was necessary to remove an import warning when importing a utility from a .tsx extension

"paths": {
"@src/*": ["src/*"],
"support/*": ["cypress/support/*"],
Expand Down
21 changes: 19 additions & 2 deletions packages/manager/src/factories/stackscripts.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
import * as Factory from 'factory.ts';

import type {
StackScript,
UserDefinedField,
} from '@linode/api-v4/lib/stackscripts/types';
import * as Factory from 'factory.ts';
import type { OCA } from 'src/features/OneClickApps/types';

export const stackScriptFactory = Factory.Sync.makeFactory<StackScript>({
created: '2010-12-31T23:59:58',
Expand All @@ -24,6 +26,21 @@ export const stackScriptFactory = Factory.Sync.makeFactory<StackScript>({
username: 'Linode',
});

export const oneClickAppFactory = Factory.Sync.makeFactory<OCA>({
alt_description: 'A test app',
alt_name: 'Test App',
categories: ['App Creators'],
colors: {
end: '#000000',
start: '#000000',
},
description: 'A test app',
logo_url: 'nodejs.svg',
name: 'Test App',
summary: 'A test app',
website: 'https://www.linode.com',
});

export const userDefinedFieldFactory = Factory.Sync.makeFactory<UserDefinedField>(
{
label: Factory.each((i) => `Field${i}`),
Expand Down
8 changes: 3 additions & 5 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { TPAProvider } from '@linode/api-v4/lib/profile';

import { NoticeVariant } from 'src/components/Notice/Notice';

import { Doc } from './features/OneClickApps/oneClickApps';
import type { Doc } from './features/OneClickApps/types';
import type { TPAProvider } from '@linode/api-v4/lib/profile';
import type { NoticeVariant } from 'src/components/Notice/Notice';

// These flags should correspond with active features flags in LD

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { StackScript, UserDefinedField } from '@linode/api-v4/lib/stackscripts';
import Grid from '@mui/material/Unstable_Grid2';
import { styled } from '@mui/material/styles';
import { decode } from 'he';
import * as React from 'react';

import { Box } from 'src/components/Box';
import { Chip } from 'src/components/Chip';
import { Divider } from 'src/components/Divider';
import { Typography } from 'src/components/Typography';
import { SelectionCardWrapper } from 'src/features/Linodes/LinodesCreate/SelectionCardWrapper';

import { handleAppLabel } from './utilities';

interface Props {
apps: StackScript[];
disabled: boolean;
Expand Down Expand Up @@ -41,24 +43,15 @@ export const AppPanelSection = (props: Props) => {
}

return (
<>
<Box data-testid={heading}>
<Typography variant="h2">{heading}</Typography>
{heading && heading.length > 0 ? (
<Divider spacingBottom={16} spacingTop={16} />
) : null}
{apps.length > 0 ? (
<AppPanelGrid container spacing={2}>
{apps.map((eachApp) => {
const decodedLabel = decode(eachApp.label);
const isCluster =
decodedLabel.endsWith('Cluster ') &&
eachApp.user_defined_fields.some(
(field) => field.name === 'cluster_size'
);

const label = isCluster
? decodedLabel.split(' Cluster')[0]
: decodedLabel;
const { decodedLabel, isCluster, label } = handleAppLabel(eachApp);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this to a util so it's reusable and testable in isolation.


return (
<SelectionCardWrapper
Expand Down Expand Up @@ -86,7 +79,7 @@ export const AppPanelSection = (props: Props) => {
{`Sorry, no results matching "${searchValue}" were found.`}
</Typography>
)}
</>
</Box>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ import { WithTypesProps } from 'src/containers/types.container';
import { FeatureFlagConsumerProps } from 'src/containers/withFeatureFlagConsumer.container';
import { WithLinodesProps } from 'src/containers/withLinodes.container';
import EUAgreementCheckbox from 'src/features/Account/Agreements/EUAgreementCheckbox';
import { regionSupportsMetadata } from 'src/features/Linodes/LinodesCreate/utilities';
import {
getMonthlyAndHourlyNodePricing,
utoa,
} from 'src/features/Linodes/LinodesCreate/utilities';
import { regionSupportsMetadata } from 'src/features/Linodes/LinodesCreate/utilities';
import { SMTPRestrictionText } from 'src/features/Linodes/SMTPRestrictionText';
import {
getCommunityStackscripts,
Expand All @@ -56,6 +56,7 @@ import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature
import { getErrorMap } from 'src/utilities/errorUtils';
import { extendType } from 'src/utilities/extendType';
import { filterCurrentTypes } from 'src/utilities/filterCurrentLinodeTypes';
import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups';
import { getQueryParamsFromQueryString } from 'src/utilities/queryParams';

import { AddonsPanel } from './AddonsPanel';
Expand Down Expand Up @@ -88,7 +89,6 @@ import {
} from './types';

import type { Tab } from 'src/components/TabLinkList/TabLinkList';
import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups';

export interface LinodeCreateProps {
checkValidation: LinodeCreateValidation;
Expand Down Expand Up @@ -220,6 +220,7 @@ export class LinodeCreate extends React.PureComponent<
const {
accountBackupsEnabled,
errors,
flags,
formIsSubmitting,
handleAgreementChange,
handleShowApiAwarenessModal,
Expand Down Expand Up @@ -404,6 +405,7 @@ export class LinodeCreate extends React.PureComponent<
// error={hasErrorFor.image}
accountBackupsEnabled={accountBackupsEnabled}
errors={errors}
flags={flags}
imagesData={imagesData!}
regionsData={regionsData!}
typesData={typesData!}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import { validatePassword } from 'src/utilities/validatePassword';
import LinodeCreate from './LinodeCreate';
import { deriveDefaultLabel } from './deriveDefaultLabel';
import { HandleSubmit, Info, LinodeCreateValidation, TypeInfo } from './types';
import { getRegionIDFromLinodeID } from './utilities';
import { filterOneClickApps, getRegionIDFromLinodeID } from './utilities';

import type {
CreateLinodeRequest,
Expand Down Expand Up @@ -175,13 +175,6 @@ const getDisabledClasses = (regionID: string, regions: Region[] = []) => {
return disabledClasses;
};

const trimOneClickFromLabel = (script: StackScript) => {
return {
...script,
label: script.label.replace('One-Click', ''),
};
};

const nonImageCreateTypes = ['fromStackScript', 'fromBackup', 'fromLinode'];

const isNonDefaultImageType = (prevType: string, type: string) => {
Expand All @@ -194,7 +187,6 @@ class LinodeCreateContainer extends React.PureComponent<CombinedProps, State> {
componentDidMount() {
// Allowed apps include the base set of original apps + anything LD tells us to show
const newApps = this.props.flags.oneClickApps || [];
const allowedApps = Object.keys({ ...baseApps, ...newApps });
if (nonImageCreateTypes.includes(this.props.createType)) {
// If we're navigating directly to e.g. the clone page, don't select an image by default
this.setState({ selectedImageID: undefined });
Expand All @@ -204,17 +196,12 @@ class LinodeCreateContainer extends React.PureComponent<CombinedProps, State> {
this.props.queryClient
.fetchQuery('stackscripts-oca-all', () => getAllOCAsRequest())
.then((res: StackScript[]) => {
// Don't display One-Click Helpers to the user
// Filter out any apps that we don't have info for
const filteredApps = res.filter((script) => {
return (
!script.label.match(/helpers/i) &&
allowedApps.includes(String(script.id))
);
const trimmedApps = filterOneClickApps({
baseApps,
newApps,
queryResults: res,
});
const trimmedApps = filteredApps.map((stackscript) =>
trimOneClickFromLabel(stackscript)
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Moved to a util. This helps testing in isolation and re-use as an assertion in the e2e test.

this.setState({
appInstances: trimmedApps,
appInstancesLoading: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { UserDefinedField } from '@linode/api-v4/lib/stackscripts';
import { Theme, styled } from '@mui/material/styles';
import * as React from 'react';
import { compose } from 'recompose';
Expand All @@ -11,11 +10,15 @@ import { AppPanelSection } from 'src/features/Linodes/LinodesCreate/AppPanelSect
import { getQueryParamFromQueryString } from 'src/utilities/queryParams';

import { Panel } from './Panel';
import { AppsData } from './types';

import type { AppsData } from './types';
import type { UserDefinedField } from '@linode/api-v4/lib/stackscripts';
import type { FlagSet } from 'src/featureFlags';

interface Props extends AppsData {
disabled: boolean;
error?: string;
flags: FlagSet;
handleClick: (
id: number,
label: string,
Expand Down Expand Up @@ -51,6 +54,7 @@ class SelectAppPanel extends React.PureComponent<Props> {
appInstancesLoading,
disabled,
error,
flags,
handleClick,
isFiltering,
isSearching,
Expand Down Expand Up @@ -81,11 +85,9 @@ class SelectAppPanel extends React.PureComponent<Props> {
return null;
}

const newAppsIds = Object.keys(flags.oneClickApps || {});
const newApps = appInstances.filter((app) => {
return [
'hashicorp nomad clients cluster',
'hashicorp nomad cluster',
].includes(app.label.toLowerCase().trim());
return newAppsIds.includes(app.id.toString());
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we were hardcoding those since they are already returning from the oneClickApps feature flag. This fix also makes the New Apps selection more robust since it references the Id rather than a label. This is the reason for passing the flags from the Class Component to the selection panel. (also a class component so no useFlag hook there...)

Confirmed with @HanaXu this was the right thing to do.

const popularApps = appInstances.slice(0, 10);
Expand All @@ -98,7 +100,10 @@ class SelectAppPanel extends React.PureComponent<Props> {
const isFilteringOrSearching = isFiltering || isSearching;

return (
<StyledPaper data-qa-tp="Select Image">
<StyledPaper
data-qa-tp="Select Image"
data-testid="one-click-apps-container"
>
{error && <Notice text={error} variant="error" />}
{!isFilteringOrSearching ? (
<AppPanelSection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const SelectionCardWrapper = (props: Props) => {
<InfoGrid xs={2}>
<Info
aria-label={`Info for "${label}"`}
data-qa-selection-card-info={label}
onClick={handleInfoClick}
onKeyDown={handleKeyPress}
role="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,12 @@ import { Box } from 'src/components/Box';
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
import Select, { Item } from 'src/components/EnhancedSelect';
import { ImageSelect } from 'src/components/ImageSelect/ImageSelect';
import { Typography } from 'src/components/Typography';
import { Paper } from 'src/components/Paper';
import { Typography } from 'src/components/Typography';
import { APP_ROOT } from 'src/constants';
import { ImageEmptyState } from 'src/features/Linodes/LinodesCreate/TabbedContent/ImageEmptyState';
import { AppDetailDrawer } from 'src/features/OneClickApps';
import {
AppCategory,
oneClickApps,
} from 'src/features/OneClickApps/oneClickApps';
import { oneClickApps } from 'src/features/OneClickApps/oneClickApps';
import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel';
import { sendMarketplaceSearchEvent } from 'src/utilities/analytics';
import getAPIErrorsFor from 'src/utilities/getAPIErrorFor';
Expand All @@ -30,8 +27,11 @@ import {
StackScriptFormStateHandlers,
WithTypesRegionsAndImages,
} from '../types';
import { filterUDFErrors } from './formUtilities';
import { StyledGrid } from './CommonTabbedContent.styles';
import { filterUDFErrors } from './formUtilities';

import type { FlagSet } from 'src/featureFlags';
import type { AppCategory } from 'src/features/OneClickApps/types';

const appCategories = [
'Control Panels',
Expand Down Expand Up @@ -65,6 +65,7 @@ const errorResources = {
};

interface Props {
flags: FlagSet;
setNumberOfNodesForAppCluster: (num: number) => void;
}

Expand Down Expand Up @@ -144,6 +145,7 @@ export class FromAppsContent extends React.Component<CombinedProps, State> {
availableStackScriptImages: compatibleImages,
availableUserDefinedFields: userDefinedFields,
errors,
flags,
selectedImageID,
selectedStackScriptID,
selectedStackScriptLabel,
Expand Down Expand Up @@ -239,6 +241,7 @@ export class FromAppsContent extends React.Component<CombinedProps, State> {
appInstancesLoading={appInstancesLoading}
disabled={userCannotCreateLinode}
error={hasErrorFor('stackscript_id')}
flags={flags}
handleClick={handleSelectStackScript}
isFiltering={isFiltering}
isSearching={isSearching}
Expand Down
Loading
Loading