From fcf7827e2274edfb28842128372f451917281a0c Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Tue, 9 Apr 2024 17:32:52 -0400
Subject: [PATCH 01/22] initial work

---
 .../src/components/PrimaryNav/PrimaryNav.tsx  |  4 +-
 .../src/containers/withMarketplaceApps.ts     |  4 +-
 .../Tabs/StackScripts/Images.tsx              | 41 ++++++++
 .../StackScripts/StackScriptSelectionList.tsx | 96 +++++++++++++++++++
 .../StackScripts/StackScriptSelectionRow.tsx  | 30 ++++++
 .../Tabs/StackScripts/StackScripts.tsx        | 45 +++++++++
 .../Tabs/StackScripts/utilities.ts            | 17 ++++
 .../features/Linodes/LinodeCreatev2/index.tsx |  5 +-
 .../Linodes/LinodeCreatev2/utilities.ts       |  3 +
 packages/manager/src/queries/stackscripts.ts  | 70 ++++++++++----
 10 files changed, 294 insertions(+), 21 deletions(-)
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts

diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx
index 631a36b6991..5f6af7b2d8d 100644
--- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx
+++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx
@@ -35,7 +35,7 @@ import {
   useObjectStorageClusters,
 } from 'src/queries/objectStorage';
 import { useRegionsQuery } from 'src/queries/regions/regions';
-import { useStackScriptsOCA } from 'src/queries/stackscripts';
+import { useMarketplaceAppsQuery } from 'src/queries/stackscripts';
 import { isFeatureEnabled } from 'src/utilities/accountCapabilities';
 
 import useStyles from './PrimaryNav.styles';
@@ -115,7 +115,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
     data: oneClickApps,
     error: oneClickAppsError,
     isLoading: oneClickAppsLoading,
-  } = useStackScriptsOCA(enableMarketplacePrefetch);
+  } = useMarketplaceAppsQuery(enableMarketplacePrefetch);
 
   const {
     data: clusters,
diff --git a/packages/manager/src/containers/withMarketplaceApps.ts b/packages/manager/src/containers/withMarketplaceApps.ts
index 0dda04baf9f..f5040df7241 100644
--- a/packages/manager/src/containers/withMarketplaceApps.ts
+++ b/packages/manager/src/containers/withMarketplaceApps.ts
@@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom';
 
 import { baseApps } from 'src/features/StackScripts/stackScriptUtils';
 import { useFlags } from 'src/hooks/useFlags';
-import { useStackScriptsOCA } from 'src/queries/stackscripts';
+import { useMarketplaceAppsQuery } from 'src/queries/stackscripts';
 import { getQueryParamFromQueryString } from 'src/utilities/queryParams';
 
 const trimOneClickFromLabel = (script: StackScript) => {
@@ -31,7 +31,7 @@ export const withMarketplaceApps = <Props>(
   // Only enable the query when the user is on the Marketplace page
   const enabled = type === 'One-Click';
 
-  const { data, error, isLoading } = useStackScriptsOCA(enabled);
+  const { data, error, isLoading } = useMarketplaceAppsQuery(enabled);
 
   const newApps = flags.oneClickApps || [];
   const allowedApps = Object.keys({ ...baseApps, ...newApps });
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx
new file mode 100644
index 00000000000..4e5f09256a3
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { Controller, useWatch } from 'react-hook-form';
+
+import { ImageSelectv2 } from 'src/components/ImageSelectv2/ImageSelectv2';
+import { Paper } from 'src/components/Paper';
+import { Typography } from 'src/components/Typography';
+import { useStackScriptQuery } from 'src/queries/stackscripts';
+
+import type { CreateLinodeRequest } from '@linode/api-v4';
+
+export const Images = () => {
+  const stackscriptId = useWatch<CreateLinodeRequest>({
+    name: 'stackscript_id',
+  });
+
+  const { data: stackscript } = useStackScriptQuery(
+    stackscriptId,
+    Boolean(stackscriptId)
+  );
+
+  const imageSelectVariant = stackscript?.images.includes('any/all')
+    ? 'all'
+    : 'public';
+
+  return (
+    <Paper>
+      <Typography variant="h2">Select an Image</Typography>
+      <Controller<CreateLinodeRequest, 'image'>
+        render={({ field, fieldState }) => (
+          <ImageSelectv2
+            errorText={fieldState.error?.message}
+            onChange={(e, image) => field.onChange(image?.id ?? null)}
+            value={field.value}
+            variant={imageSelectVariant}
+          />
+        )}
+        name="image"
+      />
+    </Paper>
+  );
+};
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
new file mode 100644
index 00000000000..dec2b25b1bd
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { useController } from 'react-hook-form';
+import { Waypoint } from 'react-waypoint';
+
+import { Box } from 'src/components/Box';
+import { Table } from 'src/components/Table';
+import { TableBody } from 'src/components/TableBody';
+import { TableCell } from 'src/components/TableCell/TableCell';
+import { TableHead } from 'src/components/TableHead';
+import { TableRow } from 'src/components/TableRow/TableRow';
+import { TableRowError } from 'src/components/TableRowError/TableRowError';
+import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading';
+import { TableSortCell } from 'src/components/TableSortCell';
+import { useOrder } from 'src/hooks/useOrder';
+import { useStackScriptsInfiniteQuery } from 'src/queries/stackscripts';
+
+import { StackScriptSelectionRow } from './StackScriptSelectionRow';
+
+import type { StackScriptTabType } from './utilities';
+import type { CreateLinodeRequest } from '@linode/api-v4';
+
+interface Props {
+  type: StackScriptTabType;
+}
+
+export const StackScriptSelectionList = ({ type }: Props) => {
+  const { handleOrderChange, order, orderBy } = useOrder({
+    order: 'desc',
+    orderBy: 'deployments_total',
+  });
+
+  const { field } = useController<CreateLinodeRequest, 'stackscript_id'>({
+    name: 'stackscript_id',
+  });
+
+  const filter =
+    type === 'Community'
+      ? {
+          '+and': [
+            { username: { '+neq': 'linode' } },
+            { username: { '+neq': 'linode-stackscripts' } },
+          ],
+          mine: false,
+        }
+      : { mine: true };
+
+  const {
+    data,
+    error,
+    fetchNextPage,
+    hasNextPage,
+    isFetchingNextPage,
+    isLoading,
+  } = useStackScriptsInfiniteQuery({
+    ['+order']: order,
+    ['+order_by']: orderBy,
+    ...filter,
+  });
+
+  const stackscripts = data?.pages.flatMap((page) => page.data);
+
+  return (
+    <Box sx={{ maxHeight: 500, overflow: 'auto' }}>
+      <Table>
+        <TableHead>
+          <TableRow>
+            <TableCell sx={{ width: 20 }}></TableCell>
+            <TableSortCell
+              active={orderBy === 'label'}
+              direction={order}
+              handleClick={handleOrderChange}
+              label="label"
+            >
+              StackScript
+            </TableSortCell>
+            <TableCell></TableCell>
+          </TableRow>
+        </TableHead>
+        <TableBody>
+          {stackscripts?.map((stackscript) => (
+            <StackScriptSelectionRow
+              isSelected={field.value === stackscript.id}
+              key={stackscript.id}
+              onSelect={() => field.onChange(stackscript.id)}
+              stackscript={stackscript}
+            />
+          ))}
+          {error && <TableRowError colSpan={3} message={error[0].reason} />}
+          {isLoading && <TableRowLoading columns={3} rows={25} />}
+          {isFetchingNextPage && <TableRowLoading columns={3} rows={1} />}
+          {hasNextPage && <Waypoint onEnter={() => fetchNextPage()} />}
+        </TableBody>
+      </Table>
+    </Box>
+  );
+};
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
new file mode 100644
index 00000000000..96095dfb67b
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+
+import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction';
+import { Radio } from 'src/components/Radio/Radio';
+import { TableCell } from 'src/components/TableCell';
+import { TableRow } from 'src/components/TableRow';
+
+import type { StackScript } from '@linode/api-v4';
+
+interface Props {
+  isSelected: boolean;
+  onSelect: () => void;
+  stackscript: StackScript;
+}
+
+export const StackScriptSelectionRow = (props: Props) => {
+  const { isSelected, onSelect, stackscript } = props;
+
+  return (
+    <TableRow>
+      <TableCell>
+        <Radio checked={isSelected} onChange={onSelect} />
+      </TableCell>
+      <TableCell>{stackscript.label}</TableCell>
+      <TableCell actionCell>
+        <InlineMenuAction actionText="Show Details" />
+      </TableCell>
+    </TableRow>
+  );
+};
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
new file mode 100644
index 00000000000..0449b38cf91
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+
+import { Paper } from 'src/components/Paper';
+import { Stack } from 'src/components/Stack';
+import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel';
+import { Tab } from 'src/components/Tabs/Tab';
+import { TabList } from 'src/components/Tabs/TabList';
+import { TabPanels } from 'src/components/Tabs/TabPanels';
+import { Tabs } from 'src/components/Tabs/Tabs';
+import { Typography } from 'src/components/Typography';
+
+import { useLinodeCreateQueryParams } from '../../utilities';
+import { Images } from './Images';
+import { StackScriptSelectionList } from './StackScriptSelectionList';
+import { getStackScriptTabIndex, tabs } from './utilities';
+
+export const StackScripts = () => {
+  const { params, updateParams } = useLinodeCreateQueryParams();
+
+  return (
+    <Stack spacing={3}>
+      <Paper>
+        <Typography variant="h2">Create From:</Typography>
+        <Tabs
+          index={getStackScriptTabIndex(params.subtype)}
+          onChange={(index) => updateParams({ subtype: tabs[index] })}
+        >
+          <TabList>
+            <Tab>Account StackScripts</Tab>
+            <Tab>Community StackScripts</Tab>
+          </TabList>
+          <TabPanels>
+            <SafeTabPanel index={0}>
+              <StackScriptSelectionList type="Account" />
+            </SafeTabPanel>
+            <SafeTabPanel index={1}>
+              <StackScriptSelectionList type="Community" />
+            </SafeTabPanel>
+          </TabPanels>
+        </Tabs>
+      </Paper>
+      <Images />
+    </Stack>
+  );
+};
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts
new file mode 100644
index 00000000000..07fa6f6ecf7
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts
@@ -0,0 +1,17 @@
+export type StackScriptTabType = 'Account' | 'Community';
+
+export const getStackScriptTabIndex = (tab: StackScriptTabType | undefined) => {
+  if (tab === undefined) {
+    return 0;
+  }
+
+  const tabIndex = tabs.indexOf(tab);
+
+  if (tabIndex === -1) {
+    return 0;
+  }
+
+  return tabIndex;
+};
+
+export const tabs = ['Account', 'Community'] as const;
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
index 59675e6923a..f248b30fa4a 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
@@ -22,6 +22,7 @@ import { Region } from './Region';
 import { Summary } from './Summary';
 import { Distributions } from './Tabs/Distributions';
 import { Images } from './Tabs/Images';
+import { StackScripts } from './Tabs/StackScripts/StackScripts';
 import { UserData } from './UserData/UserData';
 import {
   defaultValues,
@@ -90,7 +91,9 @@ export const LinodeCreatev2 = () => {
                 <Distributions />
               </SafeTabPanel>
               <SafeTabPanel index={1}>Marketplace</SafeTabPanel>
-              <SafeTabPanel index={2}>StackScripts</SafeTabPanel>
+              <SafeTabPanel index={2}>
+                <StackScripts />
+              </SafeTabPanel>
               <SafeTabPanel index={3}>
                 <Images />
               </SafeTabPanel>
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
index 01da7ce0b23..c4a44c3c8e6 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
@@ -5,12 +5,14 @@ import { getQueryParamsFromQueryString } from 'src/utilities/queryParams';
 import { utoa } from '../LinodesCreate/utilities';
 
 import type { LinodeCreateType } from '../LinodesCreate/types';
+import type { StackScriptTabType } from './Tabs/StackScripts/utilities';
 import type { CreateLinodeRequest, InterfacePayload } from '@linode/api-v4';
 
 /**
  * This interface is used to type the query params on the Linode Create flow.
  */
 interface LinodeCreateQueryParams {
+  subtype: StackScriptTabType | undefined;
   type: LinodeCreateType | undefined;
 }
 
@@ -30,6 +32,7 @@ export const useLinodeCreateQueryParams = () => {
   };
 
   const params = {
+    subtype: rawParams.subtype as StackScriptTabType | undefined,
     type: rawParams.type as LinodeCreateType | undefined,
   } as LinodeCreateQueryParams;
 
diff --git a/packages/manager/src/queries/stackscripts.ts b/packages/manager/src/queries/stackscripts.ts
index 607edb6c24a..0ac93e0c100 100644
--- a/packages/manager/src/queries/stackscripts.ts
+++ b/packages/manager/src/queries/stackscripts.ts
@@ -1,26 +1,64 @@
-import { StackScript } from '@linode/api-v4/lib/stackscripts';
-import { APIError, Params } from '@linode/api-v4/lib/types';
-import { useQuery } from '@tanstack/react-query';
+import {
+  StackScript,
+  getStackScript,
+  getStackScripts,
+} from '@linode/api-v4/lib/stackscripts';
+import {
+  APIError,
+  Filter,
+  Params,
+  ResourcePage,
+} from '@linode/api-v4/lib/types';
+import { createQueryKeys } from '@lukemorales/query-key-factory';
+import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
 
 import { getOneClickApps } from 'src/features/StackScripts/stackScriptUtils';
 import { getAll } from 'src/utilities/getAll';
 
 import { queryPresets } from './base';
 
-export const queryKey = 'stackscripts';
-
-export const useStackScriptsOCA = (enabled: boolean, params: Params = {}) => {
-  return useQuery<StackScript[], APIError[]>(
-    [`${queryKey}-oca-all`, params],
-    () => getAllOCAsRequest(params),
-    {
-      enabled,
-      ...queryPresets.oneTimeFetch,
-    }
-  );
-};
-
 export const getAllOCAsRequest = (passedParams: Params = {}) =>
   getAll<StackScript>((params) =>
     getOneClickApps({ ...params, ...passedParams })
   )().then((data) => data.data);
+
+const stackscriptQueries = createQueryKeys('stackscripts', {
+  infinite: (filter: Filter = {}) => ({
+    queryFn: ({ pageParam }) =>
+      getStackScripts({ page: pageParam, page_size: 25 }, filter),
+    queryKey: [filter],
+  }),
+  marketplace: {
+    queryFn: () => getAllOCAsRequest(),
+    queryKey: null,
+  },
+  stackscript: (id: number) => ({
+    queryFn: () => getStackScript(id),
+    queryKey: [id],
+  }),
+});
+
+export const useMarketplaceAppsQuery = (enabled: boolean) => {
+  return useQuery<StackScript[], APIError[]>({
+    ...stackscriptQueries.marketplace,
+    enabled,
+    ...queryPresets.oneTimeFetch,
+  });
+};
+
+export const useStackScriptQuery = (id: number, enabled = true) =>
+  useQuery<StackScript, APIError[]>({
+    ...stackscriptQueries.stackscript(id),
+    enabled,
+  });
+
+export const useStackScriptsInfiniteQuery = (filter: Filter = {}) =>
+  useInfiniteQuery<ResourcePage<StackScript>, APIError[]>({
+    ...stackscriptQueries.infinite(filter),
+    getNextPageParam: ({ page, pages }) => {
+      if (page === pages) {
+        return undefined;
+      }
+      return page + 1;
+    },
+  });

From 0b82b26ff75ce6f82842fafd1dcde88457193a3e Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Tue, 9 Apr 2024 17:52:16 -0400
Subject: [PATCH 02/22] filter options based on stackscript compatibility

---
 .../src/components/ImageSelectv2/ImageSelectv2.tsx    | 10 ++++++++--
 .../LinodeCreatev2/Tabs/StackScripts/Images.tsx       | 11 ++++++++---
 .../Tabs/StackScripts/StackScriptSelectionRow.tsx     |  4 ++++
 3 files changed, 20 insertions(+), 5 deletions(-)

diff --git a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
index c40255eda3e..ea1369ced0a 100644
--- a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
+++ b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
@@ -16,6 +16,10 @@ export type ImageSelectVariant = 'all' | 'private' | 'public';
 
 interface Props
   extends Omit<Partial<EnhancedAutocompleteProps<Image>>, 'value'> {
+  /**
+   * Optional filter function applied to the options.
+   */
+  filter?: (image: Image) => boolean;
   /**
    * The ID of the selected image
    */
@@ -30,7 +34,7 @@ interface Props
 }
 
 export const ImageSelectv2 = (props: Props) => {
-  const { variant, ...rest } = props;
+  const { filter, variant, ...rest } = props;
 
   const { data: images, error, isLoading } = useAllImagesQuery(
     {},
@@ -40,6 +44,8 @@ export const ImageSelectv2 = (props: Props) => {
   // We can't filter out Kubernetes images using the API so we filter them here
   const options = getFilteredImagesForImageSelect(images, variant);
 
+  const filteredOptions = filter ? options?.filter(filter) : options;
+
   const value = images?.find((i) => i.id === props.value);
 
   return (
@@ -55,7 +61,7 @@ export const ImageSelectv2 = (props: Props) => {
       groupBy={(option) => option.vendor ?? 'My Images'}
       label="Images"
       loading={isLoading}
-      options={options ?? []}
+      options={filteredOptions ?? []}
       placeholder="Choose an image"
       {...rest}
       errorText={rest.errorText ?? error?.[0].reason}
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx
index 4e5f09256a3..739dc662d19 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx
@@ -18,9 +18,9 @@ export const Images = () => {
     Boolean(stackscriptId)
   );
 
-  const imageSelectVariant = stackscript?.images.includes('any/all')
-    ? 'all'
-    : 'public';
+  const shouldFilterImages = !stackscript?.images.includes('any/all');
+
+  const imageSelectVariant = shouldFilterImages ? 'public' : 'all';
 
   return (
     <Paper>
@@ -28,6 +28,11 @@ export const Images = () => {
       <Controller<CreateLinodeRequest, 'image'>
         render={({ field, fieldState }) => (
           <ImageSelectv2
+            filter={
+              shouldFilterImages
+                ? (image) => stackscript?.images.includes(image.id) ?? false
+                : undefined
+            }
             errorText={fieldState.error?.message}
             onChange={(e, image) => field.onChange(image?.id ?? null)}
             value={field.value}
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
index 96095dfb67b..4b5f58969bc 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
@@ -16,6 +16,10 @@ interface Props {
 export const StackScriptSelectionRow = (props: Props) => {
   const { isSelected, onSelect, stackscript } = props;
 
+  if (stackscript.username.startsWith('lke-service-account-')) {
+    return null;
+  }
+
   return (
     <TableRow>
       <TableCell>

From e02f1c06309cf422f0564a598e2ed3558a3ac688 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Tue, 9 Apr 2024 18:56:56 -0400
Subject: [PATCH 03/22] implement complex preselection logic

---
 .../StackScripts/StackScriptSelectionList.tsx | 67 ++++++++++++++++---
 .../StackScripts/StackScriptSelectionRow.tsx  |  5 +-
 .../Tabs/StackScripts/StackScripts.tsx        |  4 +-
 .../Linodes/LinodeCreatev2/utilities.ts       | 57 ++++++++++------
 packages/manager/src/queries/stackscripts.ts  |  6 +-
 5 files changed, 104 insertions(+), 35 deletions(-)

diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
index dec2b25b1bd..c3ff1096136 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
@@ -3,6 +3,8 @@ import { useController } from 'react-hook-form';
 import { Waypoint } from 'react-waypoint';
 
 import { Box } from 'src/components/Box';
+import { Button } from 'src/components/Button/Button';
+import { Stack } from 'src/components/Stack';
 import { Table } from 'src/components/Table';
 import { TableBody } from 'src/components/TableBody';
 import { TableCell } from 'src/components/TableCell/TableCell';
@@ -12,11 +14,15 @@ import { TableRowError } from 'src/components/TableRowError/TableRowError';
 import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading';
 import { TableSortCell } from 'src/components/TableSortCell';
 import { useOrder } from 'src/hooks/useOrder';
-import { useStackScriptsInfiniteQuery } from 'src/queries/stackscripts';
+import {
+  useStackScriptQuery,
+  useStackScriptsInfiniteQuery,
+} from 'src/queries/stackscripts';
 
+import { useLinodeCreateQueryParams } from '../../utilities';
 import { StackScriptSelectionRow } from './StackScriptSelectionRow';
+import { StackScriptTabType } from './utilities';
 
-import type { StackScriptTabType } from './utilities';
 import type { CreateLinodeRequest } from '@linode/api-v4';
 
 interface Props {
@@ -44,6 +50,15 @@ export const StackScriptSelectionList = ({ type }: Props) => {
         }
       : { mine: true };
 
+  const { params, updateParams } = useLinodeCreateQueryParams();
+
+  const hasPreselectedStackScript = Boolean(params.stackScriptID);
+
+  const { data: stackscript } = useStackScriptQuery(
+    params.stackScriptID ?? -1,
+    hasPreselectedStackScript
+  );
+
   const {
     data,
     error,
@@ -51,16 +66,50 @@ export const StackScriptSelectionList = ({ type }: Props) => {
     hasNextPage,
     isFetchingNextPage,
     isLoading,
-  } = useStackScriptsInfiniteQuery({
-    ['+order']: order,
-    ['+order_by']: orderBy,
-    ...filter,
-  });
+  } = useStackScriptsInfiniteQuery(
+    {
+      ['+order']: order,
+      ['+order_by']: orderBy,
+      ...filter,
+    },
+    !hasPreselectedStackScript
+  );
 
   const stackscripts = data?.pages.flatMap((page) => page.data);
 
+  if (hasPreselectedStackScript) {
+    return (
+      <Stack spacing={1}>
+        <Table>
+          <TableHead>
+            <TableRow>
+              <TableCell sx={{ width: 20 }}></TableCell>
+              <TableCell>StackScript</TableCell>
+              <TableCell></TableCell>
+            </TableRow>
+          </TableHead>
+          <TableBody>
+            {stackscript && (
+              <StackScriptSelectionRow
+                disabled
+                isSelected={field.value === stackscript.id}
+                onSelect={() => field.onChange(stackscript.id)}
+                stackscript={stackscript}
+              />
+            )}
+          </TableBody>
+        </Table>
+        <Box display="flex" justifyContent="flex-end">
+          <Button onClick={() => updateParams({ stackScriptID: undefined })}>
+            Choose Another StackScript
+          </Button>
+        </Box>
+      </Stack>
+    );
+  }
+
   return (
-    <Box sx={{ maxHeight: 500, overflow: 'auto' }}>
+    <Stack spacing={1} sx={{ maxHeight: 500, overflow: 'auto' }}>
       <Table>
         <TableHead>
           <TableRow>
@@ -91,6 +140,6 @@ export const StackScriptSelectionList = ({ type }: Props) => {
           {hasNextPage && <Waypoint onEnter={() => fetchNextPage()} />}
         </TableBody>
       </Table>
-    </Box>
+    </Stack>
   );
 };
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
index 4b5f58969bc..a675200b769 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
@@ -8,13 +8,14 @@ import { TableRow } from 'src/components/TableRow';
 import type { StackScript } from '@linode/api-v4';
 
 interface Props {
+  disabled?: boolean;
   isSelected: boolean;
   onSelect: () => void;
   stackscript: StackScript;
 }
 
 export const StackScriptSelectionRow = (props: Props) => {
-  const { isSelected, onSelect, stackscript } = props;
+  const { disabled, isSelected, onSelect, stackscript } = props;
 
   if (stackscript.username.startsWith('lke-service-account-')) {
     return null;
@@ -23,7 +24,7 @@ export const StackScriptSelectionRow = (props: Props) => {
   return (
     <TableRow>
       <TableCell>
-        <Radio checked={isSelected} onChange={onSelect} />
+        <Radio checked={isSelected} disabled={disabled} onChange={onSelect} />
       </TableCell>
       <TableCell>{stackscript.label}</TableCell>
       <TableCell actionCell>
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
index 0449b38cf91..13d9546e581 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
@@ -22,8 +22,10 @@ export const StackScripts = () => {
       <Paper>
         <Typography variant="h2">Create From:</Typography>
         <Tabs
+          onChange={(index) =>
+            updateParams({ stackScriptID: undefined, subtype: tabs[index] })
+          }
           index={getStackScriptTabIndex(params.subtype)}
-          onChange={(index) => updateParams({ subtype: tabs[index] })}
         >
           <TabList>
             <Tab>Account StackScripts</Tab>
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
index c4a44c3c8e6..47197eb9b92 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
@@ -12,6 +12,7 @@ import type { CreateLinodeRequest, InterfacePayload } from '@linode/api-v4';
  * This interface is used to type the query params on the Linode Create flow.
  */
 interface LinodeCreateQueryParams {
+  stackScriptID: string | undefined;
   subtype: StackScriptTabType | undefined;
   type: LinodeCreateType | undefined;
 }
@@ -32,9 +33,12 @@ export const useLinodeCreateQueryParams = () => {
   };
 
   const params = {
+    stackScriptID: rawParams.stackScriptID
+      ? Number(rawParams.stackScriptID)
+      : undefined,
     subtype: rawParams.subtype as StackScriptTabType | undefined,
     type: rawParams.type as LinodeCreateType | undefined,
-  } as LinodeCreateQueryParams;
+  };
 
   return { params, updateParams };
 };
@@ -126,25 +130,34 @@ export const getInterfacesPayload = (
   return interfaces;
 };
 
-export const defaultValues: CreateLinodeRequest = {
-  image: 'linode/debian11',
-  interfaces: [
-    {
-      ipam_address: '',
-      label: '',
-      purpose: 'public',
-    },
-    {
-      ipam_address: '',
-      label: '',
-      purpose: 'vlan',
-    },
-    {
-      ipam_address: '',
-      label: '',
-      purpose: 'vpc',
-    },
-  ],
-  region: '',
-  type: '',
+export const defaultValues = async (): Promise<CreateLinodeRequest> => {
+  const queryParams = getQueryParamsFromQueryString(window.location.search);
+
+  const stackScriptID = queryParams.stackScriptID
+    ? Number(queryParams.stackScriptID)
+    : undefined;
+
+  return {
+    image: 'linode/debian11',
+    interfaces: [
+      {
+        ipam_address: '',
+        label: '',
+        purpose: 'public',
+      },
+      {
+        ipam_address: '',
+        label: '',
+        purpose: 'vlan',
+      },
+      {
+        ipam_address: '',
+        label: '',
+        purpose: 'vpc',
+      },
+    ],
+    region: '',
+    stackscript_id: stackScriptID,
+    type: '',
+  };
 };
diff --git a/packages/manager/src/queries/stackscripts.ts b/packages/manager/src/queries/stackscripts.ts
index 0ac93e0c100..b2eb2159179 100644
--- a/packages/manager/src/queries/stackscripts.ts
+++ b/packages/manager/src/queries/stackscripts.ts
@@ -52,9 +52,13 @@ export const useStackScriptQuery = (id: number, enabled = true) =>
     enabled,
   });
 
-export const useStackScriptsInfiniteQuery = (filter: Filter = {}) =>
+export const useStackScriptsInfiniteQuery = (
+  filter: Filter = {},
+  enabled = true
+) =>
   useInfiniteQuery<ResourcePage<StackScript>, APIError[]>({
     ...stackscriptQueries.infinite(filter),
+    enabled,
     getNextPageParam: ({ page, pages }) => {
       if (page === pages) {
         return undefined;

From c656c1a42ffa78611d2928a404ddcb1af1ce5bfd Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Tue, 9 Apr 2024 19:02:51 -0400
Subject: [PATCH 04/22] clean up a bit

---
 .../StackScripts/StackScriptSelectionList.tsx | 26 +++++++++----------
 .../Tabs/StackScripts/utilities.ts            | 10 +++++++
 2 files changed, 22 insertions(+), 14 deletions(-)

diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
index c3ff1096136..622b4d9a1ad 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
@@ -21,7 +21,11 @@ import {
 
 import { useLinodeCreateQueryParams } from '../../utilities';
 import { StackScriptSelectionRow } from './StackScriptSelectionRow';
-import { StackScriptTabType } from './utilities';
+import {
+  StackScriptTabType,
+  accountStackScriptFilter,
+  communityStackScriptFilter,
+} from './utilities';
 
 import type { CreateLinodeRequest } from '@linode/api-v4';
 
@@ -39,17 +43,6 @@ export const StackScriptSelectionList = ({ type }: Props) => {
     name: 'stackscript_id',
   });
 
-  const filter =
-    type === 'Community'
-      ? {
-          '+and': [
-            { username: { '+neq': 'linode' } },
-            { username: { '+neq': 'linode-stackscripts' } },
-          ],
-          mine: false,
-        }
-      : { mine: true };
-
   const { params, updateParams } = useLinodeCreateQueryParams();
 
   const hasPreselectedStackScript = Boolean(params.stackScriptID);
@@ -59,6 +52,11 @@ export const StackScriptSelectionList = ({ type }: Props) => {
     hasPreselectedStackScript
   );
 
+  const filter =
+    type === 'Community'
+      ? communityStackScriptFilter
+      : accountStackScriptFilter;
+
   const {
     data,
     error,
@@ -109,7 +107,7 @@ export const StackScriptSelectionList = ({ type }: Props) => {
   }
 
   return (
-    <Stack spacing={1} sx={{ maxHeight: 500, overflow: 'auto' }}>
+    <Box sx={{ maxHeight: 500, overflow: 'auto' }}>
       <Table>
         <TableHead>
           <TableRow>
@@ -140,6 +138,6 @@ export const StackScriptSelectionList = ({ type }: Props) => {
           {hasNextPage && <Waypoint onEnter={() => fetchNextPage()} />}
         </TableBody>
       </Table>
-    </Stack>
+    </Box>
   );
 };
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts
index 07fa6f6ecf7..4bbf74f5f42 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts
@@ -15,3 +15,13 @@ export const getStackScriptTabIndex = (tab: StackScriptTabType | undefined) => {
 };
 
 export const tabs = ['Account', 'Community'] as const;
+
+export const communityStackScriptFilter = {
+  '+and': [
+    { username: { '+neq': 'linode' } },
+    { username: { '+neq': 'linode-stackscripts' } },
+  ],
+  mine: false,
+};
+
+export const accountStackScriptFilter = { mine: true };

From 9a5b510341f692d29d8f37c8445f929702be5a72 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Wed, 10 Apr 2024 11:27:01 -0400
Subject: [PATCH 05/22] implement details dialog

---
 .../Tabs/StackScripts/StackScriptDialog.tsx   | 41 +++++++++++++++++++
 .../StackScripts/StackScriptSelectionList.tsx | 14 ++++++-
 .../StackScripts/StackScriptSelectionRow.tsx  | 33 ++++++++++++---
 3 files changed, 81 insertions(+), 7 deletions(-)
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDialog.tsx

diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDialog.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDialog.tsx
new file mode 100644
index 00000000000..1b8469b31a0
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDialog.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+
+import { CircleProgress } from 'src/components/CircleProgress';
+import { Dialog } from 'src/components/Dialog/Dialog';
+import { ErrorState } from 'src/components/ErrorState/ErrorState';
+import { StackScript } from 'src/components/StackScript/StackScript';
+import { useStackScriptQuery } from 'src/queries/stackscripts';
+
+interface Props {
+  id: number | undefined;
+  onClose: () => void;
+  open: boolean;
+}
+
+export const StackScriptDetailsDialog = (props: Props) => {
+  const { id, onClose, open } = props;
+
+  const { data: stackscript, error, isLoading } = useStackScriptQuery(
+    id ?? -1,
+    Boolean(id)
+  );
+
+  const title = stackscript
+    ? `${stackscript.username} / ${stackscript.label}`
+    : 'StackScript';
+
+  return (
+    <Dialog
+      fullHeight
+      fullWidth
+      maxWidth="md"
+      onClose={onClose}
+      open={open}
+      title={title}
+    >
+      {isLoading && <CircleProgress />}
+      {error && <ErrorState errorText={error[0].reason} />}
+      {stackscript && <StackScript data={stackscript} userCanModify={false} />}
+    </Dialog>
+  );
+};
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
index 622b4d9a1ad..7eb3c1ff9cb 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
 import { useController } from 'react-hook-form';
 import { Waypoint } from 'react-waypoint';
 
@@ -28,6 +28,7 @@ import {
 } from './utilities';
 
 import type { CreateLinodeRequest } from '@linode/api-v4';
+import { StackScriptDetailsDialog } from './StackScriptDialog';
 
 interface Props {
   type: StackScriptTabType;
@@ -43,6 +44,8 @@ export const StackScriptSelectionList = ({ type }: Props) => {
     name: 'stackscript_id',
   });
 
+  const [selectedStackScriptId, setSelectedStackScriptId] = useState<number>();
+
   const { params, updateParams } = useLinodeCreateQueryParams();
 
   const hasPreselectedStackScript = Boolean(params.stackScriptID);
@@ -83,7 +86,7 @@ export const StackScriptSelectionList = ({ type }: Props) => {
             <TableRow>
               <TableCell sx={{ width: 20 }}></TableCell>
               <TableCell>StackScript</TableCell>
-              <TableCell></TableCell>
+              <TableCell sx={{ minWidth: 120 }}></TableCell>
             </TableRow>
           </TableHead>
           <TableBody>
@@ -91,6 +94,7 @@ export const StackScriptSelectionList = ({ type }: Props) => {
               <StackScriptSelectionRow
                 disabled
                 isSelected={field.value === stackscript.id}
+                onOpenDetails={() => setSelectedStackScriptId(stackscript.id)}
                 onSelect={() => field.onChange(stackscript.id)}
                 stackscript={stackscript}
               />
@@ -128,6 +132,7 @@ export const StackScriptSelectionList = ({ type }: Props) => {
             <StackScriptSelectionRow
               isSelected={field.value === stackscript.id}
               key={stackscript.id}
+              onOpenDetails={() => setSelectedStackScriptId(stackscript.id)}
               onSelect={() => field.onChange(stackscript.id)}
               stackscript={stackscript}
             />
@@ -138,6 +143,11 @@ export const StackScriptSelectionList = ({ type }: Props) => {
           {hasNextPage && <Waypoint onEnter={() => fetchNextPage()} />}
         </TableBody>
       </Table>
+      <StackScriptDetailsDialog
+        id={selectedStackScriptId}
+        onClose={() => setSelectedStackScriptId(undefined)}
+        open={selectedStackScriptId !== undefined}
+      />
     </Box>
   );
 };
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
index a675200b769..bd9b254e07a 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
@@ -2,20 +2,24 @@ import React from 'react';
 
 import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction';
 import { Radio } from 'src/components/Radio/Radio';
+import { Stack } from 'src/components/Stack';
 import { TableCell } from 'src/components/TableCell';
 import { TableRow } from 'src/components/TableRow';
+import { Typography } from 'src/components/Typography';
+import { truncate } from 'src/utilities/truncate';
 
 import type { StackScript } from '@linode/api-v4';
 
 interface Props {
   disabled?: boolean;
   isSelected: boolean;
+  onOpenDetails: () => void;
   onSelect: () => void;
   stackscript: StackScript;
 }
 
 export const StackScriptSelectionRow = (props: Props) => {
-  const { disabled, isSelected, onSelect, stackscript } = props;
+  const { disabled, isSelected, onOpenDetails, onSelect, stackscript } = props;
 
   if (stackscript.username.startsWith('lke-service-account-')) {
     return null;
@@ -24,11 +28,30 @@ export const StackScriptSelectionRow = (props: Props) => {
   return (
     <TableRow>
       <TableCell>
-        <Radio checked={isSelected} disabled={disabled} onChange={onSelect} />
+        <Radio
+          checked={isSelected}
+          disabled={disabled}
+          id={`stackscript-${stackscript.id}`}
+          onChange={onSelect}
+        />
       </TableCell>
-      <TableCell>{stackscript.label}</TableCell>
-      <TableCell actionCell>
-        <InlineMenuAction actionText="Show Details" />
+      <TableCell>
+        <Stack>
+          <Typography>
+            {stackscript.username} / {stackscript.label}
+          </Typography>
+          <Typography
+            sx={(theme) => ({
+              color: theme.textColors.tableHeader,
+              fontSize: '.75rem',
+            })}
+          >
+            {truncate(stackscript.description, 100)}
+          </Typography>
+        </Stack>
+      </TableCell>
+      <TableCell actionCell sx={{ minWidth: 120 }}>
+        <InlineMenuAction actionText="Show Details" onClick={onOpenDetails} />
       </TableCell>
     </TableRow>
   );

From 1b936980fb008e289619ddd4e921398538df565c Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Wed, 10 Apr 2024 11:39:25 -0400
Subject: [PATCH 06/22] fix some query param logic

---
 .../Tabs/StackScripts/StackScriptSelectionList.tsx    | 11 ++++++++---
 .../Tabs/StackScripts/StackScriptSelectionRow.tsx     |  7 +------
 .../src/features/Linodes/LinodeCreatev2/utilities.ts  | 11 ++++++++++-
 3 files changed, 19 insertions(+), 10 deletions(-)

diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
index 7eb3c1ff9cb..d0d47f64889 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
@@ -20,15 +20,15 @@ import {
 } from 'src/queries/stackscripts';
 
 import { useLinodeCreateQueryParams } from '../../utilities';
+import { StackScriptDetailsDialog } from './StackScriptDialog';
 import { StackScriptSelectionRow } from './StackScriptSelectionRow';
 import {
-  StackScriptTabType,
   accountStackScriptFilter,
   communityStackScriptFilter,
 } from './utilities';
 
+import type { StackScriptTabType } from './utilities';
 import type { CreateLinodeRequest } from '@linode/api-v4';
-import { StackScriptDetailsDialog } from './StackScriptDialog';
 
 interface Props {
   type: StackScriptTabType;
@@ -102,7 +102,12 @@ export const StackScriptSelectionList = ({ type }: Props) => {
           </TableBody>
         </Table>
         <Box display="flex" justifyContent="flex-end">
-          <Button onClick={() => updateParams({ stackScriptID: undefined })}>
+          <Button
+            onClick={() => {
+              field.onChange(null);
+              updateParams({ stackScriptID: undefined });
+            }}
+          >
             Choose Another StackScript
           </Button>
         </Box>
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
index bd9b254e07a..c6fc066c024 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
@@ -28,12 +28,7 @@ export const StackScriptSelectionRow = (props: Props) => {
   return (
     <TableRow>
       <TableCell>
-        <Radio
-          checked={isSelected}
-          disabled={disabled}
-          id={`stackscript-${stackscript.id}`}
-          onChange={onSelect}
-        />
+        <Radio checked={isSelected} disabled={disabled} onChange={onSelect} />
       </TableCell>
       <TableCell>
         <Stack>
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
index 47197eb9b92..749ce90047b 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
@@ -28,7 +28,16 @@ export const useLinodeCreateQueryParams = () => {
   const rawParams = getQueryParamsFromQueryString(history.location.search);
 
   const updateParams = (params: Partial<LinodeCreateQueryParams>) => {
-    const newParams = new URLSearchParams({ ...rawParams, ...params });
+    const newParams = new URLSearchParams(rawParams);
+
+    for (const key in params) {
+      if (!params[key]) {
+        newParams.delete(key);
+      } else {
+        newParams.set(key, params[key]);
+      }
+    }
+
     history.push({ search: newParams.toString() });
   };
 

From 325ef5f926ca26b90b2c56baca75c43253be9f14 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Wed, 10 Apr 2024 12:05:22 -0400
Subject: [PATCH 07/22] lots of behavior changes to match production

---
 packages/api-v4/src/linodes/types.ts          |  2 +-
 .../ImageSelectv2/ImageSelectv2.tsx           |  1 +
 .../StackScripts/StackScriptSelectionList.tsx | 20 +++++++++++++++----
 .../Tabs/StackScripts/StackScripts.tsx        | 20 ++++++++++++++++---
 packages/validation/src/linodes.schema.ts     |  2 +-
 5 files changed, 36 insertions(+), 9 deletions(-)

diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts
index 2b39c104c81..70a4feeaef5 100644
--- a/packages/api-v4/src/linodes/types.ts
+++ b/packages/api-v4/src/linodes/types.ts
@@ -363,7 +363,7 @@ export interface CreateLinodeRequest {
    *
    * This field cannot be used when deploying from a Backup or a Private Image.
    */
-  stackscript_id?: number;
+  stackscript_id?: number | null;
   /**
    * A Backup ID from another Linode’s available backups.
    *
diff --git a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
index ea1369ced0a..ba6726267b0 100644
--- a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
+++ b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
@@ -58,6 +58,7 @@ export const ImageSelectv2 = (props: Props) => {
           listItemProps={props}
         />
       )}
+      clearOnBlur
       groupBy={(option) => option.vendor ?? 'My Images'}
       label="Images"
       loading={isLoading}
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
index d0d47f64889..e6ab66872b2 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
@@ -1,5 +1,5 @@
 import React, { useState } from 'react';
-import { useController } from 'react-hook-form';
+import { useController, useFormContext } from 'react-hook-form';
 import { Waypoint } from 'react-waypoint';
 
 import { Box } from 'src/components/Box';
@@ -40,7 +40,10 @@ export const StackScriptSelectionList = ({ type }: Props) => {
     orderBy: 'deployments_total',
   });
 
-  const { field } = useController<CreateLinodeRequest, 'stackscript_id'>({
+  const { control, setValue } = useFormContext<CreateLinodeRequest>();
+
+  const { field } = useController({
+    control,
     name: 'stackscript_id',
   });
 
@@ -135,17 +138,26 @@ export const StackScriptSelectionList = ({ type }: Props) => {
         <TableBody>
           {stackscripts?.map((stackscript) => (
             <StackScriptSelectionRow
+              onSelect={() => {
+                setValue('image', null);
+                field.onChange(stackscript.id);
+              }}
               isSelected={field.value === stackscript.id}
               key={stackscript.id}
               onOpenDetails={() => setSelectedStackScriptId(stackscript.id)}
-              onSelect={() => field.onChange(stackscript.id)}
               stackscript={stackscript}
             />
           ))}
           {error && <TableRowError colSpan={3} message={error[0].reason} />}
           {isLoading && <TableRowLoading columns={3} rows={25} />}
           {isFetchingNextPage && <TableRowLoading columns={3} rows={1} />}
-          {hasNextPage && <Waypoint onEnter={() => fetchNextPage()} />}
+          {hasNextPage && (
+            <TableRow>
+              <TableCell>
+                <Waypoint onEnter={() => fetchNextPage()} />
+              </TableCell>
+            </TableRow>
+          )}
         </TableBody>
       </Table>
       <StackScriptDetailsDialog
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
index 13d9546e581..fc61517c8e0 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
@@ -1,5 +1,7 @@
 import React from 'react';
+import { useFormContext } from 'react-hook-form';
 
+import { Notice } from 'src/components/Notice/Notice';
 import { Paper } from 'src/components/Paper';
 import { Stack } from 'src/components/Stack';
 import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel';
@@ -14,17 +16,29 @@ import { Images } from './Images';
 import { StackScriptSelectionList } from './StackScriptSelectionList';
 import { getStackScriptTabIndex, tabs } from './utilities';
 
+import type { CreateLinodeRequest } from '@linode/api-v4';
+
 export const StackScripts = () => {
   const { params, updateParams } = useLinodeCreateQueryParams();
+  const { formState, setValue } = useFormContext<CreateLinodeRequest>();
 
   return (
     <Stack spacing={3}>
       <Paper>
         <Typography variant="h2">Create From:</Typography>
+        {formState.errors.stackscript_id && (
+          <Notice
+            spacingBottom={0}
+            spacingTop={8}
+            text={formState.errors.stackscript_id.message}
+            variant="error"
+          />
+        )}
         <Tabs
-          onChange={(index) =>
-            updateParams({ stackScriptID: undefined, subtype: tabs[index] })
-          }
+          onChange={(index) => {
+            updateParams({ stackScriptID: undefined, subtype: tabs[index] });
+            setValue('stackscript_id', null);
+          }}
           index={getStackScriptTabIndex(params.subtype)}
         >
           <TabList>
diff --git a/packages/validation/src/linodes.schema.ts b/packages/validation/src/linodes.schema.ts
index 551e912a0a5..927e643e697 100644
--- a/packages/validation/src/linodes.schema.ts
+++ b/packages/validation/src/linodes.schema.ts
@@ -267,7 +267,7 @@ const PlacementGroupPayloadSchema = object({
 export const CreateLinodeSchema = object({
   type: string().ensure().required('Plan is required.'),
   region: string().ensure().required('Region is required.'),
-  stackscript_id: number().notRequired(),
+  stackscript_id: number().nullable().notRequired(),
   backup_id: number().notRequired(),
   swap_size: number().notRequired(),
   image: string().when('stackscript_id', {

From 8f1d6d66026a57db27113fcaacd05d4ddd1937d2 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Wed, 10 Apr 2024 12:18:55 -0400
Subject: [PATCH 08/22] fix image select clear behavior and fix table waypoint
 console error

---
 .../manager/src/components/ImageSelectv2/utilities.ts  |  6 +++---
 .../Tabs/StackScripts/StackScriptSelectionList.tsx     | 10 +++-------
 2 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/packages/manager/src/components/ImageSelectv2/utilities.ts b/packages/manager/src/components/ImageSelectv2/utilities.ts
index 23510e3aafc..fadd4bf42cf 100644
--- a/packages/manager/src/components/ImageSelectv2/utilities.ts
+++ b/packages/manager/src/components/ImageSelectv2/utilities.ts
@@ -33,7 +33,7 @@ export const getFilteredImagesForImageSelect = (
   images: Image[] | undefined,
   variant: ImageSelectVariant | undefined
 ) => {
-  return variant === 'public'
-    ? images?.filter((image) => !image.id.includes('kube'))
-    : images;
+  return variant === 'private'
+    ? images
+    : images?.filter((image) => !image.id.includes('kube'));
 };
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
index e6ab66872b2..61e7397750b 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
@@ -150,16 +150,12 @@ export const StackScriptSelectionList = ({ type }: Props) => {
           ))}
           {error && <TableRowError colSpan={3} message={error[0].reason} />}
           {isLoading && <TableRowLoading columns={3} rows={25} />}
-          {isFetchingNextPage && <TableRowLoading columns={3} rows={1} />}
-          {hasNextPage && (
-            <TableRow>
-              <TableCell>
-                <Waypoint onEnter={() => fetchNextPage()} />
-              </TableCell>
-            </TableRow>
+          {(isFetchingNextPage || hasNextPage) && (
+            <TableRowLoading columns={3} rows={1} />
           )}
         </TableBody>
       </Table>
+      {hasNextPage && <Waypoint onEnter={() => fetchNextPage()} />}
       <StackScriptDetailsDialog
         id={selectedStackScriptId}
         onClose={() => setSelectedStackScriptId(undefined)}

From c694197c4ab6d6afd189ba68f7d8002dad2d4b14 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Wed, 10 Apr 2024 12:29:44 -0400
Subject: [PATCH 09/22] improve validation for when image is `null`

---
 packages/validation/src/linodes.schema.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/validation/src/linodes.schema.ts b/packages/validation/src/linodes.schema.ts
index 927e643e697..b3456f6bb9f 100644
--- a/packages/validation/src/linodes.schema.ts
+++ b/packages/validation/src/linodes.schema.ts
@@ -272,7 +272,7 @@ export const CreateLinodeSchema = object({
   swap_size: number().notRequired(),
   image: string().when('stackscript_id', {
     is: (value?: number) => value !== undefined,
-    then: string().required('Image is required.'),
+    then: string().ensure().required('Image is required.'),
     otherwise: string().nullable().notRequired(),
   }),
   authorized_keys: array().of(string()).notRequired(),

From 5148d520676ce349c8cf7a82d9318b5ef8525c88 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Wed, 10 Apr 2024 14:02:36 -0400
Subject: [PATCH 10/22] first unit tests

---
 .../StackScriptSelectionList.test.tsx         | 88 +++++++++++++++++++
 .../StackScripts/StackScriptSelectionRow.tsx  | 36 +++++---
 2 files changed, 110 insertions(+), 14 deletions(-)
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.test.tsx

diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.test.tsx
new file mode 100644
index 00000000000..c7f3c178571
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.test.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+
+import { stackScriptFactory } from 'src/factories';
+import { makeResourcePage } from 'src/mocks/serverHandlers';
+import { HttpResponse, http, server } from 'src/mocks/testServer';
+import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';
+
+import { StackScriptSelectionList } from './StackScriptSelectionList';
+
+describe('StackScriptSelectionList', () => {
+  it('renders StackScripts returned by the API', async () => {
+    const stackscripts = stackScriptFactory.buildList(5);
+
+    server.use(
+      http.get('*/v4/linode/stackscripts', () => {
+        return HttpResponse.json(makeResourcePage(stackscripts));
+      })
+    );
+
+    const { findByText } = renderWithThemeAndHookFormContext({
+      component: <StackScriptSelectionList type="Account" />,
+    });
+
+    for (const stackscript of stackscripts) {
+      // eslint-disable-next-line no-await-in-loop
+      const item = await findByText(stackscript.label, { exact: false });
+
+      expect(item).toBeVisible();
+    }
+  });
+
+  it('renders and selected a StackScript from query params if one is specified', async () => {
+    const stackscript = stackScriptFactory.build();
+
+    server.use(
+      http.get('*/v4/linode/stackscripts/:id', () => {
+        return HttpResponse.json(stackscript);
+      })
+    );
+
+    const { findByLabelText, getByText } = renderWithThemeAndHookFormContext({
+      component: <StackScriptSelectionList type="Account" />,
+      options: {
+        MemoryRouter: {
+          initialEntries: [
+            '/linodes/create?type=StackScripts&subtype=Account&stackScriptID=921609',
+          ],
+        },
+      },
+    });
+
+    const stackscriptItem = await findByLabelText(stackscript.label, {
+      exact: false,
+    });
+
+    expect(stackscriptItem).toBeInTheDocument();
+
+    expect(getByText('Choose Another StackScript')).toBeVisible();
+  });
+
+  it('checks the selected StackScripts Radio if it is selected', async () => {
+    const stackscripts = stackScriptFactory.buildList(5);
+
+    const selectedStackScript = stackscripts[2];
+
+    server.use(
+      http.get('*/v4/linode/stackscripts', () => {
+        return HttpResponse.json(makeResourcePage(stackscripts));
+      })
+    );
+
+    const { findByLabelText } = renderWithThemeAndHookFormContext({
+      component: <StackScriptSelectionList type="Account" />,
+      useFormOptions: {
+        defaultValues: { stackscript_id: selectedStackScript.id },
+      },
+    });
+
+    const selectedStackScriptRadio = await findByLabelText(
+      selectedStackScript.label,
+      {
+        exact: false,
+      }
+    );
+
+    expect(selectedStackScriptRadio).toBeChecked();
+  });
+});
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
index c6fc066c024..a15d8b0fa05 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
 import React from 'react';
 
 import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction';
@@ -28,22 +29,29 @@ export const StackScriptSelectionRow = (props: Props) => {
   return (
     <TableRow>
       <TableCell>
-        <Radio checked={isSelected} disabled={disabled} onChange={onSelect} />
+        <Radio
+          checked={isSelected}
+          disabled={disabled}
+          id={`stackscript-${stackscript.id}`}
+          onChange={onSelect}
+        />
       </TableCell>
       <TableCell>
-        <Stack>
-          <Typography>
-            {stackscript.username} / {stackscript.label}
-          </Typography>
-          <Typography
-            sx={(theme) => ({
-              color: theme.textColors.tableHeader,
-              fontSize: '.75rem',
-            })}
-          >
-            {truncate(stackscript.description, 100)}
-          </Typography>
-        </Stack>
+        <label htmlFor={`stackscript-${stackscript.id}`}>
+          <Stack sx={{ cursor: 'pointer' }}>
+            <Typography>
+              {stackscript.username} / {stackscript.label}
+            </Typography>
+            <Typography
+              sx={(theme) => ({
+                color: theme.textColors.tableHeader,
+                fontSize: '.75rem',
+              })}
+            >
+              {truncate(stackscript.description, 100)}
+            </Typography>
+          </Stack>
+        </label>
       </TableCell>
       <TableCell actionCell sx={{ minWidth: 120 }}>
         <InlineMenuAction actionText="Show Details" onClick={onOpenDetails} />

From 94fa5f45d92a7ddb35133bde9e86a9ffcf7ce2ac Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Wed, 10 Apr 2024 15:47:59 -0400
Subject: [PATCH 11/22] add lots of unit testing

---
 .../Linodes/LinodeCreatev2/Access.test.tsx    |  24 ++--
 .../StackScriptDetailsDialog.test.tsx         |  33 ++++++
 ...ialog.tsx => StackScriptDetailsDialog.tsx} |   2 +-
 .../StackScripts/StackScriptImages.test.tsx   |  73 ++++++++++++
 .../{Images.tsx => StackScriptImages.tsx}     |   8 +-
 .../StackScriptSelectionList.test.tsx         |   2 +-
 .../StackScripts/StackScriptSelectionList.tsx |   2 +-
 .../StackScriptSelectionRow.test.tsx          | 109 ++++++++++++++++++
 .../Tabs/StackScripts/StackScripts.test.tsx   |  24 ++++
 .../Tabs/StackScripts/StackScripts.tsx        |   4 +-
 .../Tabs/StackScripts/utilities.test.ts       |  17 +++
 .../Tabs/StackScripts/utilities.ts            |  18 ++-
 .../Linodes/LinodeCreatev2/utilities.ts       |   2 +-
 13 files changed, 296 insertions(+), 22 deletions(-)
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.test.tsx
 rename packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/{StackScriptDialog.tsx => StackScriptDetailsDialog.tsx} (98%)
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.test.tsx
 rename packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/{Images.tsx => StackScriptImages.tsx} (86%)
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.test.tsx
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.test.tsx
 create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.test.ts

diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Access.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Access.test.tsx
index 93fe0b6fa6a..303005d5803 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Access.test.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Access.test.tsx
@@ -10,16 +10,20 @@ import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';
 import { Access } from './Access';
 
 describe('Access', () => {
-  it('should render a root password input', async () => {
-    const { findByLabelText } = renderWithThemeAndHookFormContext({
-      component: <Access />,
-    });
-
-    const rootPasswordInput = await findByLabelText('Root Password');
-
-    expect(rootPasswordInput).toBeVisible();
-    expect(rootPasswordInput).toBeEnabled();
-  });
+  it(
+    'should render a root password input',
+    async () => {
+      const { findByLabelText } = renderWithThemeAndHookFormContext({
+        component: <Access />,
+      });
+
+      const rootPasswordInput = await findByLabelText('Root Password');
+
+      expect(rootPasswordInput).toBeVisible();
+      expect(rootPasswordInput).toBeEnabled();
+    },
+    { timeout: 5_000 }
+  );
 
   it('should render a SSH Keys heading', async () => {
     const { getAllByText } = renderWithThemeAndHookFormContext({
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.test.tsx
new file mode 100644
index 00000000000..c1be7504701
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.test.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import { stackScriptFactory } from 'src/factories';
+import { HttpResponse, http, server } from 'src/mocks/testServer';
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { StackScriptDetailsDialog } from './StackScriptDetailsDialog';
+
+describe('StackScriptDetailsDialog', () => {
+  it('should render StackScript data from the API', async () => {
+    const stackscript = stackScriptFactory.build();
+
+    server.use(
+      http.get('*/v4/linode/stackscripts/:id', () => {
+        return HttpResponse.json(stackscript);
+      })
+    );
+
+    const { findByText } = renderWithTheme(
+      <StackScriptDetailsDialog
+        id={stackscript.id}
+        onClose={vi.fn()}
+        open={true}
+      />
+    );
+
+    await findByText(stackscript.id);
+    await findByText(stackscript.label);
+    await findByText(stackscript.username);
+    await findByText(stackscript.description);
+    await findByText(stackscript.script);
+  });
+});
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDialog.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.tsx
similarity index 98%
rename from packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDialog.tsx
rename to packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.tsx
index 1b8469b31a0..046a5fe7040 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDialog.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.tsx
@@ -17,7 +17,7 @@ export const StackScriptDetailsDialog = (props: Props) => {
 
   const { data: stackscript, error, isLoading } = useStackScriptQuery(
     id ?? -1,
-    Boolean(id)
+    id !== undefined
   );
 
   const title = stackscript
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.test.tsx
new file mode 100644
index 00000000000..c25ab318107
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.test.tsx
@@ -0,0 +1,73 @@
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+
+import { imageFactory, stackScriptFactory } from 'src/factories';
+import { makeResourcePage } from 'src/mocks/serverHandlers';
+import { HttpResponse, http, server } from 'src/mocks/testServer';
+import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';
+
+import { StackScriptImages } from './StackScriptImages';
+
+describe('Images', () => {
+  it('should render a heading', () => {
+    const { getByText } = renderWithThemeAndHookFormContext({
+      component: <StackScriptImages />,
+    });
+
+    expect(getByText('Select an Image')).toBeVisible();
+  });
+
+  it('should render an Image Select', () => {
+    const { getByLabelText } = renderWithThemeAndHookFormContext({
+      component: <StackScriptImages />,
+    });
+
+    expect(getByLabelText('Images')).toBeVisible();
+  });
+
+  it('should only render images that are compatible with the selected StackScript', async () => {
+    const images = imageFactory.buildList(5);
+
+    // For the sake of this test, we pretend this image is the only compatible image.
+    const compatibleImage = images[2];
+
+    const stackscript = stackScriptFactory.build({
+      images: [compatibleImage.id],
+    });
+
+    server.use(
+      http.get('*/v4/images', () => {
+        return HttpResponse.json(makeResourcePage(images));
+      }),
+      http.get('*/v4/linode/stackscripts/:id', () => {
+        return HttpResponse.json(stackscript);
+      })
+    );
+
+    const {
+      findByText,
+      getByLabelText,
+      queryByText,
+    } = renderWithThemeAndHookFormContext({
+      component: <StackScriptImages />,
+      useFormOptions: {
+        defaultValues: { stackscript_id: stackscript.id },
+      },
+    });
+
+    const imageSelect = getByLabelText('Images');
+
+    await userEvent.click(imageSelect);
+
+    // Verify that the compabile image is show in the dropdown.
+    await findByText(compatibleImage.label);
+
+    // Verify that the images returned by the API that are NOT compatible
+    // with this StackScript are *not* shown in the dropdown.
+    for (const image of images) {
+      if (image !== compatibleImage) {
+        expect(queryByText(image.label)).toBeNull();
+      }
+    }
+  });
+});
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx
similarity index 86%
rename from packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx
rename to packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx
index 739dc662d19..3fb9543e8c2 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/Images.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx
@@ -8,14 +8,14 @@ import { useStackScriptQuery } from 'src/queries/stackscripts';
 
 import type { CreateLinodeRequest } from '@linode/api-v4';
 
-export const Images = () => {
-  const stackscriptId = useWatch<CreateLinodeRequest>({
+export const StackScriptImages = () => {
+  const stackscriptId = useWatch<CreateLinodeRequest, 'stackscript_id'>({
     name: 'stackscript_id',
   });
 
   const { data: stackscript } = useStackScriptQuery(
-    stackscriptId,
-    Boolean(stackscriptId)
+    stackscriptId ?? -1,
+    stackscriptId !== null && stackscriptId !== undefined
   );
 
   const shouldFilterImages = !stackscript?.images.includes('any/all');
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.test.tsx
index c7f3c178571..0ea53a9acf0 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.test.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.test.tsx
@@ -58,7 +58,7 @@ describe('StackScriptSelectionList', () => {
     expect(getByText('Choose Another StackScript')).toBeVisible();
   });
 
-  it('checks the selected StackScripts Radio if it is selected', async () => {
+  it('checks the selected StackScripts Radio if it is clicked', async () => {
     const stackscripts = stackScriptFactory.buildList(5);
 
     const selectedStackScript = stackscripts[2];
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
index 61e7397750b..2790e3c2230 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionList.tsx
@@ -20,7 +20,7 @@ import {
 } from 'src/queries/stackscripts';
 
 import { useLinodeCreateQueryParams } from '../../utilities';
-import { StackScriptDetailsDialog } from './StackScriptDialog';
+import { StackScriptDetailsDialog } from './StackScriptDetailsDialog';
 import { StackScriptSelectionRow } from './StackScriptSelectionRow';
 import {
   accountStackScriptFilter,
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.test.tsx
new file mode 100644
index 00000000000..e3180ef5566
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.test.tsx
@@ -0,0 +1,109 @@
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+
+import { stackScriptFactory } from 'src/factories';
+import { renderWithTheme, wrapWithTableBody } from 'src/utilities/testHelpers';
+
+import { StackScriptSelectionRow } from './StackScriptSelectionRow';
+
+describe('StackScriptSelectionRow', () => {
+  it('render a stackscript label and username', () => {
+    const stackscript = stackScriptFactory.build();
+
+    const { getByText } = renderWithTheme(
+      wrapWithTableBody(
+        <StackScriptSelectionRow
+          isSelected={false}
+          onOpenDetails={vi.fn()}
+          onSelect={vi.fn()}
+          stackscript={stackscript}
+        />
+      )
+    );
+
+    expect(getByText(stackscript.username, { exact: false })).toBeVisible();
+    expect(getByText(stackscript.label, { exact: false })).toBeVisible();
+  });
+
+  it('render a checked Radio if isSelected is true', () => {
+    const stackscript = stackScriptFactory.build();
+
+    const { getByLabelText } = renderWithTheme(
+      wrapWithTableBody(
+        <StackScriptSelectionRow
+          isSelected={true}
+          onOpenDetails={vi.fn()}
+          onSelect={vi.fn()}
+          stackscript={stackscript}
+        />
+      )
+    );
+
+    const radio = getByLabelText(stackscript.label, { exact: false });
+
+    expect(radio).toBeChecked();
+  });
+
+  it('render an unchecked Radio if isSelected is false', () => {
+    const stackscript = stackScriptFactory.build();
+
+    const { getByLabelText } = renderWithTheme(
+      wrapWithTableBody(
+        <StackScriptSelectionRow
+          isSelected={false}
+          onOpenDetails={vi.fn()}
+          onSelect={vi.fn()}
+          stackscript={stackscript}
+        />
+      )
+    );
+
+    const radio = getByLabelText(stackscript.label, { exact: false });
+
+    expect(radio).not.toBeChecked();
+  });
+
+  it('should call onSelect when a stackscript is clicked', async () => {
+    const stackscript = stackScriptFactory.build();
+    const onSelect = vi.fn();
+
+    const { getByLabelText } = renderWithTheme(
+      wrapWithTableBody(
+        <StackScriptSelectionRow
+          isSelected={false}
+          onOpenDetails={vi.fn()}
+          onSelect={onSelect}
+          stackscript={stackscript}
+        />
+      )
+    );
+
+    const radio = getByLabelText(stackscript.label, { exact: false });
+
+    await userEvent.click(radio);
+
+    expect(onSelect).toHaveBeenCalled();
+  });
+
+  it('should call onOpenDetails when a stackscript details button is clicked', async () => {
+    const stackscript = stackScriptFactory.build();
+    const onOpenDetails = vi.fn();
+
+    const { getByText } = renderWithTheme(
+      wrapWithTableBody(
+        <StackScriptSelectionRow
+          isSelected={false}
+          onOpenDetails={onOpenDetails}
+          onSelect={vi.fn()}
+          stackscript={stackscript}
+        />
+      )
+    );
+
+    const detailsButton = getByText('Show Details');
+
+    await userEvent.click(detailsButton);
+
+    expect(onOpenDetails).toHaveBeenCalled();
+  });
+});
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.test.tsx
new file mode 100644
index 00000000000..c81b02dc195
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.test.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';
+
+import { StackScripts } from './StackScripts';
+
+describe('StackScripts', () => {
+  it('should render a StackScript section', () => {
+    const { getByText } = renderWithThemeAndHookFormContext({
+      component: <StackScripts />,
+    });
+
+    expect(getByText('Account StackScripts')).toBeVisible();
+    expect(getByText('Community StackScripts')).toBeVisible();
+  });
+
+  it('should render an Image section', () => {
+    const { getByText } = renderWithThemeAndHookFormContext({
+      component: <StackScripts />,
+    });
+
+    expect(getByText('Select an Image')).toBeVisible();
+  });
+});
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
index fc61517c8e0..8de7bc5a5c5 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
@@ -12,7 +12,7 @@ import { Tabs } from 'src/components/Tabs/Tabs';
 import { Typography } from 'src/components/Typography';
 
 import { useLinodeCreateQueryParams } from '../../utilities';
-import { Images } from './Images';
+import { StackScriptImages } from './StackScriptImages';
 import { StackScriptSelectionList } from './StackScriptSelectionList';
 import { getStackScriptTabIndex, tabs } from './utilities';
 
@@ -55,7 +55,7 @@ export const StackScripts = () => {
           </TabPanels>
         </Tabs>
       </Paper>
-      <Images />
+      <StackScriptImages />
     </Stack>
   );
 };
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.test.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.test.ts
new file mode 100644
index 00000000000..6a3831ee9b2
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.test.ts
@@ -0,0 +1,17 @@
+import { getStackScriptTabIndex } from './utilities';
+
+describe('getStackScriptTabIndex', () => {
+  it('should return 0 for Account', () => {
+    expect(getStackScriptTabIndex('Account')).toBe(0);
+  });
+  it('should return 1 for Community', () => {
+    expect(getStackScriptTabIndex('Community')).toBe(1);
+  });
+  it('should return 0 for an unexpected value', () => {
+    // @ts-expect-error intentionally passing an unexpected value
+    expect(getStackScriptTabIndex('hey')).toBe(0);
+  });
+  it('should return 0 for undefined (default to first tab)', () => {
+    expect(getStackScriptTabIndex(undefined)).toBe(0);
+  });
+});
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts
index 4bbf74f5f42..41bb6d1c721 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/utilities.ts
@@ -1,5 +1,13 @@
 export type StackScriptTabType = 'Account' | 'Community';
 
+export const tabs = ['Account', 'Community'] as const;
+
+/**
+ * Returns the index of the currently selected StackScripts tab
+ *
+ * @param tab the current tab. Currently, this value comes from 'subtype' query param on the Linode Create flow.
+ * @returns the index of the selected tab
+ */
 export const getStackScriptTabIndex = (tab: StackScriptTabType | undefined) => {
   if (tab === undefined) {
     return 0;
@@ -14,8 +22,11 @@ export const getStackScriptTabIndex = (tab: StackScriptTabType | undefined) => {
   return tabIndex;
 };
 
-export const tabs = ['Account', 'Community'] as const;
-
+/**
+ * API filter for fetching community StackScripts
+ *
+ * We omit some usernames so that Marketplace StackScripts don't show up.
+ */
 export const communityStackScriptFilter = {
   '+and': [
     { username: { '+neq': 'linode' } },
@@ -24,4 +35,7 @@ export const communityStackScriptFilter = {
   mine: false,
 };
 
+/**
+ * API filter for fetching account StackScripts
+ */
 export const accountStackScriptFilter = { mine: true };
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
index 749ce90047b..f810b97032f 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
@@ -147,7 +147,7 @@ export const defaultValues = async (): Promise<CreateLinodeRequest> => {
     : undefined;
 
   return {
-    image: 'linode/debian11',
+    image: stackScriptID ? undefined : 'linode/debian11',
     interfaces: [
       {
         ipam_address: '',

From e4e71f69b634dd5121b01de29ebe3b05202beccc Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Wed, 10 Apr 2024 16:12:50 -0400
Subject: [PATCH 12/22] add stackscript event handler

---
 packages/manager/src/hooks/useEventHandlers.ts |  5 +++++
 packages/manager/src/queries/stackscripts.ts   | 16 ++++++++++++++++
 2 files changed, 21 insertions(+)

diff --git a/packages/manager/src/hooks/useEventHandlers.ts b/packages/manager/src/hooks/useEventHandlers.ts
index dc674252841..659bec203e9 100644
--- a/packages/manager/src/hooks/useEventHandlers.ts
+++ b/packages/manager/src/hooks/useEventHandlers.ts
@@ -15,6 +15,7 @@ import { volumeEventsHandler } from 'src/queries/volumes';
 
 import type { Event } from '@linode/api-v4';
 import type { QueryClient } from '@tanstack/react-query';
+import { stackScriptEventHandler } from 'src/queries/stackscripts';
 
 export interface EventHandlerData {
   event: Event;
@@ -76,6 +77,10 @@ export const eventHandlers: {
     filter: (event) => event.action.startsWith('disk'),
     handler: diskEventHandler,
   },
+  {
+    filter: (event) => event.action.startsWith('stackscript'),
+    handler: stackScriptEventHandler,
+  },
 ];
 
 export const useEventHandlers = () => {
diff --git a/packages/manager/src/queries/stackscripts.ts b/packages/manager/src/queries/stackscripts.ts
index b2eb2159179..5799751d14b 100644
--- a/packages/manager/src/queries/stackscripts.ts
+++ b/packages/manager/src/queries/stackscripts.ts
@@ -13,6 +13,7 @@ import { createQueryKeys } from '@lukemorales/query-key-factory';
 import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
 
 import { getOneClickApps } from 'src/features/StackScripts/stackScriptUtils';
+import { EventHandlerData } from 'src/hooks/useEventHandlers';
 import { getAll } from 'src/utilities/getAll';
 
 import { queryPresets } from './base';
@@ -66,3 +67,18 @@ export const useStackScriptsInfiniteQuery = (
       return page + 1;
     },
   });
+
+export const stackScriptEventHandler = ({
+  event,
+  queryClient,
+}: EventHandlerData) => {
+  // Keep the infinite store up to date
+  queryClient.invalidateQueries(stackscriptQueries.infinite._def);
+
+  // If the event has a StackScript entity attached, invalidate it
+  if (event.entity?.id) {
+    queryClient.invalidateQueries(
+      stackscriptQueries.stackscript(event.entity.id).queryKey
+    );
+  }
+};

From f56e4ad16d281c5b8ee1a20c9cd1f1ec2992f18b Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Wed, 10 Apr 2024 17:45:56 -0400
Subject: [PATCH 13/22] hook up validation packages for realtime validation

---
 packages/manager/package.json                             | 1 +
 .../manager/src/features/Linodes/LinodeCreatev2/index.tsx | 8 +++++++-
 yarn.lock                                                 | 5 +++++
 3 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/packages/manager/package.json b/packages/manager/package.json
index 95ba61b9112..8a7751e8327 100644
--- a/packages/manager/package.json
+++ b/packages/manager/package.json
@@ -16,6 +16,7 @@
   "dependencies": {
     "@emotion/react": "^11.11.1",
     "@emotion/styled": "^11.11.0",
+    "@hookform/resolvers": "2.9.11",
     "@linode/api-v4": "*",
     "@linode/validation": "*",
     "@lukemorales/query-key-factory": "^1.3.4",
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
index f248b30fa4a..3e39b009799 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
@@ -1,3 +1,5 @@
+import { yupResolver } from '@hookform/resolvers/yup';
+import { CreateLinodeSchema } from '@linode/validation';
 import React from 'react';
 import { FormProvider, useForm } from 'react-hook-form';
 import { useHistory } from 'react-router-dom';
@@ -37,7 +39,11 @@ import type { CreateLinodeRequest } from '@linode/api-v4';
 import type { SubmitHandler } from 'react-hook-form';
 
 export const LinodeCreatev2 = () => {
-  const methods = useForm<CreateLinodeRequest>({ defaultValues });
+  const methods = useForm<CreateLinodeRequest>({
+    defaultValues,
+    resolver: yupResolver(CreateLinodeSchema),
+    mode: 'onChange',
+  });
   const history = useHistory();
 
   const { mutateAsync: createLinode } = useCreateLinodeMutation();
diff --git a/yarn.lock b/yarn.lock
index 878b7763065..30739364956 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1817,6 +1817,11 @@
   resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
   integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
 
+"@hookform/resolvers@2.9.11":
+  version "2.9.11"
+  resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-2.9.11.tgz#9ce96e7746625a89239f68ca57c4f654264c17ef"
+  integrity sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==
+
 "@humanwhocodes/config-array@^0.11.13":
   version "0.11.14"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"

From c464078ae8a6465cca1961897336c19594fcc504 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Wed, 10 Apr 2024 17:54:56 -0400
Subject: [PATCH 14/22] use default validation behavior

---
 packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
index 65e2f044fee..9886d896043 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
@@ -44,8 +44,8 @@ export const LinodeCreatev2 = () => {
   const methods = useForm<CreateLinodeRequest>({
     defaultValues,
     resolver: yupResolver(CreateLinodeSchema),
-    mode: 'onChange',
   });
+
   const history = useHistory();
 
   const { mutateAsync: createLinode } = useCreateLinodeMutation();

From de158af0f8cb764747628332dd4d22b5f22f12f3 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Thu, 11 Apr 2024 00:37:43 -0400
Subject: [PATCH 15/22] Revert "hook up validation packages for realtime
 validation"

This reverts commit f56e4ad16d281c5b8ee1a20c9cd1f1ec2992f18b.
---
 packages/manager/package.json                             | 1 -
 .../manager/src/features/Linodes/LinodeCreatev2/index.tsx | 8 +-------
 yarn.lock                                                 | 5 -----
 3 files changed, 1 insertion(+), 13 deletions(-)

diff --git a/packages/manager/package.json b/packages/manager/package.json
index 8a7751e8327..95ba61b9112 100644
--- a/packages/manager/package.json
+++ b/packages/manager/package.json
@@ -16,7 +16,6 @@
   "dependencies": {
     "@emotion/react": "^11.11.1",
     "@emotion/styled": "^11.11.0",
-    "@hookform/resolvers": "2.9.11",
     "@linode/api-v4": "*",
     "@linode/validation": "*",
     "@lukemorales/query-key-factory": "^1.3.4",
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
index 9886d896043..60f48af6a91 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
@@ -1,5 +1,3 @@
-import { yupResolver } from '@hookform/resolvers/yup';
-import { CreateLinodeSchema } from '@linode/validation';
 import React from 'react';
 import { FormProvider, useForm } from 'react-hook-form';
 import { useHistory } from 'react-router-dom';
@@ -41,11 +39,7 @@ import type { CreateLinodeRequest } from '@linode/api-v4';
 import type { SubmitHandler } from 'react-hook-form';
 
 export const LinodeCreatev2 = () => {
-  const methods = useForm<CreateLinodeRequest>({
-    defaultValues,
-    resolver: yupResolver(CreateLinodeSchema),
-  });
-
+  const methods = useForm<CreateLinodeRequest>({ defaultValues });
   const history = useHistory();
 
   const { mutateAsync: createLinode } = useCreateLinodeMutation();
diff --git a/yarn.lock b/yarn.lock
index 30739364956..878b7763065 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1817,11 +1817,6 @@
   resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
   integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
 
-"@hookform/resolvers@2.9.11":
-  version "2.9.11"
-  resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-2.9.11.tgz#9ce96e7746625a89239f68ca57c4f654264c17ef"
-  integrity sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==
-
 "@humanwhocodes/config-array@^0.11.13":
   version "0.11.14"
   resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"

From e7f668b84dfc07bd97a6fe53a1df4066e5a981a3 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Thu, 11 Apr 2024 16:41:27 -0400
Subject: [PATCH 16/22] handle resets when switching tabs

---
 .../Tabs/StackScripts/StackScriptImages.tsx   | 24 +++--
 .../Tabs/StackScripts/StackScripts.tsx        |  9 +-
 .../features/Linodes/LinodeCreatev2/index.tsx |  9 +-
 .../Linodes/LinodeCreatev2/utilities.ts       | 98 +++++++++++++++----
 4 files changed, 112 insertions(+), 28 deletions(-)

diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx
index 3fb9543e8c2..d82bb2fbb42 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx
@@ -6,34 +6,44 @@ import { Paper } from 'src/components/Paper';
 import { Typography } from 'src/components/Typography';
 import { useStackScriptQuery } from 'src/queries/stackscripts';
 
-import type { CreateLinodeRequest } from '@linode/api-v4';
+import type { CreateLinodeRequest, Image } from '@linode/api-v4';
 
 export const StackScriptImages = () => {
   const stackscriptId = useWatch<CreateLinodeRequest, 'stackscript_id'>({
     name: 'stackscript_id',
   });
 
+  const hasStackScriptSelected =
+    stackscriptId !== null && stackscriptId !== undefined;
+
   const { data: stackscript } = useStackScriptQuery(
     stackscriptId ?? -1,
-    stackscriptId !== null && stackscriptId !== undefined
+    hasStackScriptSelected
   );
 
   const shouldFilterImages = !stackscript?.images.includes('any/all');
 
   const imageSelectVariant = shouldFilterImages ? 'public' : 'all';
 
+  const imageFilter = shouldFilterImages
+    ? (image: Image) => stackscript?.images.includes(image.id) ?? false
+    : undefined;
+
+  const helperText = !hasStackScriptSelected
+    ? 'Select a StackScript to see compatible Images.'
+    : undefined;
+
   return (
     <Paper>
       <Typography variant="h2">Select an Image</Typography>
       <Controller<CreateLinodeRequest, 'image'>
         render={({ field, fieldState }) => (
           <ImageSelectv2
-            filter={
-              shouldFilterImages
-                ? (image) => stackscript?.images.includes(image.id) ?? false
-                : undefined
-            }
+            disabled={!hasStackScriptSelected}
             errorText={fieldState.error?.message}
+            filter={imageFilter}
+            helperText={helperText}
+            noOptionsText="No Compatible Images Available"
             onChange={(e, image) => field.onChange(image?.id ?? null)}
             value={field.value}
             variant={imageSelectVariant}
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
index 8de7bc5a5c5..3f4e4f7ed3d 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
@@ -20,7 +20,7 @@ import type { CreateLinodeRequest } from '@linode/api-v4';
 
 export const StackScripts = () => {
   const { params, updateParams } = useLinodeCreateQueryParams();
-  const { formState, setValue } = useFormContext<CreateLinodeRequest>();
+  const { formState, reset } = useFormContext<CreateLinodeRequest>();
 
   return (
     <Stack spacing={3}>
@@ -37,7 +37,12 @@ export const StackScripts = () => {
         <Tabs
           onChange={(index) => {
             updateParams({ stackScriptID: undefined, subtype: tabs[index] });
-            setValue('stackscript_id', null);
+            reset((prev) => ({
+              ...prev,
+              image: null,
+              stackscript_data: null,
+              stackscript_id: null,
+            }));
           }}
           index={getStackScriptTabIndex(params.subtype)}
         >
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
index 60f48af6a91..4fdf4e22dfc 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
@@ -27,6 +27,7 @@ import { StackScripts } from './Tabs/StackScripts/StackScripts';
 import { UserData } from './UserData/UserData';
 import {
   defaultValues,
+  defaultValuesMap,
   getLinodeCreatePayload,
   getTabIndex,
   tabs,
@@ -62,7 +63,7 @@ export const LinodeCreatev2 = () => {
     }
   };
 
-  const { params, updateParams } = useLinodeCreateQueryParams();
+  const { params, setParams } = useLinodeCreateQueryParams();
 
   const currentTabIndex = getTabIndex(params.type);
 
@@ -78,8 +79,12 @@ export const LinodeCreatev2 = () => {
         <Error />
         <Stack gap={3}>
           <Tabs
+            onChange={(index) => {
+              const newTab = tabs[index];
+              setParams({ type: newTab });
+              methods.reset(defaultValuesMap[newTab]);
+            }}
             index={currentTabIndex}
-            onChange={(index) => updateParams({ type: tabs[index] })}
           >
             <TabList>
               <Tab>Distributions</Tab>
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
index 6c6d35d59dd..dc8adfd788a 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
@@ -12,6 +12,7 @@ import type { CreateLinodeRequest, InterfacePayload } from '@linode/api-v4';
  * This interface is used to type the query params on the Linode Create flow.
  */
 interface LinodeCreateQueryParams {
+  imageID: string | undefined;
   stackScriptID: string | undefined;
   subtype: StackScriptTabType | undefined;
   type: LinodeCreateType | undefined;
@@ -27,6 +28,9 @@ export const useLinodeCreateQueryParams = () => {
 
   const rawParams = getQueryParamsFromQueryString(history.location.search);
 
+  /**
+   * Updates query params
+   */
   const updateParams = (params: Partial<LinodeCreateQueryParams>) => {
     const newParams = new URLSearchParams(rawParams);
 
@@ -41,7 +45,17 @@ export const useLinodeCreateQueryParams = () => {
     history.push({ search: newParams.toString() });
   };
 
+  /**
+   * Replaces query params with the provided values
+   */
+  const setParams = (params: Partial<LinodeCreateQueryParams>) => {
+    const newParams = new URLSearchParams(params);
+
+    history.push({ search: newParams.toString() });
+  };
+
   const params = {
+    imageID: rawParams.imageID as string | undefined,
     stackScriptID: rawParams.stackScriptID
       ? Number(rawParams.stackScriptID)
       : undefined,
@@ -49,7 +63,7 @@ export const useLinodeCreateQueryParams = () => {
     type: rawParams.type as LinodeCreateType | undefined,
   };
 
-  return { params, updateParams };
+  return { params, setParams, updateParams };
 };
 
 /**
@@ -155,6 +169,24 @@ export const getInterfacesPayload = (
   return undefined;
 };
 
+const defaultVPCInterface = {
+  ipam_address: '',
+  label: '',
+  purpose: 'vpc',
+} as const;
+
+const defaultVLANInterface = {
+  ipam_address: '',
+  label: '',
+  purpose: 'vlan',
+} as const;
+
+const defaultPublicInterface = {
+  ipam_address: '',
+  label: '',
+  purpose: 'public',
+} as const;
+
 export const defaultValues = async (): Promise<CreateLinodeRequest> => {
   const queryParams = getQueryParamsFromQueryString(window.location.search);
 
@@ -162,27 +194,59 @@ export const defaultValues = async (): Promise<CreateLinodeRequest> => {
     ? Number(queryParams.stackScriptID)
     : undefined;
 
+  const imageID = queryParams.imageID;
+
   return {
-    image: stackScriptID ? undefined : 'linode/debian11',
+    image: stackScriptID ? undefined : imageID ?? 'linode/debian11',
     interfaces: [
-      {
-        ipam_address: '',
-        label: '',
-        purpose: 'vpc',
-      },
-      {
-        ipam_address: '',
-        label: '',
-        purpose: 'vlan',
-      },
-      {
-        ipam_address: '',
-        label: '',
-        purpose: 'public',
-      },
+      defaultVPCInterface,
+      defaultVLANInterface,
+      defaultPublicInterface,
     ],
     region: '',
     stackscript_id: stackScriptID,
     type: '',
   };
 };
+
+const defaultValuesForImages = {
+  interfaces: [
+    defaultVPCInterface,
+    defaultVLANInterface,
+    defaultPublicInterface,
+  ],
+  region: '',
+  type: '',
+};
+
+const defaultValuesForDistributions = {
+  image: 'linode/debian11',
+  interfaces: [
+    defaultVPCInterface,
+    defaultVLANInterface,
+    defaultPublicInterface,
+  ],
+  region: '',
+  type: '',
+};
+
+const defaultValuesForStackScripts = {
+  image: undefined,
+  interfaces: [
+    defaultVPCInterface,
+    defaultVLANInterface,
+    defaultPublicInterface,
+  ],
+  region: '',
+  stackscript_id: null,
+  type: '',
+};
+
+export const defaultValuesMap: Record<LinodeCreateType, CreateLinodeRequest> = {
+  Backups: defaultValuesForImages,
+  'Clone Linode': defaultValuesForImages,
+  Distributions: defaultValuesForDistributions,
+  Images: defaultValuesForImages,
+  'One-Click': defaultValuesForImages,
+  StackScripts: defaultValuesForStackScripts,
+};

From 5ed5f57f85425870a60c341891a7ab94ded36266 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Thu, 11 Apr 2024 16:48:28 -0400
Subject: [PATCH 17/22] add changesets

---
 packages/api-v4/.changeset/pr-10367-changed-1712868469030.md | 5 +++++
 .../.changeset/pr-10367-upcoming-features-1712868443784.md   | 5 +++++
 .../validation/.changeset/pr-10367-changed-1712868495330.md  | 5 +++++
 3 files changed, 15 insertions(+)
 create mode 100644 packages/api-v4/.changeset/pr-10367-changed-1712868469030.md
 create mode 100644 packages/manager/.changeset/pr-10367-upcoming-features-1712868443784.md
 create mode 100644 packages/validation/.changeset/pr-10367-changed-1712868495330.md

diff --git a/packages/api-v4/.changeset/pr-10367-changed-1712868469030.md b/packages/api-v4/.changeset/pr-10367-changed-1712868469030.md
new file mode 100644
index 00000000000..696ea3d2fbc
--- /dev/null
+++ b/packages/api-v4/.changeset/pr-10367-changed-1712868469030.md
@@ -0,0 +1,5 @@
+---
+"@linode/api-v4": Changed
+---
+
+Allow `stackscript_id` to be `null` in `CreateLinodeRequest` ([#10367](https://github.com/linode/manager/pull/10367))
diff --git a/packages/manager/.changeset/pr-10367-upcoming-features-1712868443784.md b/packages/manager/.changeset/pr-10367-upcoming-features-1712868443784.md
new file mode 100644
index 00000000000..38e31bf052a
--- /dev/null
+++ b/packages/manager/.changeset/pr-10367-upcoming-features-1712868443784.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Upcoming Features
+---
+
+Linode Create Refactor - StackScripts ([#10367](https://github.com/linode/manager/pull/10367))
diff --git a/packages/validation/.changeset/pr-10367-changed-1712868495330.md b/packages/validation/.changeset/pr-10367-changed-1712868495330.md
new file mode 100644
index 00000000000..c5e47f8f393
--- /dev/null
+++ b/packages/validation/.changeset/pr-10367-changed-1712868495330.md
@@ -0,0 +1,5 @@
+---
+"@linode/validation": Changed
+---
+
+Allow `stackscript_id` to be `null` in `CreateLinodeSchema` ([#10367](https://github.com/linode/manager/pull/10367))

From 658e7ec681f4011f080e2924801e1e4c5f50a326 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Fri, 12 Apr 2024 09:09:37 -0400
Subject: [PATCH 18/22] add some comments

---
 .../StackScripts/StackScriptDetailsDialog.tsx |  9 ++++++++
 .../Tabs/StackScripts/StackScripts.tsx        | 22 +++++++++++--------
 .../features/Linodes/LinodeCreatev2/index.tsx | 17 +++++++-------
 .../Linodes/LinodeCreatev2/utilities.ts       | 11 +++++++++-
 4 files changed, 41 insertions(+), 18 deletions(-)

diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.tsx
index 046a5fe7040..7e96c887204 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptDetailsDialog.tsx
@@ -7,8 +7,17 @@ import { StackScript } from 'src/components/StackScript/StackScript';
 import { useStackScriptQuery } from 'src/queries/stackscripts';
 
 interface Props {
+  /**
+   * The id of the StackScript
+   */
   id: number | undefined;
+  /**
+   * Function called when when the dialog is closed
+   */
   onClose: () => void;
+  /**
+   * Controls the open/close state of the dialog
+   */
   open: boolean;
 }
 
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
index 3f4e4f7ed3d..d52ffc6fe69 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScripts.tsx
@@ -22,6 +22,18 @@ export const StackScripts = () => {
   const { params, updateParams } = useLinodeCreateQueryParams();
   const { formState, reset } = useFormContext<CreateLinodeRequest>();
 
+  const onTabChange = (index: number) => {
+    // Update the "subtype" query param. (This switches between "Community" and "Account" tabs).
+    updateParams({ stackScriptID: undefined, subtype: tabs[index] });
+    // Reset the selected image, the selected StackScript, and the StackScript data when changing tabs.
+    reset((prev) => ({
+      ...prev,
+      image: null,
+      stackscript_data: null,
+      stackscript_id: null,
+    }));
+  };
+
   return (
     <Stack spacing={3}>
       <Paper>
@@ -35,16 +47,8 @@ export const StackScripts = () => {
           />
         )}
         <Tabs
-          onChange={(index) => {
-            updateParams({ stackScriptID: undefined, subtype: tabs[index] });
-            reset((prev) => ({
-              ...prev,
-              image: null,
-              stackscript_data: null,
-              stackscript_id: null,
-            }));
-          }}
           index={getStackScriptTabIndex(params.subtype)}
+          onChange={onTabChange}
         >
           <TabList>
             <Tab>Account StackScripts</Tab>
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
index 4fdf4e22dfc..8e9532947da 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx
@@ -67,6 +67,14 @@ export const LinodeCreatev2 = () => {
 
   const currentTabIndex = getTabIndex(params.type);
 
+  const onTabChange = (index: number) => {
+    const newTab = tabs[index];
+    // Update tab "type" query param. (This changes the selected tab)
+    setParams({ type: newTab });
+    // Reset the form values
+    methods.reset(defaultValuesMap[newTab]);
+  };
+
   return (
     <FormProvider {...methods}>
       <DocumentTitleSegment segment="Create a Linode" />
@@ -78,14 +86,7 @@ export const LinodeCreatev2 = () => {
       <form onSubmit={methods.handleSubmit(onSubmit)}>
         <Error />
         <Stack gap={3}>
-          <Tabs
-            onChange={(index) => {
-              const newTab = tabs[index];
-              setParams({ type: newTab });
-              methods.reset(defaultValuesMap[newTab]);
-            }}
-            index={currentTabIndex}
-          >
+          <Tabs index={currentTabIndex} onChange={onTabChange}>
             <TabList>
               <Tab>Distributions</Tab>
               <Tab>Marketplace</Tab>
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
index dc8adfd788a..2e6798ba9c2 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts
@@ -187,6 +187,12 @@ const defaultPublicInterface = {
   purpose: 'public',
 } as const;
 
+/**
+ * This function initializes the Linode Create flow form
+ * when the form mounts.
+ *
+ * The default values are dependent on the query params present.
+ */
 export const defaultValues = async (): Promise<CreateLinodeRequest> => {
   const queryParams = getQueryParamsFromQueryString(window.location.search);
 
@@ -197,7 +203,7 @@ export const defaultValues = async (): Promise<CreateLinodeRequest> => {
   const imageID = queryParams.imageID;
 
   return {
-    image: stackScriptID ? undefined : imageID ?? 'linode/debian11',
+    image: stackScriptID ? imageID : imageID ?? 'linode/debian11',
     interfaces: [
       defaultVPCInterface,
       defaultVLANInterface,
@@ -242,6 +248,9 @@ const defaultValuesForStackScripts = {
   type: '',
 };
 
+/**
+ * A map that conatins default values for each Tab of the Linode Create flow.
+ */
 export const defaultValuesMap: Record<LinodeCreateType, CreateLinodeRequest> = {
   Backups: defaultValuesForImages,
   'Clone Linode': defaultValuesForImages,

From fe974cb4431d00aee3eb3f6d887a3691cd8c0db4 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Fri, 12 Apr 2024 10:30:42 -0400
Subject: [PATCH 19/22] bold label and pre-select if only one option

---
 .../ImageSelectv2/ImageSelectv2.tsx           | 24 +++++++++++++++++--
 .../LinodeCreatev2/Tabs/Distributions.tsx     |  2 +-
 .../Linodes/LinodeCreatev2/Tabs/Images.tsx    |  2 +-
 .../Tabs/StackScripts/StackScriptImages.tsx   |  3 ++-
 .../StackScripts/StackScriptSelectionRow.tsx  |  8 ++++++-
 5 files changed, 33 insertions(+), 6 deletions(-)

diff --git a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
index ba6726267b0..c4b0942f6bc 100644
--- a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
+++ b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
@@ -15,11 +15,22 @@ import type { EnhancedAutocompleteProps } from 'src/components/Autocomplete/Auto
 export type ImageSelectVariant = 'all' | 'private' | 'public';
 
 interface Props
-  extends Omit<Partial<EnhancedAutocompleteProps<Image>>, 'value'> {
+  extends Omit<
+    Partial<EnhancedAutocompleteProps<Image>>,
+    'onChange' | 'value'
+  > {
   /**
    * Optional filter function applied to the options.
    */
   filter?: (image: Image) => boolean;
+  /**
+   * Called when the value is changed
+   */
+  onChange: (image: Image | null) => void;
+  /**
+   * If there is only one avaiblable option, selected it by default.
+   */
+  selectIfOnlyOneOption?: boolean;
   /**
    * The ID of the selected image
    */
@@ -34,7 +45,7 @@ interface Props
 }
 
 export const ImageSelectv2 = (props: Props) => {
-  const { filter, variant, ...rest } = props;
+  const { filter, onChange, selectIfOnlyOneOption, variant, ...rest } = props;
 
   const { data: images, error, isLoading } = useAllImagesQuery(
     {},
@@ -48,6 +59,14 @@ export const ImageSelectv2 = (props: Props) => {
 
   const value = images?.find((i) => i.id === props.value);
 
+  if (
+    filteredOptions?.length === 1 &&
+    props.onChange &&
+    selectIfOnlyOneOption
+  ) {
+    props.onChange(filteredOptions[0]);
+  }
+
   return (
     <Autocomplete
       renderOption={(props, option, state) => (
@@ -66,6 +85,7 @@ export const ImageSelectv2 = (props: Props) => {
       placeholder="Choose an image"
       {...rest}
       errorText={rest.errorText ?? error?.[0].reason}
+      onChange={(e, image) => onChange(image)}
       value={value ?? null}
     />
   );
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Distributions.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Distributions.tsx
index 67fb49eef7f..d0121d60ca6 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Distributions.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Distributions.tsx
@@ -23,7 +23,7 @@ export const Distributions = () => {
       <ImageSelectv2
         disabled={isCreateLinodeRestricted}
         errorText={fieldState.error?.message}
-        onChange={(_, image) => field.onChange(image?.id ?? null)}
+        onChange={(image) => field.onChange(image?.id ?? null)}
         value={field.value}
         variant="public"
       />
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.tsx
index 84ccb844c03..272a79a94f1 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.tsx
@@ -23,7 +23,7 @@ export const Images = () => {
       <ImageSelectv2
         disabled={isCreateLinodeRestricted}
         errorText={fieldState.error?.message}
-        onChange={(_, image) => field.onChange(image?.id ?? null)}
+        onChange={(image) => field.onChange(image?.id ?? null)}
         value={field.value}
         variant="private"
       />
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx
index d82bb2fbb42..dfbac510d91 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.tsx
@@ -44,7 +44,8 @@ export const StackScriptImages = () => {
             filter={imageFilter}
             helperText={helperText}
             noOptionsText="No Compatible Images Available"
-            onChange={(e, image) => field.onChange(image?.id ?? null)}
+            onChange={(image) => field.onChange(image?.id ?? null)}
+            selectIfOnlyOneOption
             value={field.value}
             variant={imageSelectVariant}
           />
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
index a15d8b0fa05..ed33f0361a4 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
@@ -40,7 +40,13 @@ export const StackScriptSelectionRow = (props: Props) => {
         <label htmlFor={`stackscript-${stackscript.id}`}>
           <Stack sx={{ cursor: 'pointer' }}>
             <Typography>
-              {stackscript.username} / {stackscript.label}
+              {stackscript.username} /{' '}
+              <Typography
+                component="span"
+                fontFamily={(theme) => theme.font.bold}
+              >
+                {stackscript.label}
+              </Typography>
             </Typography>
             <Typography
               sx={(theme) => ({

From d53719aef324c3736df0a4a90c9056fb69decb57 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Fri, 12 Apr 2024 10:40:19 -0400
Subject: [PATCH 20/22] improve ux

---
 .../manager/src/components/ImageSelectv2/ImageSelectv2.tsx    | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
index c4b0942f6bc..49cc3774bff 100644
--- a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
+++ b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
@@ -84,6 +84,10 @@ export const ImageSelectv2 = (props: Props) => {
       options={filteredOptions ?? []}
       placeholder="Choose an image"
       {...rest}
+      disableClearable={
+        rest.disableClearable ??
+        (selectIfOnlyOneOption && filteredOptions?.length === 1)
+      }
       errorText={rest.errorText ?? error?.[0].reason}
       onChange={(e, image) => onChange(image)}
       value={value ?? null}

From 884b4b4c8d494a171c7dc603913fe2b007547e1e Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Fri, 12 Apr 2024 10:59:57 -0400
Subject: [PATCH 21/22] fix unit tests

---
 .../src/components/ImageSelectv2/ImageSelectv2.test.tsx   | 7 +------
 .../src/components/ImageSelectv2/ImageSelectv2.tsx        | 8 ++++++--
 2 files changed, 7 insertions(+), 8 deletions(-)

diff --git a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.test.tsx b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.test.tsx
index a18f808dabb..5e3862d1684 100644
--- a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.test.tsx
+++ b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.test.tsx
@@ -65,12 +65,7 @@ describe('ImageSelectv2', () => {
 
     await userEvent.click(imageOption);
 
-    expect(onChange).toHaveBeenCalledWith(
-      expect.anything(),
-      image,
-      'selectOption',
-      expect.anything()
-    );
+    expect(onChange).toHaveBeenCalledWith(image);
   });
 
   it('should correctly initialize with a default value', async () => {
diff --git a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
index 49cc3774bff..363ad62d11e 100644
--- a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
+++ b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.tsx
@@ -26,7 +26,7 @@ interface Props
   /**
    * Called when the value is changed
    */
-  onChange: (image: Image | null) => void;
+  onChange?: (image: Image | null) => void;
   /**
    * If there is only one avaiblable option, selected it by default.
    */
@@ -88,8 +88,12 @@ export const ImageSelectv2 = (props: Props) => {
         rest.disableClearable ??
         (selectIfOnlyOneOption && filteredOptions?.length === 1)
       }
+      onChange={(e, image) => {
+        if (onChange) {
+          onChange(image);
+        }
+      }}
       errorText={rest.errorText ?? error?.[0].reason}
-      onChange={(e, image) => onChange(image)}
       value={value ?? null}
     />
   );

From 09188d4645358c82ba9b5f19cd1aea7572445229 Mon Sep 17 00:00:00 2001
From: Banks Nussman <banks@nussman.us>
Date: Mon, 15 Apr 2024 11:02:58 -0400
Subject: [PATCH 22/22] add comment

---
 .../Tabs/StackScripts/StackScriptSelectionRow.tsx               | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
index ed33f0361a4..52a1c8c32d2 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptSelectionRow.tsx
@@ -22,6 +22,8 @@ interface Props {
 export const StackScriptSelectionRow = (props: Props) => {
   const { disabled, isSelected, onOpenDetails, onSelect, stackscript } = props;
 
+  // Never show LKE StackScripts. We try to hide these from the user, even though they
+  // are returned by the API.
   if (stackscript.username.startsWith('lke-service-account-')) {
     return null;
   }