From 2016d5b12ea82dbe54ffcd208d40259a62b89134 Mon Sep 17 00:00:00 2001 From: mmckenna Date: Tue, 5 Mar 2019 12:04:57 -0500 Subject: [PATCH 1/9] init commit --- .../linodes/LinodesCreate/CALinodeCreate.tsx | 10 +- .../TabbedContent/FromAppsContent.tsx | 308 ++++++++++++++++++ .../TabbedContent/FromImageContent.tsx | 4 - 3 files changed, 317 insertions(+), 5 deletions(-) create mode 100644 src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx diff --git a/src/features/linodes/LinodesCreate/CALinodeCreate.tsx b/src/features/linodes/LinodesCreate/CALinodeCreate.tsx index 0f24f9114fb..403228442c6 100644 --- a/src/features/linodes/LinodesCreate/CALinodeCreate.tsx +++ b/src/features/linodes/LinodesCreate/CALinodeCreate.tsx @@ -12,6 +12,7 @@ import { 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'; @@ -276,7 +277,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/TabbedContent/FromAppsContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx new file mode 100644 index 00000000000..9c1f14c99cc --- /dev/null +++ b/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx @@ -0,0 +1,308 @@ +// import { assocPath, pathOr } from 'ramda'; +import { 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 SelectImagePanel from '../SelectImagePanel'; +import SelectPlanPanel from '../SelectPlanPanel'; + +import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { renderBackupsDisplaySection } from './utils'; + +import { + 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' +}; + +interface Props {} + +type CombinedProps = Props & + WithStyles & + WithDisplayData & + StackScriptFormStateHandlers & + WithAll; + +const FromAppsContent: React.SFC = props => { + const { + accountBackupsEnabled, + classes, + typesData, + regionsData, + imageDisplayInfo, + regionDisplayInfo, + typeDisplayInfo, + backupsMonthlyPrice, + userSSHKeys, + userCannotCreateLinode, + selectedImageID, + selectedRegionID, + selectedStackScriptLabel, + selectedStackScriptUsername, + selectedTypeID, + selectedUDFs: udf_data, + label, + tags, + availableUserDefinedFields: userDefinedFields, + availableStackScriptImages: compatibleImages, + updateImageID, + updateLabel, + updatePassword, + updateRegionID, + updateTags, + updateTypeID, + formIsSubmitting, + password, + backupsEnabled, + toggleBackupsEnabled, + privateIPEnabled, + togglePrivateIPEnabled, + errors + } = props; + + const hasBackups = props.backupsEnabled || accountBackupsEnabled; + const hasErrorFor = getAPIErrorsFor(errorResources, errors); + const generalError = hasErrorFor('none'); + + return ( + + + + {generalError && } +
select app panel
+ {!userCannotCreateLinode && + userDefinedFields && + userDefinedFields.length > 0 && ( + null} + userDefinedFields={userDefinedFields} + updateFor={[userDefinedFields, udf_data, errors]} + udf_data={udf_data || {}} + /> + )} + {!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 ( + null} + displaySections={displaySections} + {...stickyProps} + /> + ); + }} + + +
+ ); +}; + +/** + * @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); + +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', From bb5bbae4a0715857f8966b77a129b83af1bc2033 Mon Sep 17 00:00:00 2001 From: mmckenna Date: Tue, 5 Mar 2019 12:17:56 -0500 Subject: [PATCH 2/9] cleanup --- .../TabbedContent/FromAppsContent.tsx | 30 +--------- .../TabbedContent/FromStackScriptContent.tsx | 55 ++----------------- .../TabbedContent/formUtilities.ts | 29 ++++++++++ 3 files changed, 36 insertions(+), 78 deletions(-) create mode 100644 src/features/linodes/LinodesCreate/TabbedContent/formUtilities.ts diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx index 9c1f14c99cc..a0fb2c323b7 100644 --- a/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx +++ b/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx @@ -25,6 +25,7 @@ import SelectImagePanel from '../SelectImagePanel'; import SelectPlanPanel from '../SelectPlanPanel'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { filterUDFErrors } from './formUtilities'; import { renderBackupsDisplaySection } from './utils'; import { @@ -118,7 +119,7 @@ const FromAppsContent: React.SFC = props => { userDefinedFields && userDefinedFields.length > 0 && ( null} @@ -273,33 +274,6 @@ const FromAppsContent: React.SFC = props => { ); }; -/** - * @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); export default compose( diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx index 40f5f7051ea..a934a558704 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 { @@ -32,15 +34,9 @@ import { WithDisplayData } from '../types'; -type ClassNames = - | 'root' - | 'main' - | 'sidebar' - | 'emptyImagePanel' - | 'emptyImagePanelText'; +type ClassNames = 'main' | 'sidebar' | 'emptyImagePanel' | 'emptyImagePanelText'; const styles: StyleRulesCallback = theme => ({ - root: {}, main: { '&.mlMain': { [theme.breakpoints.up('lg')]: { @@ -63,13 +59,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, @@ -178,7 +168,6 @@ export class FromStackScriptContent extends React.PureComponent { const { accountBackupsEnabled, errors, - notice, backupsMonthlyPrice, regionsData, typesData, @@ -221,15 +210,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..518cf665571 --- /dev/null +++ b/src/features/linodes/LinodesCreate/TabbedContent/formUtilities.ts @@ -0,0 +1,29 @@ +/** + * @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 + ); + }); +}; From 390cf6525191968a197dc40468fe2a00d39ad766 Mon Sep 17 00:00:00 2001 From: mmckenna Date: Tue, 5 Mar 2019 12:22:36 -0500 Subject: [PATCH 3/9] init createapppanel --- .../TabbedContent/SelectAppPanel.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/features/linodes/LinodesCreate/TabbedContent/SelectAppPanel.tsx diff --git a/src/features/linodes/LinodesCreate/TabbedContent/SelectAppPanel.tsx b/src/features/linodes/LinodesCreate/TabbedContent/SelectAppPanel.tsx new file mode 100644 index 00000000000..6575f902ce3 --- /dev/null +++ b/src/features/linodes/LinodesCreate/TabbedContent/SelectAppPanel.tsx @@ -0,0 +1,28 @@ +import { + StyleRulesCallback, + withStyles, + WithStyles +} from '@material-ui/core/styles'; +import * as React from 'react'; +import { compose } from 'recompose'; + +type ClassNames = 'root'; + +const styles: StyleRulesCallback = theme => ({ + root: {} +}); + +interface Props {} + +type CombinedProps = Props & WithStyles; + +const SelectAppPanel: React.SFC = props => { + return
hello world
; +}; + +const styled = withStyles(styles); + +export default compose( + styled, + React.memo +)(SelectAppPanel); From 8662431c416c6326e399adc5e0441f92692edb45 Mon Sep 17 00:00:00 2001 From: mmckenna Date: Tue, 5 Mar 2019 15:39:10 -0500 Subject: [PATCH 4/9] add apps panel --- .../LinodesCreate/LinodeCreateContainer.tsx | 3 +- src/features/linodes/LinodesCreate/Panel.tsx | 11 +- .../linodes/LinodesCreate/SelectAppPanel.tsx | 169 ++++++ .../TabbedContent/FromAppsContent.tsx | 516 +++++++++++------- .../TabbedContent/FromStackScriptContent.tsx | 5 +- .../TabbedContent/SelectAppPanel.tsx | 28 - .../TabbedContent/formUtilities.ts | 15 + 7 files changed, 513 insertions(+), 234 deletions(-) create mode 100644 src/features/linodes/LinodesCreate/SelectAppPanel.tsx delete mode 100644 src/features/linodes/LinodesCreate/TabbedContent/SelectAppPanel.tsx diff --git a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx index c156db78c17..b9a4a52f9b0 100644 --- a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -99,7 +99,8 @@ const defaultState: State = { selectedRegionID: undefined, selectedTypeID: undefined, tags: [], - formIsSubmitting: false + formIsSubmitting: false, + errors: undefined }; const getRegionIDFromLinodeID = ( 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..6ed11c16239 --- /dev/null +++ b/src/features/linodes/LinodesCreate/SelectAppPanel.tsx @@ -0,0 +1,169 @@ +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'; + +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 { + stackScripts?: Linode.StackScript.Response[]; + stackScriptsLoading: boolean; + stackScriptsError?: string; + 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, + stackScripts, + stackScriptsError, + stackScriptsLoading, + handleClick + } = props; + + if (stackScriptsError) { + return ( + + + + ); + } + + if (stackScriptsLoading) { + return ( + + + + ); + } + + if (!stackScripts) { + return null; + } + + /** hacky and bad */ + const interceptedError = error ? 'You must select an App to create from' : ''; + + return ( + + + {stackScripts.map(eachStackScript => ( + + ))} + + + ); +}; + +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 index a0fb2c323b7..48935dbe78a 100644 --- a/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx +++ b/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx @@ -1,5 +1,4 @@ -// import { assocPath, pathOr } from 'ramda'; -import { pathOr } from 'ramda'; +import { assocPath, pathOr } from 'ramda'; import * as React from 'react'; import { Sticky, StickyProps } from 'react-sticky'; import { compose } from 'recompose'; @@ -18,14 +17,15 @@ 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 { 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 { filterUDFErrors, getCloudApps } from './formUtilities'; import { renderBackupsDisplaySection } from './utils'; import { @@ -57,7 +57,8 @@ const errorResources = { label: 'A label', root_pass: 'A root password', image: 'Image', - tags: 'Tags' + tags: 'Tags', + stackscript_id: 'A StackScript' }; interface Props {} @@ -68,211 +69,328 @@ type CombinedProps = Props & StackScriptFormStateHandlers & WithAll; -const FromAppsContent: React.SFC = props => { - const { - accountBackupsEnabled, - classes, - typesData, - regionsData, - imageDisplayInfo, - regionDisplayInfo, - typeDisplayInfo, - backupsMonthlyPrice, - userSSHKeys, - userCannotCreateLinode, - selectedImageID, - selectedRegionID, - selectedStackScriptLabel, - selectedStackScriptUsername, - selectedTypeID, - selectedUDFs: udf_data, - label, - tags, - availableUserDefinedFields: userDefinedFields, - availableStackScriptImages: compatibleImages, - updateImageID, - updateLabel, - updatePassword, - updateRegionID, - updateTags, - updateTypeID, - formIsSubmitting, - password, - backupsEnabled, - toggleBackupsEnabled, - privateIPEnabled, - togglePrivateIPEnabled, - errors - } = props; +interface State { + stackScripts?: Linode.StackScript.Response[]; + stackScriptsLoading: boolean; + stackScriptsError?: string; +} - const hasBackups = props.backupsEnabled || accountBackupsEnabled; - const hasErrorFor = getAPIErrorsFor(errorResources, errors); - const generalError = hasErrorFor('none'); +class FromAppsContent extends React.PureComponent { + state: State = { + stackScriptsLoading: true + }; - return ( - - - - {generalError && } -
select app panel
- {!userCannotCreateLinode && - userDefinedFields && - userDefinedFields.length > 0 && ( - null} - userDefinedFields={userDefinedFields} - updateFor={[userDefinedFields, udf_data, errors]} - udf_data={udf_data || {}} + componentDidMount() { + getCloudApps() + .then(response => { + this.setState({ + stackScriptsLoading: false, + stackScripts: response.data + }); + }) + .catch(e => { + this.setState({ + stackScriptsLoading: false, + stackScriptsError: 'There was an error loading Cloud Apps.' + }); + }); + } + + 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 + } = this.props; + + const { stackScripts, stackScriptsError, stackScriptsLoading } = this.state; + + 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 + + )} - {!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); + + + 0 && selectedImageID ? userSSHKeys : []} + /> + + + + + {(stickyProps: StickyProps) => { + const displaySections = []; + if (imageDisplayInfo) { + displaySections.push(imageDisplayInfo); + } - if (regionDisplayInfo) { - displaySections.push({ - title: regionDisplayInfo.title, - details: regionDisplayInfo.details - }); - } + if (regionDisplayInfo) { + displaySections.push({ + title: regionDisplayInfo.title, + details: regionDisplayInfo.details + }); + } - if (typeDisplayInfo) { - displaySections.push(typeDisplayInfo); - } + if (typeDisplayInfo) { + displaySections.push(typeDisplayInfo); + } - if ( - hasBackups && - typeDisplayInfo && - typeDisplayInfo.backupsMonthly - ) { - displaySections.push( - renderBackupsDisplaySection( - accountBackupsEnabled, - typeDisplayInfo.backupsMonthly - ) - ); - } + 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; - } + let calculatedPrice = pathOr(0, ['monthly'], typeDisplayInfo); + if ( + hasBackups && + typeDisplayInfo && + typeDisplayInfo.backupsMonthly + ) { + calculatedPrice += typeDisplayInfo.backupsMonthly; + } - return ( - null} - displaySections={displaySections} - {...stickyProps} - /> - ); - }} - - - - ); -}; + return ( + + ); + }} + +
+
+ ); + } +} const styled = withStyles(styles); diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx index a934a558704..a87f1300c19 100644 --- a/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx +++ b/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx @@ -74,7 +74,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; @@ -143,6 +144,7 @@ export class FromStackScriptContent extends React.PureComponent { selectedStackScriptID, selectedTypeID, selectedUDFs, + privateIPEnabled, tags } = this.props; @@ -155,6 +157,7 @@ export class FromStackScriptContent extends React.PureComponent { root_pass: password /* required if image ID is provided */, image: selectedImageID /* optional */, backups_enabled: backupsEnabled /* optional */, + private_ip: privateIPEnabled, booted: true, private_ip: privateIPEnabled, authorized_users: userSSHKeys diff --git a/src/features/linodes/LinodesCreate/TabbedContent/SelectAppPanel.tsx b/src/features/linodes/LinodesCreate/TabbedContent/SelectAppPanel.tsx deleted file mode 100644 index 6575f902ce3..00000000000 --- a/src/features/linodes/LinodesCreate/TabbedContent/SelectAppPanel.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { - StyleRulesCallback, - withStyles, - WithStyles -} from '@material-ui/core/styles'; -import * as React from 'react'; -import { compose } from 'recompose'; - -type ClassNames = 'root'; - -const styles: StyleRulesCallback = theme => ({ - root: {} -}); - -interface Props {} - -type CombinedProps = Props & WithStyles; - -const SelectAppPanel: React.SFC = props => { - return
hello world
; -}; - -const styled = withStyles(styles); - -export default compose( - styled, - React.memo -)(SelectAppPanel); diff --git a/src/features/linodes/LinodesCreate/TabbedContent/formUtilities.ts b/src/features/linodes/LinodesCreate/TabbedContent/formUtilities.ts index 518cf665571..a9488bcd589 100644 --- a/src/features/linodes/LinodesCreate/TabbedContent/formUtilities.ts +++ b/src/features/linodes/LinodesCreate/TabbedContent/formUtilities.ts @@ -1,3 +1,5 @@ +import { getStackscripts } from 'src/services/stackscripts'; + /** * @returns { Linode.Image[] } - a list of public images AKA * images that are officially supported by Linode @@ -27,3 +29,16 @@ export const filterUDFErrors = ( ); }); }; + +/** + * 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' + }); From 025b2847ef8cb10ccaf890ed1e7d03297ffbefc7 Mon Sep 17 00:00:00 2001 From: mmckenna Date: Tue, 5 Mar 2019 17:11:33 -0500 Subject: [PATCH 5/9] update label logic --- .../LinodesCreate/LinodeCreateContainer.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx index b9a4a52f9b0..da680728146 100644 --- a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -181,7 +181,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, @@ -192,6 +200,7 @@ class LinodeCreateContainer extends React.PureComponent { /** reset image because stackscript might not be compatible with selected one */ selectedImageID: undefined }); + }; setDiskSize = (size: number) => this.setState({ selectedDiskSize: size }); @@ -209,14 +218,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) From 91013494d1c3eb613f80d4912018d6eac9b831b1 Mon Sep 17 00:00:00 2001 From: mmckenna Date: Tue, 5 Mar 2019 17:28:09 -0500 Subject: [PATCH 6/9] move apps request to container --- .../linodes/LinodesCreate/CALinodeCreate.tsx | 20 ++- .../LinodesCreate/LinodeCreateContainer.tsx | 139 +++++++++++------- .../linodes/LinodesCreate/SelectAppPanel.tsx | 37 +++-- .../TabbedContent/FromAppsContent.tsx | 54 ++----- src/features/linodes/LinodesCreate/types.ts | 5 + 5 files changed, 138 insertions(+), 117 deletions(-) diff --git a/src/features/linodes/LinodesCreate/CALinodeCreate.tsx b/src/features/linodes/LinodesCreate/CALinodeCreate.tsx index 403228442c6..e72e2b99146 100644 --- a/src/features/linodes/LinodesCreate/CALinodeCreate.tsx +++ b/src/features/linodes/LinodesCreate/CALinodeCreate.tsx @@ -7,6 +7,7 @@ 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 @@ -25,6 +26,7 @@ import { import { AllFormStateAndHandlers, + AppsData, WithAll, WithDisplayData, WithLinodesImagesTypesAndRegions @@ -38,7 +40,9 @@ type CombinedProps = Props & WithLinodesImagesTypesAndRegions & WithDisplayData & WithAll & - AllFormStateAndHandlers; + AppsData & + AllFormStateAndHandlers & + userSSHKeyProps; interface State { selectedTab: number; @@ -47,7 +51,7 @@ interface State { export class LinodeCreate extends React.PureComponent< CombinedProps & DispatchProps, State -> { + > { constructor(props: CombinedProps & DispatchProps) { super(props); @@ -108,6 +112,9 @@ export class LinodeCreate extends React.PureComponent< selectedStackScriptUsername, selectedStackScriptLabel, selectedLinodeID, + appInstances, + appInstancesError, + appInstancesLoading, ...rest } = this.props; return ( @@ -169,6 +176,9 @@ export class LinodeCreate extends React.PureComponent< selectedStackScriptUsername, selectedStackScriptLabel, selectedLinodeID, + appInstances, + appInstancesError, + appInstancesLoading, ...rest } = this.props; @@ -232,6 +242,9 @@ export class LinodeCreate extends React.PureComponent< linodesError, regionsLoading, regionsError, + appInstances, + appInstancesError, + appInstancesLoading, ...rest } = this.props; return ( @@ -257,6 +270,9 @@ export class LinodeCreate extends React.PureComponent< updateLinodeID, selectedBackupID, setBackupID, + appInstances, + appInstancesError, + appInstancesLoading, ...rest } = this.props; return ( diff --git a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx index da680728146..7161dc8b588 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 & @@ -100,7 +104,8 @@ const defaultState: State = { selectedTypeID: undefined, tags: [], formIsSubmitting: false, - errors: undefined + errors: undefined, + appInstancesLoading: false }; const getRegionIDFromLinodeID = ( @@ -128,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); @@ -434,62 +456,67 @@ class LinodeCreateContainer extends React.PureComponent { Create New Linode +
-
); diff --git a/src/features/linodes/LinodesCreate/SelectAppPanel.tsx b/src/features/linodes/LinodesCreate/SelectAppPanel.tsx index 6ed11c16239..5d95a19899b 100644 --- a/src/features/linodes/LinodesCreate/SelectAppPanel.tsx +++ b/src/features/linodes/LinodesCreate/SelectAppPanel.tsx @@ -12,6 +12,8 @@ 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 => ({ @@ -28,10 +30,7 @@ const styles: StyleRulesCallback = theme => ({ } }); -interface Props { - stackScripts?: Linode.StackScript.Response[]; - stackScriptsLoading: boolean; - stackScriptsError?: string; +interface Props extends AppsData { handleClick: ( id: number, label: string, @@ -52,21 +51,21 @@ const SelectAppPanel: React.SFC = props => { selectedStackScriptID, classes, error, - stackScripts, - stackScriptsError, - stackScriptsLoading, + appInstances, + appInstancesError, + appInstancesLoading, handleClick } = props; - if (stackScriptsError) { + if (appInstancesError) { return ( - + ); } - if (stackScriptsLoading) { + if (appInstancesLoading) { return ( @@ -74,7 +73,7 @@ const SelectAppPanel: React.SFC = props => { ); } - if (!stackScripts) { + if (!appInstances) { return null; } @@ -88,17 +87,17 @@ const SelectAppPanel: React.SFC = props => { title="Select App" > - {stackScripts.map(eachStackScript => ( + {appInstances.map(eachApp => ( ))} diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx index 48935dbe78a..26e44eaf189 100644 --- a/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx +++ b/src/features/linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx @@ -25,10 +25,11 @@ import SelectImagePanel from '../SelectImagePanel'; import SelectPlanPanel from '../SelectPlanPanel'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; -import { filterUDFErrors, getCloudApps } from './formUtilities'; +import { filterUDFErrors } from './formUtilities'; import { renderBackupsDisplaySection } from './utils'; import { + AppsData, StackScriptFormStateHandlers, WithAll, WithDisplayData @@ -61,41 +62,13 @@ const errorResources = { stackscript_id: 'A StackScript' }; -interface Props {} +type InnerProps = WithDisplayData & WithAll & AppsData; -type CombinedProps = Props & +type CombinedProps = InnerProps & WithStyles & - WithDisplayData & - StackScriptFormStateHandlers & - WithAll; - -interface State { - stackScripts?: Linode.StackScript.Response[]; - stackScriptsLoading: boolean; - stackScriptsError?: string; -} - -class FromAppsContent extends React.PureComponent { - state: State = { - stackScriptsLoading: true - }; - - componentDidMount() { - getCloudApps() - .then(response => { - this.setState({ - stackScriptsLoading: false, - stackScripts: response.data - }); - }) - .catch(e => { - this.setState({ - stackScriptsLoading: false, - stackScriptsError: 'There was an error loading Cloud Apps.' - }); - }); - } + StackScriptFormStateHandlers; +class FromAppsContent extends React.PureComponent { handleSelectStackScript = ( id: number, label: string, @@ -209,11 +182,12 @@ class FromAppsContent extends React.PureComponent { toggleBackupsEnabled, privateIPEnabled, togglePrivateIPEnabled, - errors + errors, + appInstances, + appInstancesError, + appInstancesLoading } = this.props; - const { stackScripts, stackScriptsError, stackScriptsLoading } = this.state; - const hasBackups = backupsEnabled || accountBackupsEnabled; const hasErrorFor = getAPIErrorsFor(errorResources, errors); const generalError = hasErrorFor('none'); @@ -224,9 +198,9 @@ class FromAppsContent extends React.PureComponent { {generalError && } { const styled = withStyles(styles); -export default compose( +export default compose( styled, React.memo )(FromAppsContent); 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 & From 4f9c63c8fb79f839c72cf03b8764f9570451b973 Mon Sep 17 00:00:00 2001 From: mmckenna Date: Thu, 7 Mar 2019 09:29:55 -0500 Subject: [PATCH 7/9] reset errors on new stackscript selection --- src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx index 7161dc8b588..c32d965fee5 100644 --- a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -220,7 +220,8 @@ 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 }); }; From ef729e7b9b6de71a95a8679b80b5a27c2bcab6c2 Mon Sep 17 00:00:00 2001 From: mmckenna Date: Thu, 7 Mar 2019 09:33:25 -0500 Subject: [PATCH 8/9] fix placement of closing grid tag --- .../LinodesCreate/LinodeCreateContainer.tsx | 118 +++++++++--------- 1 file changed, 58 insertions(+), 60 deletions(-) diff --git a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx index c32d965fee5..c1dadcb9b45 100644 --- a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -457,67 +457,65 @@ class LinodeCreateContainer extends React.PureComponent { Create New Linode - + ); From 9fd934ca818f89264f58177e50234305f0721344 Mon Sep 17 00:00:00 2001 From: mmckenna Date: Thu, 7 Mar 2019 10:06:29 -0500 Subject: [PATCH 9/9] fix rebase issues --- .../TabbedContent/FromStackScriptContent.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx index a87f1300c19..f67976c6893 100644 --- a/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx +++ b/src/features/linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx @@ -34,7 +34,11 @@ import { WithDisplayData } from '../types'; -type ClassNames = 'main' | 'sidebar' | 'emptyImagePanel' | 'emptyImagePanelText'; +type ClassNames = + | 'main' + | 'sidebar' + | 'emptyImagePanel' + | 'emptyImagePanelText'; const styles: StyleRulesCallback = theme => ({ main: { @@ -144,7 +148,6 @@ export class FromStackScriptContent extends React.PureComponent { selectedStackScriptID, selectedTypeID, selectedUDFs, - privateIPEnabled, tags } = this.props; @@ -157,7 +160,6 @@ export class FromStackScriptContent extends React.PureComponent { root_pass: password /* required if image ID is provided */, image: selectedImageID /* optional */, backups_enabled: backupsEnabled /* optional */, - private_ip: privateIPEnabled, booted: true, private_ip: privateIPEnabled, authorized_users: userSSHKeys