From a6014ad7de8f3ae3b1e70fbf2e7e75c022b63eba Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Tue, 26 Sep 2023 21:53:56 -0400 Subject: [PATCH 1/7] Initial commit --- .../LinodesCreate/LoadingAppPanelSection.tsx | 51 +++++++++++++++++++ .../Linodes/LinodesCreate/SelectAppPanel.tsx | 31 ++++++----- .../TabbedContent/FromAppsContent.tsx | 26 ++++++++-- .../src/utilities/percentageCounter.ts | 33 ++++++++++++ 4 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 packages/manager/src/features/Linodes/LinodesCreate/LoadingAppPanelSection.tsx create mode 100644 packages/manager/src/utilities/percentageCounter.ts 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..e7c12d31e8a 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/SelectAppPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/SelectAppPanel.tsx @@ -4,12 +4,12 @@ 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'; @@ -68,12 +68,25 @@ class SelectAppPanel extends React.PureComponent { } if (appInstancesLoading || !appInstances) { + // if (true === true) { return ( - - - - - + + + + + ); } @@ -191,10 +204,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..0a4b60fe19d 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx @@ -10,8 +10,8 @@ import { Box } from 'src/components/Box'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import Select, { Item } from 'src/components/EnhancedSelect'; 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 +22,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 +31,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 +81,7 @@ interface State { filteredApps: CombinedProps['appInstances']; isFiltering: boolean; isSearching: boolean; + percentageCounter: number; query: string; selectedScriptForDrawer: string; } @@ -136,6 +138,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, @@ -179,6 +189,7 @@ export class FromAppsContent extends React.Component { filteredApps, isFiltering, isSearching, + percentageCounter, query, } = this.state; @@ -193,7 +204,9 @@ export class FromAppsContent extends React.Component { { }); }; + percentageCounter = initializePercentageCounter(6000); + state: State = { categoryFilter: null, detailDrawerOpen: false, filteredApps: [], isFiltering: false, isSearching: false, + percentageCounter: 0, query: '', selectedScriptForDrawer: '', }; + + updatePercentage = (newPercentage: number) => { + this.setState({ percentageCounter: newPercentage }); + }; } const StyledSearchFilterBox = styled(Box, { label: 'StyledSearchFilterBox' })( diff --git a/packages/manager/src/utilities/percentageCounter.ts b/packages/manager/src/utilities/percentageCounter.ts new file mode 100644 index 00000000000..fb7a5419c80 --- /dev/null +++ b/packages/manager/src/utilities/percentageCounter.ts @@ -0,0 +1,33 @@ +export const initializePercentageCounter = (duration: number) => { + let startTimestamp: number; + let animationFrameId: number; + let percentage: number = 0; + let onUpdate: (percentage: number) => void; + + const animatePercentage = (timestamp: number) => { + if (!startTimestamp) { + startTimestamp = timestamp; + } + + const progress = timestamp - startTimestamp; + percentage = Math.min(100, (progress / duration) * 100); + onUpdate(percentage); + + if (percentage < 100) { + animationFrameId = requestAnimationFrame(animatePercentage); + } + }; + + const startAnimation = (updateCallback: (percentage: number) => void) => { + onUpdate = updateCallback; + animationFrameId = requestAnimationFrame(animatePercentage); + }; + + const stopAnimation = () => { + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); + } + }; + + return { startAnimation, stopAnimation }; +}; From 410d48fb3cfe91dc0b2c86ec334aa25fb9883870 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 27 Sep 2023 11:53:39 -0400 Subject: [PATCH 2/7] Fix stackscripts prefetching --- .../src/components/PrimaryNav/PrimaryNav.tsx | 19 ++++++------- packages/manager/src/queries/stackscripts.ts | 27 ++++++++++++------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index e1f2ad9ce10..4d11cdb979b 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -84,7 +84,6 @@ export const PrimaryNav = (props: Props) => { const location = useLocation(); const [enableObjectPrefetch, setEnableObjectPrefetch] = React.useState(false); - const [ enableMarketplacePrefetch, setEnableMarketplacePrefetch, @@ -92,11 +91,7 @@ export const PrimaryNav = (props: Props) => { const { _isManagedAccount, account } = useAccountManagement(); - const { - data: oneClickApps, - error: oneClickAppsError, - isLoading: oneClickAppsLoading, - } = useStackScriptsOCA(enableMarketplacePrefetch); + const { prefetchStackScriptsOCA } = useStackScriptsOCA(); const { data: clusters, @@ -118,9 +113,6 @@ export const PrimaryNav = (props: Props) => { !clustersError && !bucketsError; - const allowMarketplacePrefetch = - !oneClickApps && !oneClickAppsLoading && !oneClickAppsError; - const showDatabases = isFeatureEnabled( 'Managed Databases', Boolean(flags.databases), @@ -145,6 +137,12 @@ export const PrimaryNav = (props: Props) => { } }; + React.useEffect(() => { + if (enableMarketplacePrefetch) { + prefetchStackScriptsOCA(); + } + }, [enableMarketplacePrefetch, prefetchStackScriptsOCA]); + const primaryLinkGroups: PrimaryLink[][] = React.useMemo( () => [ [ @@ -248,7 +246,7 @@ export const PrimaryNav = (props: Props) => { display: 'Marketplace', href: '/linodes/create?type=One-Click', icon: , - prefetchRequestCondition: allowMarketplacePrefetch, + prefetchRequestCondition: true, prefetchRequestFn: prefetchMarketplace, }, ], @@ -276,7 +274,6 @@ export const PrimaryNav = (props: Props) => { showDatabases, _isManagedAccount, allowObjPrefetch, - allowMarketplacePrefetch, flags.databaseBeta, flags.aglb, showVPCs, diff --git a/packages/manager/src/queries/stackscripts.ts b/packages/manager/src/queries/stackscripts.ts index d719e80e645..91a10755448 100644 --- a/packages/manager/src/queries/stackscripts.ts +++ b/packages/manager/src/queries/stackscripts.ts @@ -1,6 +1,6 @@ import { StackScript } from '@linode/api-v4/lib/stackscripts'; import { APIError, Params } from '@linode/api-v4/lib/types'; -import { useQuery } from 'react-query'; +import { useQueryClient } from 'react-query'; import { getOneClickApps } from 'src/features/StackScripts/stackScriptUtils'; import { getAll } from 'src/utilities/getAll'; @@ -9,15 +9,22 @@ import { queryPresets } from './base'; export const queryKey = 'stackscripts'; -export const useStackScriptsOCA = (enabled: boolean, params: Params = {}) => { - return useQuery( - [`${queryKey}-oca-all`, params], - () => getAllOCAsRequest(params), - { - enabled, - ...queryPresets.oneTimeFetch, - } - ); +export const useStackScriptsOCA = (params: Params = {}) => { + const queryClient = useQueryClient(); + + const prefetchStackScriptsOCA = async () => { + await queryClient.prefetchQuery( + `${queryKey}-oca-all`, + () => getAllOCAsRequest(params), + { + ...queryPresets.oneTimeFetch, + } + ); + }; + + return { + prefetchStackScriptsOCA, + }; }; export const getAllOCAsRequest = (passedParams: Params = {}) => From 1ce788445b2bef902ed7a28cb9978abeabe2965b Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 27 Sep 2023 12:09:58 -0400 Subject: [PATCH 3/7] Revert "Fix stackscripts prefetching" This reverts commit 410d48fb3cfe91dc0b2c86ec334aa25fb9883870. --- .../src/components/PrimaryNav/PrimaryNav.tsx | 19 +++++++------ packages/manager/src/queries/stackscripts.ts | 27 +++++++------------ 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index 4d11cdb979b..e1f2ad9ce10 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -84,6 +84,7 @@ export const PrimaryNav = (props: Props) => { const location = useLocation(); const [enableObjectPrefetch, setEnableObjectPrefetch] = React.useState(false); + const [ enableMarketplacePrefetch, setEnableMarketplacePrefetch, @@ -91,7 +92,11 @@ export const PrimaryNav = (props: Props) => { const { _isManagedAccount, account } = useAccountManagement(); - const { prefetchStackScriptsOCA } = useStackScriptsOCA(); + const { + data: oneClickApps, + error: oneClickAppsError, + isLoading: oneClickAppsLoading, + } = useStackScriptsOCA(enableMarketplacePrefetch); const { data: clusters, @@ -113,6 +118,9 @@ export const PrimaryNav = (props: Props) => { !clustersError && !bucketsError; + const allowMarketplacePrefetch = + !oneClickApps && !oneClickAppsLoading && !oneClickAppsError; + const showDatabases = isFeatureEnabled( 'Managed Databases', Boolean(flags.databases), @@ -137,12 +145,6 @@ export const PrimaryNav = (props: Props) => { } }; - React.useEffect(() => { - if (enableMarketplacePrefetch) { - prefetchStackScriptsOCA(); - } - }, [enableMarketplacePrefetch, prefetchStackScriptsOCA]); - const primaryLinkGroups: PrimaryLink[][] = React.useMemo( () => [ [ @@ -246,7 +248,7 @@ export const PrimaryNav = (props: Props) => { display: 'Marketplace', href: '/linodes/create?type=One-Click', icon: , - prefetchRequestCondition: true, + prefetchRequestCondition: allowMarketplacePrefetch, prefetchRequestFn: prefetchMarketplace, }, ], @@ -274,6 +276,7 @@ export const PrimaryNav = (props: Props) => { showDatabases, _isManagedAccount, allowObjPrefetch, + allowMarketplacePrefetch, flags.databaseBeta, flags.aglb, showVPCs, diff --git a/packages/manager/src/queries/stackscripts.ts b/packages/manager/src/queries/stackscripts.ts index 91a10755448..d719e80e645 100644 --- a/packages/manager/src/queries/stackscripts.ts +++ b/packages/manager/src/queries/stackscripts.ts @@ -1,6 +1,6 @@ import { StackScript } from '@linode/api-v4/lib/stackscripts'; import { APIError, Params } from '@linode/api-v4/lib/types'; -import { useQueryClient } from 'react-query'; +import { useQuery } from 'react-query'; import { getOneClickApps } from 'src/features/StackScripts/stackScriptUtils'; import { getAll } from 'src/utilities/getAll'; @@ -9,22 +9,15 @@ import { queryPresets } from './base'; export const queryKey = 'stackscripts'; -export const useStackScriptsOCA = (params: Params = {}) => { - const queryClient = useQueryClient(); - - const prefetchStackScriptsOCA = async () => { - await queryClient.prefetchQuery( - `${queryKey}-oca-all`, - () => getAllOCAsRequest(params), - { - ...queryPresets.oneTimeFetch, - } - ); - }; - - return { - prefetchStackScriptsOCA, - }; +export const useStackScriptsOCA = (enabled: boolean, params: Params = {}) => { + return useQuery( + [`${queryKey}-oca-all`, params], + () => getAllOCAsRequest(params), + { + enabled, + ...queryPresets.oneTimeFetch, + } + ); }; export const getAllOCAsRequest = (passedParams: Params = {}) => From 10d90f3f3ec178b54b9f8edea4cb897496340cc4 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 27 Sep 2023 12:13:27 -0400 Subject: [PATCH 4/7] Fix stackscripts prefetching --- packages/manager/src/queries/stackscripts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/queries/stackscripts.ts b/packages/manager/src/queries/stackscripts.ts index d719e80e645..4ad5a08e4c6 100644 --- a/packages/manager/src/queries/stackscripts.ts +++ b/packages/manager/src/queries/stackscripts.ts @@ -11,7 +11,7 @@ export const queryKey = 'stackscripts'; export const useStackScriptsOCA = (enabled: boolean, params: Params = {}) => { return useQuery( - [`${queryKey}-oca-all`, params], + `${queryKey}-oca-all`, () => getAllOCAsRequest(params), { enabled, From 8d39f4da59ff4f763a34a70418800438b0d25a83 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 27 Sep 2023 15:38:34 -0400 Subject: [PATCH 5/7] Add test and clear function --- .../TabbedContent/FromAppsContent.tsx | 81 +++++++++++------- .../src/utilities/percentageCounter.test.ts | 85 +++++++++++++++++++ .../src/utilities/percentageCounter.ts | 25 +++++- 3 files changed, 157 insertions(+), 34 deletions(-) create mode 100644 packages/manager/src/utilities/percentageCounter.test.ts diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx index 0a4b60fe19d..8696fc36918 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx @@ -1,14 +1,15 @@ 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 { Paper } from 'src/components/Paper'; import { Typography } from 'src/components/Typography'; @@ -178,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; @@ -200,9 +208,36 @@ export class FromAppsContent extends React.Component { Select an App - - + + + + + ), + } + : undefined + } placeholder={ appInstancesLoading ? `Loading... ${percentageCounter.toFixed(0)}%` @@ -211,6 +246,9 @@ export class FromAppsContent extends React.Component { sx={ appInstancesLoading ? { + '& .MuiInput-input::placeholder': { + opacity: 1, + }, '& input': { cursor: 'not-allowed', }, @@ -220,6 +258,7 @@ export class FromAppsContent extends React.Component { } : null } + debounceTime={250} disabled={appInstancesLoading} fullWidth hideLabel @@ -228,8 +267,8 @@ export class FromAppsContent extends React.Component { onSearch={this.onSearch} value={query} /> - - + +