Skip to content

Commit

Permalink
upcoming: [M3-7874] - Linode Create Refactor - Marketplace App Sectio…
Browse files Browse the repository at this point in the history
…ns (#10520)

* add app sections

* clean up and use `stort` insted of `toStorted`

* clean up and unit test

* break up things into more components

* Added changeset: Linode Create Refactor - Marketplace App Sections

* feedback @hana-linode

* make `oneClickAppsv2` the source of truth

* use client side icon url

* fix scrolling errors into view

* update which apps are new

---------

Co-authored-by: Banks Nussman <banks@nussman.us>
  • Loading branch information
bnussman-akamai and bnussman authored Jun 3, 2024
1 parent 8545f68 commit 894afd5
Show file tree
Hide file tree
Showing 21 changed files with 428 additions and 2,722 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Linode Create Refactor - Marketplace App Sections ([#10520](https://github.com/linode/manager/pull/10520))
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ import {
} from 'support/intercepts/feature-flags';
import { makeFeatureFlagData } from 'support/util/feature-flags';
import { mapStackScriptLabelToOCA } from 'src/features/OneClickApps/utils';
import { baseApps } from 'src/features/StackScripts/stackScriptUtils';
import { stackScriptFactory } from 'src/factories/stackscripts';
import { oneClickApps } from 'src/features/OneClickApps/oneClickApps';
import { oneClickApps } from 'src/features/OneClickApps/oneClickAppsv2';

import type { StackScript } from '@linode/api-v4';
import type { OCA } from '@src/features/OneClickApps/types';
Expand All @@ -42,7 +41,7 @@ describe('OneClick Apps (OCA)', () => {
const stackScripts: StackScript[] = xhr.response?.body.data ?? [];

const trimmedApps: StackScript[] = filterOneClickApps({
baseApps,
baseAppIds: Object.keys(oneClickApps).map(Number),
newApps: {},
queryResults: stackScripts,
});
Expand All @@ -65,7 +64,7 @@ describe('OneClick Apps (OCA)', () => {
// This is only true for the apps defined in `oneClickApps.ts`
expect(
mapStackScriptLabelToOCA({
oneClickApps,
oneClickApps: Object.values(oneClickApps),
stackScriptLabel: decodedLabel,
})
).to.not.be.undefined;
Expand All @@ -82,7 +81,7 @@ describe('OneClick Apps (OCA)', () => {
stackScriptCandidate.should('exist').click();

const app: OCA | undefined = mapStackScriptLabelToOCA({
oneClickApps,
oneClickApps: Object.values(oneClickApps),
stackScriptLabel: candidateApp.label,
});

Expand Down
1 change: 0 additions & 1 deletion packages/manager/src/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,6 @@ export const MainContent = () => {
// the followed comment is for later use, the showCloudPulse will be removed and isACLPEnabled will be used
// const { isACLPEnabled } = useIsACLPEnabled();


/**
* this is the case where the user has successfully completed signup
* but needs a manual review from Customer Support. In this case,
Expand Down
2 changes: 1 addition & 1 deletion packages/manager/src/components/PrimaryNav/PrimaryNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => {

const allowMarketplacePrefetch =
!oneClickApps && !oneClickAppsLoading && !oneClickAppsError;

const showCloudPulse = Boolean(flags.aclp?.enabled);
// the followed comment is for later use, the showCloudPulse will be removed and isACLPEnabled will be used
// const { isACLPEnabled } = useIsACLPEnabled();
Expand Down
4 changes: 2 additions & 2 deletions packages/manager/src/containers/withMarketplaceApps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { StackScript } from '@linode/api-v4';
import React from 'react';
import { useLocation } from 'react-router-dom';

import { baseApps } from 'src/features/StackScripts/stackScriptUtils';
import { oneClickApps } from 'src/features/OneClickApps/oneClickAppsv2';
import { useFlags } from 'src/hooks/useFlags';
import { useMarketplaceAppsQuery } from 'src/queries/stackscripts';
import { getQueryParamFromQueryString } from 'src/utilities/queryParams';
Expand Down Expand Up @@ -34,7 +34,7 @@ export const withMarketplaceApps = <Props>(
const { data, error, isLoading } = useMarketplaceAppsQuery(enabled);

const newApps = flags.oneClickApps || [];
const allowedApps = Object.keys({ ...baseApps, ...newApps });
const allowedApps = Object.keys({ ...oneClickApps, ...newApps });

const filteredApps = (data ?? []).filter((script) => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import userEvent from '@testing-library/user-event';
import React from 'react';

import { stackScriptFactory } from 'src/factories';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { AppSection } from './AppSection';

describe('AppSection', () => {
it('should render a title', () => {
const { getByText } = renderWithTheme(
<AppSection
onOpenDetailsDrawer={vi.fn()}
onSelect={vi.fn()}
selectedStackscriptId={undefined}
stackscripts={[]}
title="Test title"
/>
);

expect(getByText('Test title')).toBeVisible();
});

it('should render apps', () => {
const app = stackScriptFactory.build({
id: 0,
label: 'Linode Marketplace App',
});

const { getByText } = renderWithTheme(
<AppSection
onOpenDetailsDrawer={vi.fn()}
onSelect={vi.fn()}
selectedStackscriptId={undefined}
stackscripts={[app]}
title="Test title"
/>
);

expect(getByText('Linode Marketplace App')).toBeVisible();
});

it('should call `onOpenDetailsDrawer` when the details button is clicked for an app', async () => {
const app = stackScriptFactory.build({ id: 0 });
const onOpenDetailsDrawer = vi.fn();

const { getByLabelText } = renderWithTheme(
<AppSection
onOpenDetailsDrawer={onOpenDetailsDrawer}
onSelect={vi.fn()}
selectedStackscriptId={undefined}
stackscripts={[app]}
title="Test title"
/>
);

await userEvent.click(getByLabelText(`Info for "${app.label}"`));

expect(onOpenDetailsDrawer).toHaveBeenCalledWith(app.id);
});

it('should call `onSelect` when an app is clicked', async () => {
const app = stackScriptFactory.build({ id: 0 });
const onSelect = vi.fn();

const { getByText } = renderWithTheme(
<AppSection
onOpenDetailsDrawer={vi.fn()}
onSelect={onSelect}
selectedStackscriptId={undefined}
stackscripts={[app]}
title="Test title"
/>
);

await userEvent.click(getByText(app.label));

expect(onSelect).toHaveBeenCalledWith(app);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Grid from '@mui/material/Unstable_Grid2';
import React from 'react';

import { Divider } from 'src/components/Divider';
import { Stack } from 'src/components/Stack';
import { Typography } from 'src/components/Typography';
import { oneClickApps } from 'src/features/OneClickApps/oneClickAppsv2';

import { AppSelectionCard } from './AppSelectionCard';

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

interface Props {
onOpenDetailsDrawer: (stackscriptId: number) => void;
onSelect: (stackscript: StackScript) => void;
selectedStackscriptId: null | number | undefined;
stackscripts: StackScript[];
title: string;
}

export const AppSection = (props: Props) => {
const {
onOpenDetailsDrawer,
onSelect,
selectedStackscriptId,
stackscripts,
title,
} = props;

return (
<Stack>
<Typography variant="h2">{title}</Typography>
<Divider spacingBottom={16} spacingTop={16} />
<Grid container spacing={2}>
{stackscripts?.map((stackscript) => (
<AppSelectionCard
checked={stackscript.id === selectedStackscriptId}
iconUrl={`/assets/${oneClickApps[stackscript.id].logo_url}`}
key={stackscript.id}
label={stackscript.label}
onOpenDetailsDrawer={() => onOpenDetailsDrawer(stackscript.id)}
onSelect={() => onSelect(stackscript)}
/>
))}
</Grid>
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
import Grid from '@mui/material/Unstable_Grid2';
import React from 'react';
import { useController, useFormContext } from 'react-hook-form';

import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
import { Box } from 'src/components/Box';
import { CircularProgress } from 'src/components/CircularProgress';
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { Paper } from 'src/components/Paper';
import { Stack } from 'src/components/Stack';
import { Typography } from 'src/components/Typography';
import { oneClickApps } from 'src/features/OneClickApps/oneClickAppsv2';
import { useMarketplaceAppsQuery } from 'src/queries/stackscripts';

import { getDefaultUDFData } from '../StackScripts/UserDefinedFields/utilities';
import { AppSelectionCard } from './AppSelectionCard';
import { AppsList } from './AppsList';
import { categoryOptions } from './utilities';

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

interface Props {
/**
* Opens the Marketplace App details drawer for the given app
Expand All @@ -28,60 +20,8 @@ interface Props {

export const AppSelect = (props: Props) => {
const { onOpenDetailsDrawer } = props;
const { setValue } = useFormContext<LinodeCreateFormValues>();
const { field } = useController<LinodeCreateFormValues, 'stackscript_id'>({
name: 'stackscript_id',
});

const { data: stackscripts, error, isLoading } = useMarketplaceAppsQuery(
true
);

const renderContent = () => {
if (isLoading) {
return (
<Box
alignItems="center"
display="flex"
height="100%"
justifyContent="center"
width="100%"
>
<CircularProgress />
</Box>
);
}

if (error) {
return <ErrorState errorText={error?.[0].reason} />;
}

return (
<Grid container spacing={2}>
{stackscripts?.map((stackscript) => {
if (!oneClickApps[stackscript.id]) {
return null;
}
return (
<AppSelectionCard
onSelect={() => {
setValue(
'stackscript_data',
getDefaultUDFData(stackscript.user_defined_fields)
);
field.onChange(stackscript.id);
}}
checked={field.value === stackscript.id}
iconUrl={`/assets/${oneClickApps[stackscript.id].logo_url}`}
key={stackscript.id}
label={stackscript.label}
onOpenDetailsDrawer={() => onOpenDetailsDrawer(stackscript.id)}
/>
);
})}
</Grid>
);
};
const { isLoading } = useMarketplaceAppsQuery(true);

return (
<Paper>
Expand Down Expand Up @@ -111,7 +51,7 @@ export const AppSelect = (props: Props) => {
/>
</Stack>
<Box height="500px" sx={{ overflowX: 'hidden', overflowY: 'auto' }}>
{renderContent()}
<AppsList onOpenDetailsDrawer={onOpenDetailsDrawer} />
</Box>
</Stack>
</Paper>
Expand Down
Loading

0 comments on commit 894afd5

Please sign in to comment.