diff --git a/packages/manager/.changeset/pr-9724-tech-stories-1695847434977.md b/packages/manager/.changeset/pr-9724-tech-stories-1695847434977.md new file mode 100644 index 00000000000..cc56f43051e --- /dev/null +++ b/packages/manager/.changeset/pr-9724-tech-stories-1695847434977.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Improve loading patterns on Marketplace page ([#9724](https://github.com/linode/manager/pull/9724)) diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LoadingAppPanelSection.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LoadingAppPanelSection.tsx new file mode 100644 index 00000000000..5f6749c4a8e --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesCreate/LoadingAppPanelSection.tsx @@ -0,0 +1,51 @@ +import Grid from '@mui/material/Unstable_Grid2'; +import { styled } from '@mui/material/styles'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import * as React from 'react'; + +import { Divider } from 'src/components/Divider'; +import { Skeleton } from 'src/components/Skeleton'; +import { Typography } from 'src/components/Typography'; + +interface Props { + desktopCount: number; + heading: string; + mobileCount: number; +} + +export const LoadingAppPanelSection = (props: Props) => { + const { desktopCount, heading, mobileCount } = props; + const theme = useTheme(); + const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); + const count = matchesSmDown ? mobileCount : desktopCount; + + return ( + <> + {heading} + + + {Array(count) + .fill(0) + .map((_, idx) => ( + + + + ))} + + + ); +}; + +const AppPanelGrid = styled(Grid)(({ theme }) => ({ + marginBottom: theme.spacing(), + marginTop: theme.spacing(2), + padding: `${theme.spacing(1)} 0`, +})); + +const StyledSkeleton = styled(Skeleton, { + label: 'StyledSkeleton', +})(({ theme }) => ({ + height: 60, + transform: 'none', +})); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/SelectAppPanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/SelectAppPanel.tsx index 9a4b8b5ed4e..bdc0df555da 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectAppPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectAppPanel.tsx @@ -4,15 +4,19 @@ import * as React from 'react'; import { compose } from 'recompose'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; -import { LandingLoading } from 'src/components/LandingLoading/LandingLoading'; import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; import { AppPanelSection } from 'src/features/Linodes/LinodesCreate/AppPanelSection'; import { getQueryParamFromQueryString } from 'src/utilities/queryParams'; +import { LoadingAppPanelSection } from './LoadingAppPanelSection'; import { Panel } from './Panel'; import { AppsData } from './types'; +const NEW_APPS_HEADER = 'New apps'; +const POPULAR_APPS_HEADER = 'Popular apps'; +const ALL_APPS_HEADER = 'All apps'; + interface Props extends AppsData { disabled: boolean; error?: string; @@ -69,11 +73,23 @@ class SelectAppPanel extends React.PureComponent { if (appInstancesLoading || !appInstances) { return ( - - - - - + + + + + ); } @@ -105,7 +121,7 @@ class SelectAppPanel extends React.PureComponent { apps={newApps} disabled={disabled} handleClick={handleClick} - heading="New apps" + heading={NEW_APPS_HEADER} openDrawer={openDrawer} selectedStackScriptID={selectedStackScriptID} /> @@ -115,7 +131,7 @@ class SelectAppPanel extends React.PureComponent { apps={popularApps} disabled={disabled} handleClick={handleClick} - heading="Popular apps" + heading={POPULAR_APPS_HEADER} openDrawer={openDrawer} selectedStackScriptID={selectedStackScriptID} /> @@ -124,7 +140,7 @@ class SelectAppPanel extends React.PureComponent { apps={allApps} disabled={disabled} handleClick={handleClick} - heading={isFilteringOrSearching ? '' : 'All apps'} + heading={isFilteringOrSearching ? '' : ALL_APPS_HEADER} openDrawer={openDrawer} searchValue={searchValue} selectedStackScriptID={selectedStackScriptID} @@ -191,10 +207,4 @@ const StyledPaper = styled(Paper, { label: 'StyledPaper' })(({ theme }) => ({ ...commonStyling(theme), })); -const StyledLoadingSpan = styled('span', { label: 'StyledLoadingSpan' })({ - '& >div:first-of-type': { - height: 450, - }, -}); - export default compose(React.memo)(SelectAppPanel); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx index 4bf6bdb3568..9a72df3f8fe 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx @@ -1,17 +1,18 @@ import { Image } from '@linode/api-v4/lib/images'; import { StackScript, UserDefinedField } from '@linode/api-v4/lib/stackscripts'; -import { styled } from '@mui/material/styles'; +import Close from '@mui/icons-material/Close'; +import Grid from '@mui/material/Unstable_Grid2'; import compact from 'lodash/compact'; import curry from 'lodash/curry'; import { assocPath } from 'ramda'; import * as React from 'react'; -import { Box } from 'src/components/Box'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import Select, { Item } from 'src/components/EnhancedSelect'; +import { IconButton } from 'src/components/IconButton'; import { ImageSelect } from 'src/components/ImageSelect/ImageSelect'; -import { Typography } from 'src/components/Typography'; import { Paper } from 'src/components/Paper'; +import { Typography } from 'src/components/Typography'; import { APP_ROOT } from 'src/constants'; import { ImageEmptyState } from 'src/features/Linodes/LinodesCreate/TabbedContent/ImageEmptyState'; import { AppDetailDrawer } from 'src/features/OneClickApps'; @@ -22,6 +23,7 @@ import { import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel'; import { sendMarketplaceSearchEvent } from 'src/utilities/analytics'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { initializePercentageCounter } from 'src/utilities/percentageCounter'; import SelectAppPanel from '../SelectAppPanel'; import { @@ -30,8 +32,8 @@ import { StackScriptFormStateHandlers, WithTypesRegionsAndImages, } from '../types'; -import { filterUDFErrors } from './formUtilities'; import { StyledGrid } from './CommonTabbedContent.styles'; +import { filterUDFErrors } from './formUtilities'; const appCategories = [ 'Control Panels', @@ -80,6 +82,7 @@ interface State { filteredApps: CombinedProps['appInstances']; isFiltering: boolean; isSearching: boolean; + percentageCounter: number; query: string; selectedScriptForDrawer: string; } @@ -136,6 +139,14 @@ const renderLogo = (selectedStackScriptLabel?: string, logoUrl?: string) => { const curriedHandleSelectStackScript = curry(handleSelectStackScript); export class FromAppsContent extends React.Component { + componentDidMount() { + this.percentageCounter.startAnimation(this.updatePercentage); + } + + componentWillUnmount() { + this.percentageCounter.stopAnimation(); + } + render() { const { appInstances, @@ -168,6 +179,13 @@ export class FromAppsContent extends React.Component { sendMarketplaceSearchEvent('Search Field'); }; + const handleClearSearch = () => { + this.setState({ + isSearching: false, + query: '', + }); + }; + const logoUrl = appInstances?.find( (app) => app.id === selectedStackScriptID )?.logo_url; @@ -179,6 +197,7 @@ export class FromAppsContent extends React.Component { filteredApps, isFiltering, isSearching, + percentageCounter, query, } = this.state; @@ -189,15 +208,48 @@ export class FromAppsContent extends React.Component { Select an App - - + + + + + ), + } + : undefined + } placeholder={ - appInstancesLoading ? 'Loading...' : 'Search for app name' + appInstancesLoading + ? `Loading... ${percentageCounter.toFixed(0)}%` + : 'Search for app name' } sx={ appInstancesLoading ? { + '& .MuiInput-input::placeholder': { + opacity: 1, + }, '& input': { cursor: 'not-allowed', }, @@ -207,6 +259,7 @@ export class FromAppsContent extends React.Component { } : null } + debounceTime={250} disabled={appInstancesLoading} fullWidth hideLabel @@ -215,8 +268,8 @@ export class FromAppsContent extends React.Component { onSearch={this.onSearch} value={query} /> - - + +