Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat: [M3-7195] - Marketplace OCA loading pattern POC #9724

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tech Stories
---

Improve loading patterns on Marketplace page ([#9724](https://github.com/linode/manager/pull/9724))
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Typography variant="h2">{heading}</Typography>
<Divider spacingBottom={16} spacingTop={16} />
<AppPanelGrid container spacing={2}>
{Array(count)
.fill(0)
.map((_, idx) => (
<Grid key={idx} md={4} sm={6} xs={12}>
<StyledSkeleton />
</Grid>
))}
</AppPanelGrid>
</>
);
};

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',
}));
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,11 +73,23 @@ class SelectAppPanel extends React.PureComponent<Props> {

if (appInstancesLoading || !appInstances) {
return (
<StyledPanel error={error} title="Select App">
<StyledLoadingSpan>
<LandingLoading />
</StyledLoadingSpan>
</StyledPanel>
<StyledPaper error={error} title="Select App">
<LoadingAppPanelSection
desktopCount={3}
heading={NEW_APPS_HEADER}
mobileCount={2}
/>
<LoadingAppPanelSection
desktopCount={6}
heading={POPULAR_APPS_HEADER}
mobileCount={4}
/>
<LoadingAppPanelSection
desktopCount={9}
heading={ALL_APPS_HEADER}
mobileCount={6}
/>
</StyledPaper>
);
}

Expand Down Expand Up @@ -105,7 +121,7 @@ class SelectAppPanel extends React.PureComponent<Props> {
apps={newApps}
disabled={disabled}
handleClick={handleClick}
heading="New apps"
heading={NEW_APPS_HEADER}
openDrawer={openDrawer}
selectedStackScriptID={selectedStackScriptID}
/>
Expand All @@ -115,7 +131,7 @@ class SelectAppPanel extends React.PureComponent<Props> {
apps={popularApps}
disabled={disabled}
handleClick={handleClick}
heading="Popular apps"
heading={POPULAR_APPS_HEADER}
openDrawer={openDrawer}
selectedStackScriptID={selectedStackScriptID}
/>
Expand All @@ -124,7 +140,7 @@ class SelectAppPanel extends React.PureComponent<Props> {
apps={allApps}
disabled={disabled}
handleClick={handleClick}
heading={isFilteringOrSearching ? '' : 'All apps'}
heading={isFilteringOrSearching ? '' : ALL_APPS_HEADER}
openDrawer={openDrawer}
searchValue={searchValue}
selectedStackScriptID={selectedStackScriptID}
Expand Down Expand Up @@ -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<Props, Props>(React.memo)(SelectAppPanel);
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand All @@ -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',
Expand Down Expand Up @@ -80,6 +82,7 @@ interface State {
filteredApps: CombinedProps['appInstances'];
isFiltering: boolean;
isSearching: boolean;
percentageCounter: number;
query: string;
selectedScriptForDrawer: string;
}
Expand Down Expand Up @@ -136,6 +139,14 @@ const renderLogo = (selectedStackScriptLabel?: string, logoUrl?: string) => {
const curriedHandleSelectStackScript = curry(handleSelectStackScript);

export class FromAppsContent extends React.Component<CombinedProps, State> {
componentDidMount() {
this.percentageCounter.startAnimation(this.updatePercentage);
}

componentWillUnmount() {
this.percentageCounter.stopAnimation();
}

render() {
const {
appInstances,
Expand Down Expand Up @@ -168,6 +179,13 @@ export class FromAppsContent extends React.Component<CombinedProps, State> {
sendMarketplaceSearchEvent('Search Field');
};

const handleClearSearch = () => {
this.setState({
isSearching: false,
query: '',
});
};

const logoUrl = appInstances?.find(
(app) => app.id === selectedStackScriptID
)?.logo_url;
Expand All @@ -179,6 +197,7 @@ export class FromAppsContent extends React.Component<CombinedProps, State> {
filteredApps,
isFiltering,
isSearching,
percentageCounter,
query,
} = this.state;

Expand All @@ -189,15 +208,48 @@ export class FromAppsContent extends React.Component<CombinedProps, State> {
<StyledGrid>
<Paper>
<Typography variant="h2">Select an App</Typography>
<StyledSearchFilterBox>
<StyledSearchBox>
<Grid
sx={{
marginTop: 0.5,
}}
container
spacing={2}
>
<Grid
sx={{
'& .MuiInputBase-root': {
maxWidth: '100%',
},
flexGrow: 1,
}}
>
<DebouncedSearchTextField
InputProps={
isSearching
? {
endAdornment: (
<IconButton
aria-label="Clear search"
onClick={handleClearSearch}
sx={{ padding: '2px 4px' }}
>
<Close />
</IconButton>
),
}
: 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',
},
Expand All @@ -207,6 +259,7 @@ export class FromAppsContent extends React.Component<CombinedProps, State> {
}
: null
}
debounceTime={250}
disabled={appInstancesLoading}
fullWidth
hideLabel
Expand All @@ -215,8 +268,8 @@ export class FromAppsContent extends React.Component<CombinedProps, State> {
onSearch={this.onSearch}
value={query}
/>
</StyledSearchBox>
<StyledFilterBox>
</Grid>
<Grid sx={{ minWidth: 225 }}>
<Select
placeholder={
appInstancesLoading ? 'Loading...' : 'Select category'
Expand All @@ -228,8 +281,8 @@ export class FromAppsContent extends React.Component<CombinedProps, State> {
options={appCategoryOptions}
value={categoryFilter}
/>
</StyledFilterBox>
</StyledSearchFilterBox>
</Grid>
</Grid>
</Paper>
<SelectAppPanel
appInstances={
Expand Down Expand Up @@ -379,36 +432,21 @@ export class FromAppsContent extends React.Component<CombinedProps, State> {
});
};

// Based on average API response time
percentageCounter = initializePercentageCounter(8000);

state: State = {
categoryFilter: null,
detailDrawerOpen: false,
filteredApps: [],
isFiltering: false,
isSearching: false,
percentageCounter: 0,
query: '',
selectedScriptForDrawer: '',
};
}

const StyledSearchFilterBox = styled(Box, { label: 'StyledSearchFilterBox' })(
({ theme }) => ({
'& > h2': {
width: '100%',
},
display: 'flex',
gap: theme.spacing(),
justifyContent: 'space-between',
marginTop: theme.spacing(),
})
);

const StyledFilterBox = styled(Box, { label: 'StyledFilterBox' })({
flexGrow: 1.5,
});

const StyledSearchBox = styled(Box, { label: 'StyledSearchBox' })({
'& .input': {
maxWidth: 'none',
},
flexGrow: 10,
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored the markup a little bit and no need for styled components for a couple css rules.

updatePercentage = (newPercentage: number) => {
this.setState({ percentageCounter: newPercentage });
};
}
2 changes: 1 addition & 1 deletion packages/manager/src/queries/stackscripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const queryKey = 'stackscripts';

export const useStackScriptsOCA = (enabled: boolean, params: Params = {}) => {
return useQuery<StackScript[], APIError[]>(
[`${queryKey}-oca-all`, params],
`${queryKey}-oca-all`,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the fix for the prefetching request being invalidated. on page visit. CC @bnussman

() => getAllOCAsRequest(params),
{
enabled,
Expand Down
Loading