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: Statically Cache Marketplace Apps #9732

Merged
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
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-9732-added-1696340382725.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

Statically Cache Marketplace Apps ([#9732](https://github.com/linode/manager/pull/9732))
32 changes: 28 additions & 4 deletions packages/manager/scripts/prebuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,39 @@ const cachedRequests = [
endpoint: 'linode/kernels',
filename: 'kernels.json',
},
{
endpoint: 'linode/stackscripts',
filename: 'marketplace.json',
filter: [
{
'+and': [
{
'+or': [
{ username: 'linode-stackscripts' },
{ username: 'linode' },
],
},
{
label: {
'+contains': 'One-Click',
},
},
],
},
{ '+order_by': 'ordinal' },
],
},
Copy link
Contributor

Choose a reason for hiding this comment

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

so this will work for all stackscripts requests? (stackscripts + OCA)? Or just OCA because of the filter definition?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just Marketplace. This filter is a copy-paste of oneClickFilter (I don't think I can import it from TS into a JS file)

I don't think we want to attempt StackScript caching because at that point we'd have to worry about pagination.

];

async function handleRequest(endpoint, filename) {
const response = await fetch(API_ROOT + endpoint + '?page_size=500');
async function handleRequest(endpoint, filename, filter) {
const response = await fetch(API_ROOT + endpoint + '?page_size=500', {
headers: filter ? { 'x-filter': JSON.stringify(filter) } : {},
});
const data = await response.json();

if (data.data.pages > 1) {
throw new Error(
'Results over 100 will not be retrieved or cached. Aborting.'
`Request ${endpoint} has many pages but we only support caching 1 page.`
);
}

Expand All @@ -51,7 +75,7 @@ async function prebuild() {
console.log('Caching common requests');

const requests = cachedRequests.map((request) =>
handleRequest(request.endpoint, request.filename)
handleRequest(request.endpoint, request.filename, request.filter)
);

try {
Expand Down
2 changes: 1 addition & 1 deletion packages/manager/src/cachedData/kernels.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/manager/src/cachedData/marketplace.json

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions packages/manager/src/containers/withMarketplaceApps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { StackScript } from '@linode/api-v4';
import React from 'react';
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 { getQueryParamFromQueryString } from 'src/utilities/queryParams';

const trimOneClickFromLabel = (script: StackScript) => {
return {
...script,
label: script.label.replace('One-Click', ''),
};
};

export interface WithMarketplaceAppsProps {
appInstances: StackScript[] | undefined;
appInstancesError: string | undefined;
appInstancesLoading: boolean;
}

export const withMarketplaceApps = <Props>(
Component: React.ComponentType<Props & WithMarketplaceAppsProps>
) => (props: Props) => {
const location = useLocation();
const flags = useFlags();

const type = getQueryParamFromQueryString(location.search, 'type');

// Only enable the query when the user is on the Marketplace page
const enabled = type === 'One-Click';

const { data, error, isLoading } = useStackScriptsOCA(enabled);

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

const filteredApps = (data ?? []).filter((script) => {
return (
!script.label.match(/helpers/i) && allowedApps.includes(String(script.id))
);
});
const trimmedApps = filteredApps.map((stackscript) =>
trimOneClickFromLabel(stackscript)
);

return React.createElement(Component, {
...props,
appInstances: trimmedApps,
appInstancesError: error?.[0]?.reason,
appInstancesLoading: isLoading,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Agreements, signAgreement } from '@linode/api-v4/lib/account';
import { Image } from '@linode/api-v4/lib/images';
import { Region } from '@linode/api-v4/lib/regions';
import { convertYupToLinodeErrors } from '@linode/api-v4/lib/request';
import { StackScript, UserDefinedField } from '@linode/api-v4/lib/stackscripts';
import { UserDefinedField } from '@linode/api-v4/lib/stackscripts';
import { APIError } from '@linode/api-v4/lib/types';
import { vpcsValidateIP } from '@linode/validation';
import { CreateLinodeSchema } from '@linode/validation/lib/linodes.schema';
Expand Down Expand Up @@ -45,13 +45,11 @@ import { resetEventsPolling } from 'src/eventsPolling';
import withAgreements, {
AgreementsProps,
} from 'src/features/Account/Agreements/withAgreements';
import { baseApps } from 'src/features/StackScripts/stackScriptUtils';
import {
queryKey as accountAgreementsQueryKey,
reportAgreementSigningError,
} from 'src/queries/accountAgreements';
import { simpleMutationHandlers } from 'src/queries/base';
import { getAllOCAsRequest } from 'src/queries/stackscripts';
import { vpcQueryKey } from 'src/queries/vpcs';
import { CreateTypes } from 'src/store/linodeCreate/linodeCreate.actions';
import { MapState } from 'src/store/types';
Expand All @@ -70,7 +68,7 @@ import { validatePassword } from 'src/utilities/validatePassword';
import LinodeCreate from './LinodeCreate';
import { deriveDefaultLabel } from './deriveDefaultLabel';
import { HandleSubmit, Info, LinodeCreateValidation, TypeInfo } from './types';
import { filterOneClickApps, getRegionIDFromLinodeID } from './utilities';
import { getRegionIDFromLinodeID } from './utilities';

import type {
CreateLinodeRequest,
Expand All @@ -79,13 +77,14 @@ import type {
LinodeTypeClass,
PriceObject,
} from '@linode/api-v4/lib/linodes';
import {
WithMarketplaceAppsProps,
withMarketplaceApps,
} from 'src/containers/withMarketplaceApps';

const DEFAULT_IMAGE = 'linode/debian11';

interface State {
appInstances?: StackScript[];
appInstancesError?: string;
appInstancesLoading: boolean;
assignPublicIPv4Address: boolean;
attachedVLANLabel: null | string;
authorized_users: string[];
Expand Down Expand Up @@ -133,10 +132,10 @@ type CombinedProps = WithSnackbarProps &
WithProfileProps &
AgreementsProps &
WithQueryClientProps &
WithMarketplaceAppsProps &
WithAccountSettingsProps;

const defaultState: State = {
appInstancesLoading: false,
assignPublicIPv4Address: false,
attachedVLANLabel: '',
authorized_users: [],
Expand Down Expand Up @@ -200,33 +199,10 @@ const isNonDefaultImageType = (prevType: string, type: string) => {
class LinodeCreateContainer extends React.PureComponent<CombinedProps, State> {
componentDidMount() {
// Allowed apps include the base set of original apps + anything LD tells us to show
const newApps = this.props.flags.oneClickApps || [];
if (nonImageCreateTypes.includes(this.props.createType)) {
// If we're navigating directly to e.g. the clone page, don't select an image by default
this.setState({ selectedImageID: undefined });
}
this.setState({ appInstancesLoading: true });

this.props.queryClient
.fetchQuery('stackscripts-oca-all', () => getAllOCAsRequest())
.then((res: StackScript[]) => {
const trimmedApps = filterOneClickApps({
baseApps,
newApps,
queryResults: res,
});

this.setState({
appInstances: trimmedApps,
appInstancesLoading: false,
});
})
.catch((_) => {
this.setState({
appInstancesError: 'There was an error loading Marketplace Apps.',
appInstancesLoading: false,
});
});
}

componentDidUpdate(prevProps: CombinedProps) {
Expand Down Expand Up @@ -945,7 +921,8 @@ export default recompose<CombinedProps, {}>(
withProfile,
withAgreements,
withQueryClient,
withAccountSettings
withAccountSettings,
withMarketplaceApps
)(LinodeCreateContainer);

const actionsAndLabels = {
Expand Down
2 changes: 2 additions & 0 deletions packages/manager/src/queries/stackscripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { StackScript } from '@linode/api-v4/lib/stackscripts';
import { APIError, Params } from '@linode/api-v4/lib/types';
import { useQuery } from 'react-query';

import { data as placeholderData } from 'src/cachedData/marketplace.json';
import { getOneClickApps } from 'src/features/StackScripts/stackScriptUtils';
import { getAll } from 'src/utilities/getAll';

Expand All @@ -15,6 +16,7 @@ export const useStackScriptsOCA = (enabled: boolean, params: Params = {}) => {
() => getAllOCAsRequest(params),
{
enabled,
placeholderData,
...queryPresets.oneTimeFetch,
}
);
Expand Down