diff --git a/src/features/linodes/LinodesCreate/CALinodeCreate.tsx b/src/features/linodes/LinodesCreate/CALinodeCreate.tsx
index 0f24f9114fb..e72e2b99146 100644
--- a/src/features/linodes/LinodesCreate/CALinodeCreate.tsx
+++ b/src/features/linodes/LinodesCreate/CALinodeCreate.tsx
@@ -7,11 +7,13 @@ import MUITab from 'src/components/core/Tab';
import Tabs from 'src/components/core/Tabs';
import ErrorState from 'src/components/ErrorState';
import Grid from 'src/components/Grid';
+import { State as userSSHKeyProps } from 'src/features/linodes/userSSHKeyHoc';
import {
getCommunityStackscripts,
getStackScriptsByUser
} from 'src/features/StackScripts/stackScriptUtils';
import SubTabs, { Tab } from './CALinodeCreateSubTabs';
+import FromAppsContent from './TabbedContent/FromAppsContent';
import FromBackupsContent from './TabbedContent/FromBackupsContent';
import FromImageContent from './TabbedContent/FromImageContent';
import FromLinodeContent from './TabbedContent/FromLinodeContent';
@@ -24,6 +26,7 @@ import {
import {
AllFormStateAndHandlers,
+ AppsData,
WithAll,
WithDisplayData,
WithLinodesImagesTypesAndRegions
@@ -37,7 +40,9 @@ type CombinedProps = Props &
WithLinodesImagesTypesAndRegions &
WithDisplayData &
WithAll &
- AllFormStateAndHandlers;
+ AppsData &
+ AllFormStateAndHandlers &
+ userSSHKeyProps;
interface State {
selectedTab: number;
@@ -46,7 +51,7 @@ interface State {
export class LinodeCreate extends React.PureComponent<
CombinedProps & DispatchProps,
State
-> {
+ > {
constructor(props: CombinedProps & DispatchProps) {
super(props);
@@ -107,6 +112,9 @@ export class LinodeCreate extends React.PureComponent<
selectedStackScriptUsername,
selectedStackScriptLabel,
selectedLinodeID,
+ appInstances,
+ appInstancesError,
+ appInstancesLoading,
...rest
} = this.props;
return (
@@ -168,6 +176,9 @@ export class LinodeCreate extends React.PureComponent<
selectedStackScriptUsername,
selectedStackScriptLabel,
selectedLinodeID,
+ appInstances,
+ appInstancesError,
+ appInstancesLoading,
...rest
} = this.props;
@@ -231,6 +242,9 @@ export class LinodeCreate extends React.PureComponent<
linodesError,
regionsLoading,
regionsError,
+ appInstances,
+ appInstancesError,
+ appInstancesLoading,
...rest
} = this.props;
return (
@@ -256,6 +270,9 @@ export class LinodeCreate extends React.PureComponent<
updateLinodeID,
selectedBackupID,
setBackupID,
+ appInstances,
+ appInstancesError,
+ appInstancesLoading,
...rest
} = this.props;
return (
@@ -276,7 +293,14 @@ export class LinodeCreate extends React.PureComponent<
title: 'One-Click Apps',
type: 'fromApp',
render: () => {
- return ;
+ const {
+ setTab,
+ linodesError,
+ linodesLoading,
+ linodesData,
+ ...rest
+ } = this.props;
+ return ;
}
},
{
diff --git a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx
index c156db78c17..c1dadcb9b45 100644
--- a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx
+++ b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx
@@ -53,6 +53,7 @@ import { MapState } from 'src/store/types';
import { allocatePrivateIP } from 'src/utilities/allocateIPAddress';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView';
+import { getCloudApps } from './TabbedContent/formUtilities';
interface State {
selectedImageID?: string;
@@ -74,6 +75,9 @@ interface State {
tags?: Tag[];
errors?: Linode.ApiFieldError[];
formIsSubmitting: boolean;
+ appInstances?: Linode.StackScript.Response[];
+ appInstancesLoading: boolean;
+ appInstancesError?: string;
}
type CombinedProps = InjectedNotistackProps &
@@ -99,7 +103,9 @@ const defaultState: State = {
selectedRegionID: undefined,
selectedTypeID: undefined,
tags: [],
- formIsSubmitting: false
+ formIsSubmitting: false,
+ errors: undefined,
+ appInstancesLoading: false
};
const getRegionIDFromLinodeID = (
@@ -127,6 +133,23 @@ class LinodeCreateContainer extends React.PureComponent {
}
}
+ componentDidMount() {
+ this.setState({ appInstancesLoading: true });
+ getCloudApps()
+ .then(response => {
+ this.setState({
+ appInstancesLoading: false,
+ appInstances: response.data
+ });
+ })
+ .catch(e => {
+ this.setState({
+ appInstancesLoading: false,
+ appInstancesError: 'There was an error loading Cloud Apps.'
+ });
+ });
+ }
+
clearCreationState = () => {
this.props.resetSSHKeys();
this.setState(defaultState);
@@ -180,7 +203,15 @@ class LinodeCreateContainer extends React.PureComponent {
userDefinedFields: Linode.StackScript.UserDefinedField[],
images: Linode.Image[],
defaultData?: any
- ) =>
+ ) => {
+ /**
+ * reset the selected Image but only if we're creating a Linode from
+ * a StackScript and not an app
+ */
+ if (this.props.createType !== 'fromApp') {
+ this.setState({ selectedImageID: undefined });
+ }
+
this.setState({
selectedStackScriptID: id,
selectedStackScriptLabel: label,
@@ -189,8 +220,10 @@ class LinodeCreateContainer extends React.PureComponent {
availableStackScriptImages: images,
udfs: defaultData,
/** reset image because stackscript might not be compatible with selected one */
- selectedImageID: undefined
+ selectedImageID: undefined,
+ errors: undefined
});
+ };
setDiskSize = (size: number) => this.setState({ selectedDiskSize: size });
@@ -208,14 +241,24 @@ class LinodeCreateContainer extends React.PureComponent {
generateLabel = () => {
const { getLabel, imagesData, regionsData } = this.props;
- const { selectedImageID, selectedRegionID } = this.state;
+ const {
+ selectedImageID,
+ selectedRegionID,
+ selectedStackScriptLabel
+ } = this.state;
/* tslint:disable-next-line */
let arg1,
arg2,
arg3 = '';
- if (selectedImageID) {
+ /**
+ * lean in favor of using stackscript label
+ * then next priority is image label
+ */
+ if (selectedStackScriptLabel) {
+ arg1 = selectedStackScriptLabel;
+ } else if (selectedImageID) {
const selectedImage = imagesData.find(img => img.id === selectedImageID);
/**
* Use 'vendor' if it's a public image, otherwise use label (because 'vendor' will be null)
@@ -469,6 +512,9 @@ class LinodeCreateContainer extends React.PureComponent {
resetSSHKeys={this.props.resetSSHKeys}
selectedBackupID={this.state.selectedBackupID}
setBackupID={this.setBackupID}
+ appInstances={this.state.appInstances}
+ appInstancesError={this.state.appInstancesError}
+ appInstancesLoading={this.state.appInstancesLoading}
/>
diff --git a/src/features/linodes/LinodesCreate/Panel.tsx b/src/features/linodes/LinodesCreate/Panel.tsx
index 7c85cece145..d870628e780 100644
--- a/src/features/linodes/LinodesCreate/Panel.tsx
+++ b/src/features/linodes/LinodesCreate/Panel.tsx
@@ -23,15 +23,16 @@ interface Props {
children: React.ReactElement;
error?: string;
title?: string;
+ className?: string;
}
type CombinedProps = Props & WithStyles;
-const Panel: React.StatelessComponent = (props) => {
+const Panel: React.StatelessComponent = props => {
const { classes, children, error, title } = props;
return (
{error && }
@@ -40,9 +41,9 @@ const Panel: React.StatelessComponent = (props) => {
{children}
- )
-}
+ );
+};
const styled = withStyles(styles);
-export default styled(Panel);
\ No newline at end of file
+export default styled(Panel);
diff --git a/src/features/linodes/LinodesCreate/SelectAppPanel.tsx b/src/features/linodes/LinodesCreate/SelectAppPanel.tsx
new file mode 100644
index 00000000000..5d95a19899b
--- /dev/null
+++ b/src/features/linodes/LinodesCreate/SelectAppPanel.tsx
@@ -0,0 +1,168 @@
+import {
+ StyleRulesCallback,
+ withStyles,
+ WithStyles
+} from '@material-ui/core/styles';
+import * as React from 'react';
+import { compose } from 'recompose';
+
+import ErrorState from 'src/components/ErrorState';
+import Grid from 'src/components/Grid';
+import LinearProgress from 'src/components/LinearProgress';
+import SelectionCard from 'src/components/SelectionCard';
+import Panel from './Panel';
+
+import { AppsData } from './types';
+
+type ClassNames = 'flatImagePanelSelections' | 'panel' | 'loading';
+
+const styles: StyleRulesCallback = theme => ({
+ flatImagePanelSelections: {
+ marginTop: theme.spacing.unit * 2,
+ padding: `${theme.spacing.unit}px 0`
+ },
+ panel: {
+ marginBottom: theme.spacing.unit * 3
+ },
+ loading: {
+ marginTop: theme.spacing.unit * 2,
+ marginBottom: theme.spacing.unit * 2
+ }
+});
+
+interface Props extends AppsData {
+ handleClick: (
+ id: number,
+ label: string,
+ username: string,
+ stackScriptImages: string[],
+ userDefinedFields: Linode.StackScript.UserDefinedField[]
+ ) => void;
+ disabled: boolean;
+ selectedStackScriptID?: number;
+ error?: string;
+}
+
+type CombinedProps = Props & WithStyles;
+
+const SelectAppPanel: React.SFC = props => {
+ const {
+ disabled,
+ selectedStackScriptID,
+ classes,
+ error,
+ appInstances,
+ appInstancesError,
+ appInstancesLoading,
+ handleClick
+ } = props;
+
+ if (appInstancesError) {
+ return (
+
+
+
+ );
+ }
+
+ if (appInstancesLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!appInstances) {
+ return null;
+ }
+
+ /** hacky and bad */
+ const interceptedError = error ? 'You must select an App to create from' : '';
+
+ return (
+
+
+ {appInstances.map(eachApp => (
+
+ ))}
+
+
+ );
+};
+
+const styled = withStyles(styles);
+
+export default compose(
+ styled,
+ React.memo
+)(SelectAppPanel);
+
+interface SelectionProps {
+ handleClick: (
+ id: number,
+ label: string,
+ username: string,
+ stackScriptImages: string[],
+ userDefinedFields: Linode.StackScript.UserDefinedField[]
+ ) => void;
+ id: number;
+ label: string;
+ username: string;
+ userDefinedFields: Linode.StackScript.UserDefinedField[];
+ availableImages: string[];
+ disabled: boolean;
+ checked: boolean;
+}
+
+class SelectionCardWrapper extends React.PureComponent {
+ handleSelectApp = (event: React.SyntheticEvent) => {
+ const {
+ id,
+ label,
+ username,
+ userDefinedFields,
+ availableImages
+ } = this.props;
+
+ return this.props.handleClick(
+ id,
+ label,
+ username,
+ availableImages,
+ userDefinedFields
+ );
+ };
+
+ render() {
+ const { id, checked, label, disabled } = this.props;
+ return (
+ {
+ return ;
+ }}
+ heading={label}
+ subheadings={['']}
+ data-qa-selection-card
+ disabled={disabled}
+ />
+ );
+ }
+}
diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx
new file mode 100644
index 00000000000..26e44eaf189
--- /dev/null
+++ b/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx
@@ -0,0 +1,374 @@
+import { assocPath, pathOr } from 'ramda';
+import * as React from 'react';
+import { Sticky, StickyProps } from 'react-sticky';
+import { compose } from 'recompose';
+
+import AccessPanel from 'src/components/AccessPanel';
+import CheckoutBar from 'src/components/CheckoutBar';
+import Paper from 'src/components/core/Paper';
+import {
+ StyleRulesCallback,
+ withStyles,
+ WithStyles
+} from 'src/components/core/styles';
+import Typography from 'src/components/core/Typography';
+import CreateLinodeDisabled from 'src/components/CreateLinodeDisabled';
+import Grid from 'src/components/Grid';
+import LabelAndTagsPanel from 'src/components/LabelAndTagsPanel';
+import Notice from 'src/components/Notice';
+import SelectRegionPanel from 'src/components/SelectRegionPanel';
+import { Tag } from 'src/components/TagsInput';
+import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel';
+import AddonsPanel from '../AddonsPanel';
+import SelectAppPanel from '../SelectAppPanel';
+import SelectImagePanel from '../SelectImagePanel';
+import SelectPlanPanel from '../SelectPlanPanel';
+
+import getAPIErrorsFor from 'src/utilities/getAPIErrorFor';
+import { filterUDFErrors } from './formUtilities';
+import { renderBackupsDisplaySection } from './utils';
+
+import {
+ AppsData,
+ StackScriptFormStateHandlers,
+ WithAll,
+ WithDisplayData
+} from '../types';
+
+type ClassNames = 'sidebar' | 'emptyImagePanel' | 'emptyImagePanelText';
+
+const styles: StyleRulesCallback = theme => ({
+ sidebar: {
+ [theme.breakpoints.up('lg')]: {
+ marginTop: -130
+ }
+ },
+ emptyImagePanel: {
+ padding: theme.spacing.unit * 3
+ },
+ emptyImagePanelText: {
+ marginTop: theme.spacing.unit,
+ padding: `${theme.spacing.unit}px 0`
+ }
+});
+
+const errorResources = {
+ type: 'A plan selection',
+ region: 'A region selection',
+ label: 'A label',
+ root_pass: 'A root password',
+ image: 'Image',
+ tags: 'Tags',
+ stackscript_id: 'A StackScript'
+};
+
+type InnerProps = WithDisplayData & WithAll & AppsData;
+
+type CombinedProps = InnerProps &
+ WithStyles &
+ StackScriptFormStateHandlers;
+
+class FromAppsContent extends React.PureComponent {
+ handleSelectStackScript = (
+ id: number,
+ label: string,
+ username: string,
+ stackScriptImages: string[],
+ userDefinedFields: Linode.StackScript.UserDefinedField[]
+ ) => {
+ /**
+ * based on the list of images we get back from the API, compare those
+ * to our list of master images supported by Linode and filter out the ones
+ * that aren't compatible with our selected StackScript
+ */
+ const compatibleImages = this.props.imagesData.filter(eachImage => {
+ return stackScriptImages.some(
+ eachSSImage => eachSSImage === eachImage.id
+ );
+ });
+
+ /**
+ * if a UDF field comes back from the API with a "default"
+ * value, it means we need to pre-populate the field and form state
+ */
+ const defaultUDFData = userDefinedFields.reduce((accum, eachField) => {
+ if (eachField.default) {
+ accum[eachField.name] = eachField.default;
+ }
+ return accum;
+ }, {});
+
+ this.props.updateStackScript(
+ id,
+ label,
+ username,
+ userDefinedFields,
+ compatibleImages,
+ defaultUDFData
+ );
+ };
+
+ handleChangeUDF = (key: string, value: string) => {
+ // either overwrite or create new selection
+ const newUDFData = assocPath([key], value, this.props.selectedUDFs);
+
+ this.props.handleSelectUDFs({ ...this.props.selectedUDFs, ...newUDFData });
+ };
+
+ handleCreateLinode = () => {
+ const {
+ backupsEnabled,
+ password,
+ userSSHKeys,
+ handleSubmitForm,
+ selectedImageID,
+ selectedRegionID,
+ selectedStackScriptID,
+ selectedTypeID,
+ selectedUDFs,
+ privateIPEnabled,
+ tags
+ } = this.props;
+
+ handleSubmitForm('createFromStackScript', {
+ region: selectedRegionID,
+ type: selectedTypeID,
+ stackscript_id: selectedStackScriptID,
+ stackscript_data: selectedUDFs,
+ label: this.props.label /* optional */,
+ root_pass: password /* required if image ID is provided */,
+ image: selectedImageID /* optional */,
+ backups_enabled: backupsEnabled /* optional */,
+ booted: true,
+ private_ip: privateIPEnabled,
+ authorized_users: userSSHKeys
+ .filter(u => u.selected)
+ .map(u => u.username),
+ tags: tags ? tags.map((item: Tag) => item.value) : []
+ });
+ };
+
+ render() {
+ const {
+ accountBackupsEnabled,
+ classes,
+ typesData,
+ regionsData,
+ imageDisplayInfo,
+ regionDisplayInfo,
+ typeDisplayInfo,
+ backupsMonthlyPrice,
+ userSSHKeys,
+ userCannotCreateLinode,
+ selectedImageID,
+ selectedRegionID,
+ selectedStackScriptID,
+ selectedStackScriptLabel,
+ selectedTypeID,
+ selectedUDFs: udf_data,
+ label,
+ tags,
+ availableUserDefinedFields: userDefinedFields,
+ availableStackScriptImages: compatibleImages,
+ updateImageID,
+ updateLabel,
+ updatePassword,
+ updateRegionID,
+ updateTags,
+ updateTypeID,
+ formIsSubmitting,
+ password,
+ backupsEnabled,
+ toggleBackupsEnabled,
+ privateIPEnabled,
+ togglePrivateIPEnabled,
+ errors,
+ appInstances,
+ appInstancesError,
+ appInstancesLoading
+ } = this.props;
+
+ const hasBackups = backupsEnabled || accountBackupsEnabled;
+ const hasErrorFor = getAPIErrorsFor(errorResources, errors);
+ const generalError = hasErrorFor('none');
+
+ return (
+
+
+
+ {generalError && }
+
+ {!userCannotCreateLinode &&
+ userDefinedFields &&
+ userDefinedFields.length > 0 && (
+
+ )}
+ {!userCannotCreateLinode &&
+ compatibleImages &&
+ compatibleImages.length > 0 ? (
+
+ ) : (
+
+ {/* empty state for images */}
+ {hasErrorFor('image') && (
+
+ )}
+
+ Select Image
+
+
+ No Compatible Images Available
+
+
+ )}
+
+
+
+ 0 && selectedImageID ? userSSHKeys : []}
+ />
+
+
+
+
+ {(stickyProps: StickyProps) => {
+ const displaySections = [];
+ if (imageDisplayInfo) {
+ displaySections.push(imageDisplayInfo);
+ }
+
+ if (regionDisplayInfo) {
+ displaySections.push({
+ title: regionDisplayInfo.title,
+ details: regionDisplayInfo.details
+ });
+ }
+
+ if (typeDisplayInfo) {
+ displaySections.push(typeDisplayInfo);
+ }
+
+ if (
+ hasBackups &&
+ typeDisplayInfo &&
+ typeDisplayInfo.backupsMonthly
+ ) {
+ displaySections.push(
+ renderBackupsDisplaySection(
+ accountBackupsEnabled,
+ typeDisplayInfo.backupsMonthly
+ )
+ );
+ }
+
+ let calculatedPrice = pathOr(0, ['monthly'], typeDisplayInfo);
+ if (
+ hasBackups &&
+ typeDisplayInfo &&
+ typeDisplayInfo.backupsMonthly
+ ) {
+ calculatedPrice += typeDisplayInfo.backupsMonthly;
+ }
+
+ return (
+
+ );
+ }}
+
+
+
+ );
+ }
+}
+
+const styled = withStyles(styles);
+
+export default compose(
+ styled,
+ React.memo
+)(FromAppsContent);
diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromImageContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromImageContent.tsx
index cb328d13e53..0de5ca7c552 100644
--- a/src/features/linodes/LinodesCreate/TabbedContent/FromImageContent.tsx
+++ b/src/features/linodes/LinodesCreate/TabbedContent/FromImageContent.tsx
@@ -47,10 +47,6 @@ interface Props extends BaseFormStateAndHandlers {
imagePanelTitle?: string;
}
-/**
- * image, region, type, label, backups, privateIP, tags, error, isLoading,
- */
-
const errorResources = {
type: 'A plan selection',
region: 'A region selection',
diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx
index 40f5f7051ea..f67976c6893 100644
--- a/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx
+++ b/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx
@@ -24,6 +24,8 @@ import getAPIErrorsFor from 'src/utilities/getAPIErrorFor';
import AddonsPanel from '../AddonsPanel';
import SelectImagePanel from '../SelectImagePanel';
import SelectPlanPanel from '../SelectPlanPanel';
+
+import { filterPublicImages, filterUDFErrors } from './formUtilities';
import { renderBackupsDisplaySection } from './utils';
import {
@@ -33,14 +35,12 @@ import {
} from '../types';
type ClassNames =
- | 'root'
| 'main'
| 'sidebar'
| 'emptyImagePanel'
| 'emptyImagePanelText';
const styles: StyleRulesCallback = theme => ({
- root: {},
main: {
'&.mlMain': {
[theme.breakpoints.up('lg')]: {
@@ -63,13 +63,7 @@ const styles: StyleRulesCallback = theme => ({
}
});
-interface Notice {
- text: string;
- level: 'warning' | 'error'; // most likely only going to need these two
-}
interface Props {
- notice?: Notice;
- selectedTabFromQuery?: string;
request: (
username: string,
params?: any,
@@ -84,7 +78,8 @@ const errorResources = {
label: 'A label',
root_pass: 'A root password',
image: 'image',
- tags: 'Tags'
+ tags: 'Tags',
+ stackscript_id: 'A StackScript'
};
type InnerProps = Props & WithAll;
@@ -178,7 +173,6 @@ export class FromStackScriptContent extends React.PureComponent {
const {
accountBackupsEnabled,
errors,
- notice,
backupsMonthlyPrice,
regionsData,
typesData,
@@ -221,15 +215,8 @@ export class FromStackScriptContent extends React.PureComponent {
return (
-
+
- {!disabled && notice && (
-
- )}
{generalError && }
{
/>
{!disabled && userDefinedFields && userDefinedFields.length > 0 && (
{
}
}
-/**
- * @returns { Linode.Image[] } - a list of public images AKA
- * images that are officially supported by Linode
- *
- * @todo test this
- */
-export const filterPublicImages = (images: Linode.Image[]) => {
- return images.filter((image: Linode.Image) => image.is_public);
-};
-
-/**
- * filter out all the UDF errors from our error state.
- * To do this, we compare the keys from the error state to our "errorResources"
- * map and return all the errors that don't match the keys in that object
- *
- * @todo test this function
- */
-export const filterUDFErrors = (errors?: Linode.ApiFieldError[]) => {
- return !errors
- ? []
- : errors.filter(eachError => {
- return !Object.keys(errorResources).some(
- eachKey => eachKey === eachError.field
- );
- });
-};
-
const styled = withStyles(styles);
const enhanced = compose(styled);
diff --git a/src/features/linodes/LinodesCreate/TabbedContent/formUtilities.ts b/src/features/linodes/LinodesCreate/TabbedContent/formUtilities.ts
new file mode 100644
index 00000000000..a9488bcd589
--- /dev/null
+++ b/src/features/linodes/LinodesCreate/TabbedContent/formUtilities.ts
@@ -0,0 +1,44 @@
+import { getStackscripts } from 'src/services/stackscripts';
+
+/**
+ * @returns { Linode.Image[] } - a list of public images AKA
+ * images that are officially supported by Linode
+ *
+ * @todo test this
+ */
+export const filterPublicImages = (images: Linode.Image[]) => {
+ return images.filter((image: Linode.Image) => image.is_public);
+};
+
+/**
+ * filter out all the UDF errors from our error state.
+ * To do this, we compare the keys from the error state to our "errorResources"
+ * map and return all the errors that don't match the keys in that object
+ *
+ * @todo test this function
+ */
+export const filterUDFErrors = (
+ errorResources: any,
+ errors?: Linode.ApiFieldError[]
+) => {
+ return !errors
+ ? []
+ : errors.filter(eachError => {
+ return !Object.keys(errorResources).some(
+ eachKey => eachKey === eachError.field
+ );
+ });
+};
+
+/**
+ * helper function to get Cloud Apps StackScripts
+ *
+ * for the prototype, all the apps we need are going to be uploaded to
+ * Christine Puk's account. Keep in mind that the Linux distros will be missing from this
+ * list because we're intentionally not including the distros in this view
+ */
+export const getCloudApps = (params?: any, filter?: any) =>
+ getStackscripts(params, {
+ ...filter,
+ username: 'capuk'
+ });
diff --git a/src/features/linodes/LinodesCreate/types.ts b/src/features/linodes/LinodesCreate/types.ts
index 20d3db71d05..b157eb01c39 100644
--- a/src/features/linodes/LinodesCreate/types.ts
+++ b/src/features/linodes/LinodesCreate/types.ts
@@ -127,6 +127,11 @@ export interface BackupFormStateHandlers extends CloneFormStateHandlers {
selectedBackupID?: number;
setBackupID: (id: number) => void;
}
+export interface AppsData {
+ appInstances?: Linode.StackScript.Response[];
+ appInstancesLoading: boolean;
+ appInstancesError?: string;
+}
export type AllFormStateAndHandlers = BaseFormStateAndHandlers &
CloneFormStateHandlers &