Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tech Stories
---

Tag Linode Create v2 with form events ([#10840](https://github.com/linode/manager/pull/10840))
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { styled } from '@mui/material/styles';
import * as React from 'react';
import { useLocation } from 'react-router-dom';

import { Box } from 'src/components/Box';
import { Paper } from 'src/components/Paper';
Expand All @@ -10,17 +9,13 @@ import { CreateFirewallDrawer } from 'src/features/Firewalls/FirewallLanding/Cre
import { useFlags } from 'src/hooks/useFlags';
import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled';
import { useFirewallsQuery } from 'src/queries/firewalls';
import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics';
import { getQueryParamsFromQueryString } from 'src/utilities/queryParams';

import { AkamaiBanner } from '../AkamaiBanner/AkamaiBanner';
import { Autocomplete } from '../Autocomplete/Autocomplete';
import { GenerateFirewallDialog } from '../GenerateFirewallDialog/GenerateFirewallDialog';
import { LinkButton } from '../LinkButton';

import type { Firewall, FirewallDeviceEntityType } from '@linode/api-v4';
import type { LinodeCreateQueryParams } from 'src/features/Linodes/types';
import type { LinodeCreateFormEventOptions } from 'src/utilities/analytics/types';

interface Props {
disabled?: boolean;
Expand All @@ -41,18 +36,7 @@ export const SelectFirewallPanel = (props: Props) => {

const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);
const [isFirewallDialogOpen, setIsFirewallDialogOpen] = React.useState(false);
const location = useLocation();
const isFromLinodeCreate = location.pathname.includes('/linodes/create');
const queryParams = getQueryParamsFromQueryString<LinodeCreateQueryParams>(
location.search
);

const firewallFormEventOptions: LinodeCreateFormEventOptions = {
createType: queryParams.type ?? 'OS',
headerName: 'Firewall',
interaction: 'click',
label: 'Firewall',
};
const flags = useFlags();

const { secureVMNoticesEnabled } = useSecureVMNoticesEnabled();
Expand All @@ -61,12 +45,6 @@ export const SelectFirewallPanel = (props: Props) => {

const handleCreateFirewallClick = () => {
setIsDrawerOpen(true);
if (isFromLinodeCreate) {
sendLinodeCreateFormInputEvent({
...firewallFormEventOptions,
label: 'Create Firewall',
});
}
};

const handleFirewallCreated = (firewall: Firewall) => {
Expand Down Expand Up @@ -116,25 +94,9 @@ export const SelectFirewallPanel = (props: Props) => {
/>
)}
<Autocomplete
onChange={(_, selection) => {
handleFirewallChange(selection?.value ?? -1);
// Track clearing and changing the value once per page view, configured by inputValue in AA backend.
if (!selection) {
sendLinodeCreateFormInputEvent({
...firewallFormEventOptions,
interaction: 'clear',
subheaderName: 'Assign Firewall',
trackOnce: true,
});
} else {
sendLinodeCreateFormInputEvent({
...firewallFormEventOptions,
interaction: 'change',
subheaderName: 'Assign Firewall',
trackOnce: true,
});
}
}}
onChange={(_, selection) =>
handleFirewallChange(selection?.value ?? -1)
}
disabled={disabled}
errorText={error?.[0].reason}
label="Assign Firewall"
Expand Down
15 changes: 14 additions & 1 deletion packages/manager/src/features/Linodes/LinodeCreatev2/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ import { Button } from 'src/components/Button/Button';
import { useFlags } from 'src/hooks/useFlags';
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
import { sendApiAwarenessClickEvent } from 'src/utilities/analytics/customEventAnalytics';
import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics';
import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView';

import { ApiAwarenessModal } from '../LinodesCreate/ApiAwarenessModal/ApiAwarenessModal';
import { getLinodeCreatePayload } from './utilities';
import {
getLinodeCreatePayload,
useLinodeCreateQueryParams,
} from './utilities';

import type { LinodeCreateFormValues } from './utilities';

export const Actions = () => {
const flags = useFlags();

const { params } = useLinodeCreateQueryParams();

const [isAPIAwarenessModalOpen, setIsAPIAwarenessModalOpen] = useState(false);

const isDxToolsAdditionsEnabled = flags?.apicliDxToolsAdditions;
Expand All @@ -35,6 +41,13 @@ export const Actions = () => {

const onOpenAPIAwareness = async () => {
sendApiAwarenessClickEvent('Button', 'Create Using Command Line');
sendLinodeCreateFormInputEvent({
createType: params.type ?? 'OS',
interaction: 'click',
label: isDxToolsAdditionsEnabled
? 'View Code Snippets'
: 'Create Using Command Line',
});
if (await trigger()) {
// If validation is successful, we open the dialog.
setIsAPIAwarenessModalOpen(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { CreateLinodeRequest } from '@linode/api-v4';
import React from 'react';
import { Controller, useFormContext } from 'react-hook-form';

Expand All @@ -12,6 +11,8 @@ import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGran
import { useLinodeCreateQueryParams } from '../utilities';
import { PlacementGroupPanel } from './PlacementGroupPanel';

import type { CreateLinodeRequest } from '@linode/api-v4';

export const Details = () => {
const { control } = useFormContext<CreateLinodeRequest>();
const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import React from 'react';
import { useController, useWatch } from 'react-hook-form';

import { PlacementGroupsDetailPanel } from 'src/features/PlacementGroups/PlacementGroupsDetailPanel';
import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics';

import { useLinodeCreateQueryParams } from '../utilities';

import type { CreateLinodeRequest } from '@linode/api-v4';
import type { LinodeCreateFormEventOptions } from 'src/utilities/analytics/types';

export const PlacementGroupPanel = () => {
const { field } = useController<CreateLinodeRequest>({
Expand All @@ -12,11 +16,30 @@ export const PlacementGroupPanel = () => {

const regionId = useWatch<CreateLinodeRequest>({ name: 'region' });

const { params } = useLinodeCreateQueryParams();

const placementGroupFormEventOptions: LinodeCreateFormEventOptions = {
createType: params.type ?? 'OS',
headerName: 'Details',
interaction: 'change',
label: 'Placement Group',
subheaderName: 'Placement Groups in Region',
trackOnce: true,
};

return (
<PlacementGroupsDetailPanel
handlePlacementGroupChange={(placementGroup) =>
field.onChange(placementGroup?.id)
}
handlePlacementGroupChange={(placementGroup) => {
field.onChange(placementGroup?.id);
if (!placementGroup?.id) {
sendLinodeCreateFormInputEvent({
...placementGroupFormEventOptions,
interaction: 'clear',
});
} else {
sendLinodeCreateFormInputEvent(placementGroupFormEventOptions);
}
}}
selectedPlacementGroupId={field.value ?? null}
selectedRegionId={regionId}
/>
Expand Down
55 changes: 52 additions & 3 deletions packages/manager/src/features/Linodes/LinodeCreatev2/Firewall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import { useFlags } from 'src/hooks/useFlags';
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled';
import { useAllFirewallsQuery } from 'src/queries/firewalls';
import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics';

import { useLinodeCreateQueryParams } from './utilities';

import type { LinodeCreateFormValues } from './utilities';
import type { CreateLinodeRequest } from '@linode/api-v4';
import type { LinodeCreateFormEventOptions } from 'src/utilities/analytics/types';

export const Firewall = () => {
const { clearErrors } = useFormContext<LinodeCreateFormValues>();
Expand All @@ -34,6 +38,8 @@ export const Firewall = () => {

const flags = useFlags();

const { params } = useLinodeCreateQueryParams();

const { secureVMNoticesEnabled } = useSecureVMNoticesEnabled();
const secureVMFirewallBanner =
(secureVMNoticesEnabled && flags.secureVmCopy) ?? false;
Expand All @@ -52,14 +58,34 @@ export const Firewall = () => {
field.onChange(firewallId ?? null);
};

const firewallFormEventOptions: LinodeCreateFormEventOptions = {
createType: params.type ?? 'OS',
headerName: 'Firewall',
interaction: 'click',
label: 'Firewall',
};

return (
<Paper>
<Stack spacing={2}>
<Typography variant="h2">Firewall</Typography>
<Typography>
Assign an existing Firewall to this Linode to control inbound and
outbound network traffic.{' '}
<Link to={FIREWALL_GET_STARTED_LINK}>Learn more</Link>.
<Link
onClick={() =>
sendLinodeCreateFormInputEvent({
createType: params.type ?? 'OS',
headerName: 'Firewall',
interaction: 'click',
label: 'Learn more',
})
}
to={FIREWALL_GET_STARTED_LINK}
>
Learn more
</Link>
.
</Typography>
{secureVMFirewallBanner !== false &&
secureVMFirewallBanner.linodeCreate && (
Expand All @@ -77,21 +103,44 @@ export const Firewall = () => {
)}
<Stack spacing={1.5}>
<Autocomplete
onChange={(e, firewall) => {
onChange(firewall?.id);
if (!firewall?.id) {
sendLinodeCreateFormInputEvent({
...firewallFormEventOptions,
interaction: 'clear',
subheaderName: 'Assign Firewall',
trackOnce: true,
});
} else {
sendLinodeCreateFormInputEvent({
...firewallFormEventOptions,
interaction: 'change',
subheaderName: 'Assign Firewall',
trackOnce: true,
});
}
}}
disabled={isLinodeCreateRestricted}
errorText={fieldState.error?.message ?? error?.[0].reason}
label="Assign Firewall"
loading={isLoading}
noMarginTop
onBlur={field.onBlur}
onChange={(e, firewall) => onChange(firewall?.id)}
options={firewalls ?? []}
placeholder="None"
value={selectedFirewall}
/>
<Box>
<LinkButton
onClick={() => {
setIsDrawerOpen(true);
sendLinodeCreateFormInputEvent({
...firewallFormEventOptions,
label: 'Create Firewall',
});
}}
isDisabled={isLinodeCreateRestricted}
onClick={() => setIsDrawerOpen(true)}
>
Create Firewall
</LinkButton>
Expand Down
10 changes: 10 additions & 0 deletions packages/manager/src/features/Linodes/LinodeCreatev2/Plan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGran
import { useRegionsQuery } from 'src/queries/regions/regions';
import { useAllTypes } from 'src/queries/types';
import { sendLinodeCreateFlowDocsClickEvent } from 'src/utilities/analytics/customEventAnalytics';
import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics';
import { extendType } from 'src/utilities/extendType';

import { useLinodeCreateQueryParams } from './utilities';

import type { LinodeCreateFormValues } from './utilities';
import type { CreateLinodeRequest } from '@linode/api-v4';

Expand All @@ -22,6 +25,7 @@ export const Plan = () => {

const { data: regions } = useRegionsQuery();
const { data: types } = useAllTypes();
const { params } = useLinodeCreateQueryParams();

const isLinodeCreateRestricted = useRestrictedGlobalGrantCheck({
globalGrantType: 'add_linodes',
Expand All @@ -33,6 +37,12 @@ export const Plan = () => {
<DocsLink
onClick={() => {
sendLinodeCreateFlowDocsClickEvent('Choosing a Plan');
sendLinodeCreateFormInputEvent({
createType: params.type ?? 'OS',
headerName: 'Linode Plan',
interaction: 'click',
label: 'Choosing a Plan',
});
}}
href="https://www.linode.com/docs/guides/choosing-a-compute-instance-plan/"
label="Choosing a Plan"
Expand Down
17 changes: 17 additions & 0 deletions packages/manager/src/features/Linodes/LinodeCreatev2/Region.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGran
import { useImageQuery } from 'src/queries/images';
import { useRegionsQuery } from 'src/queries/regions/regions';
import { useTypeQuery } from 'src/queries/types';
import {
sendLinodeCreateFormInputEvent,
sendLinodeCreateFormStartEvent,
} from 'src/utilities/analytics/formEventAnalytics';
import {
DIFFERENT_PRICE_STRUCTURE_WARNING,
DOCS_LINK_LABEL_DC_PRICING,
Expand Down Expand Up @@ -150,6 +154,11 @@ export const Region = () => {

setValue('label', label);
}

// Begin tracking the Linode Create form.
sendLinodeCreateFormStartEvent({
createType: params.type ?? 'OS',
});
};

const showCrossDataCenterCloneWarning =
Expand Down Expand Up @@ -210,6 +219,14 @@ export const Region = () => {
<Box display="flex" justifyContent="space-between" mb={1}>
<Typography variant="h2">Region</Typography>
<DocsLink
onClick={() =>
sendLinodeCreateFormInputEvent({
createType: params.type ?? 'OS',
headerName: 'Region',
interaction: 'click',
label: DOCS_LINK_LABEL_DC_PRICING,
})
}
href="https://www.linode.com/pricing"
label={DOCS_LINK_LABEL_DC_PRICING}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { userEvent } from '@testing-library/user-event';
import React from 'react';

import { DOCS_LINK_LABEL_DC_PRICING } from 'src/utilities/pricing/constants';
import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';

import { TwoStepRegion } from './TwoStepRegion';

describe('TwoStepRegion', () => {
it('should render a heading', () => {
const { getAllByText } = renderWithThemeAndHookFormContext({
it('should render a heading and docs link', () => {
const { getAllByText, getByText } = renderWithThemeAndHookFormContext({
component: <TwoStepRegion onChange={vi.fn()} />,
});

const heading = getAllByText('Region')[0];
const link = getByText(DOCS_LINK_LABEL_DC_PRICING);

expect(heading).toBeVisible();
expect(heading.tagName).toBe('H2');

expect(link).toBeVisible();
expect(link).toHaveRole('link');
expect(link).toHaveAttribute('href', 'https://www.linode.com/pricing');
});

it('should render two tabs, Core and Distributed', () => {
Expand Down
Loading