diff --git a/src/features/linodes/LinodesCreate/CALinodeCreate.tsx b/src/features/linodes/LinodesCreate/CALinodeCreate.tsx index d243849e532..7de930a863a 100644 --- a/src/features/linodes/LinodesCreate/CALinodeCreate.tsx +++ b/src/features/linodes/LinodesCreate/CALinodeCreate.tsx @@ -9,6 +9,7 @@ import ErrorState from 'src/components/ErrorState'; import Grid from 'src/components/Grid'; import { getStackScriptsByUser } from 'src/features/StackScripts/stackScriptUtils'; import SubTabs, { Tab } from './CALinodeCreateSubTabs'; +import FromBackupsContent from './TabbedContent/FromBackupsContent'; import FromImageContent from './TabbedContent/FromImageContent'; import FromLinodeContent from './TabbedContent/FromLinodeContent'; import FromStackScriptContent from './TabbedContent/FromStackScriptContent'; @@ -180,7 +181,33 @@ export class LinodeCreate extends React.PureComponent< title: 'Backups', type: 'fromBackup', render: () => { - return ; + const { + history, + handleSelectUDFs, + selectedUDFs, + updateStackScript, + availableStackScriptImages, + availableUserDefinedFields, + selectedStackScriptID, + selectedStackScriptUsername, + selectedStackScriptLabel, + linodesLoading, + updateDiskSize, + updatePassword, + ...rest + } = this.props; + return ( + + ); } }, { diff --git a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx index 36b80b482da..c155cb5b14e 100644 --- a/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/src/features/linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -59,6 +59,7 @@ interface State { selectedRegionID?: string; selectedTypeID?: string; selectedLinodeID?: number; + selectedBackupID?: number; availableUserDefinedFields?: Linode.StackScript.UserDefinedField[]; availableStackScriptImages?: Linode.Image[]; selectedStackScriptID?: number; @@ -91,6 +92,7 @@ const defaultState: State = { label: '', password: '', selectedImageID: 'linode/debian9', + selectedBackupID: undefined, selectedDiskSize: undefined, selectedLinodeID: undefined, selectedStackScriptID: undefined, @@ -100,6 +102,14 @@ const defaultState: State = { formIsSubmitting: false }; +const getRegionIDFromLinodeID = ( + linodes: Linode.Linode[], + id: number +): string | undefined => { + const thisLinode = linodes.find(linode => linode.id === id); + return thisLinode ? thisLinode.region : undefined; +}; + class LinodeCreateContainer extends React.PureComponent { state: State = defaultState; @@ -130,6 +140,10 @@ class LinodeCreateContainer extends React.PureComponent { return this.setState({ selectedImageID: id }); }; + setBackupID = (id: number) => { + this.setState({ selectedBackupID: id }); + }; + setRegionID = (id: string) => this.setState({ selectedRegionID: id }); setTypeID = (id: string) => this.setState({ selectedTypeID: id }); @@ -139,12 +153,22 @@ class LinodeCreateContainer extends React.PureComponent { /** * reset selected plan and set the selectedDiskSize * for the purpose of disabling plans that are smaller - * than the clone source + * than the clone source. + * + * Also, when creating from backup, we set the region + * to the same region as the Linode that owns the backup, + * since the API does not infer this automatically. */ + + const selectedRegionID = getRegionIDFromLinodeID( + this.props.linodesData, + id + ); this.setState({ selectedLinodeID: id, selectedDiskSize: diskSize, - selectedTypeID: undefined + selectedTypeID: undefined, + selectedRegionID }); } }; @@ -239,7 +263,20 @@ class LinodeCreateContainer extends React.PureComponent { ); } - if (createType === 'fromStackScript' && !this.state.selectedStackScriptID) { + if (type === 'createFromBackup' && !this.state.selectedBackupID) { + /* a backup selection is also required */ + this.setState( + { + errors: [{ field: 'backup_id', reason: 'You must select a Backup' }] + }, + () => { + scrollErrorIntoView(); + } + ); + return; + } + + if (type === 'createFromStackScript' && !this.state.selectedStackScriptID) { return this.setState( () => ({ errors: [ @@ -431,6 +468,8 @@ class LinodeCreateContainer extends React.PureComponent { resetCreationState={this.clearCreationState} userSSHKeys={this.props.userSSHKeys} resetSSHKeys={this.props.resetSSHKeys} + selectedBackupID={this.state.selectedBackupID} + setBackupID={this.setBackupID} /> diff --git a/src/features/linodes/LinodesCreate/SelectLinodePanel.tsx b/src/features/linodes/LinodesCreate/SelectLinodePanel.tsx index 4749f8220a5..9599711ae85 100644 --- a/src/features/linodes/LinodesCreate/SelectLinodePanel.tsx +++ b/src/features/linodes/LinodesCreate/SelectLinodePanel.tsx @@ -38,7 +38,7 @@ const styles: StyleRulesCallback = theme => ({ interface Props { linodes: ExtendedLinode[]; selectedLinodeID?: number; - handleSelection: (linode: Linode.Linode) => void; + handleSelection: (id: number, diskSize?: number) => void; error?: string; header?: string; disabled?: boolean; @@ -55,7 +55,7 @@ class SelectLinodePanel extends React.Component { { - handleSelection(linode); + handleSelection(linode.id, linode.specs.disk); }} checked={linode.id === Number(selectedLinodeID)} heading={linode.heading} diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx index 5ccaa5375fd..00a865cedc9 100644 --- a/src/features/linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx +++ b/src/features/linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx @@ -1,545 +1,387 @@ -// import * as Promise from 'bluebird'; -// import { InjectedNotistackProps, withSnackbar } from 'notistack'; -// import { compose as ramdaCompose, pathOr } from 'ramda'; -// import * as React from 'react'; -// import { Sticky, StickyProps } from 'react-sticky'; -// import { compose } from 'recompose'; -// import VolumeIcon from 'src/assets/addnewmenu/volume.svg'; -// import CheckoutBar from 'src/components/CheckoutBar'; -// import CircleProgress from 'src/components/CircleProgress'; -// import { -// StyleRulesCallback, -// withStyles, -// WithStyles -// } from 'src/components/core/styles'; -// 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 Placeholder from 'src/components/Placeholder'; -// import { Tag } from 'src/components/TagsInput'; -// import { resetEventsPolling } from 'src/events'; -// import { getLinodeBackups } from 'src/services/linodes'; -// import { -// LinodeActionsProps, -// withLinodeActions -// } from 'src/store/linodes/linode.containers'; -// import { allocatePrivateIP } from 'src/utilities/allocateIPAddress'; -// import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -// import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; -// import getLinodeInfo from 'src/utilities/getLinodeInfo'; -// import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; -// import { aggregateBackups } from '../../LinodesDetail/LinodeBackup'; -// import AddonsPanel from '../AddonsPanel'; -// import SelectBackupPanel from '../SelectBackupPanel'; -// import SelectLinodePanel, { ExtendedLinode } from '../SelectLinodePanel'; -// import SelectPlanPanel, { ExtendedType } from '../SelectPlanPanel'; -// import { Info } from '../util'; -// import withLabelGenerator, { LabelProps } from '../withLabelGenerator'; -// import { renderBackupsDisplaySection } from './utils'; - -// type ClassNames = 'root' | 'main' | 'sidebar'; - -// const styles: StyleRulesCallback = theme => ({ -// root: {}, -// main: {}, -// sidebar: { -// [theme.breakpoints.up('lg')]: { -// marginTop: -130 -// } -// } -// }); - -// export type TypeInfo = -// | { -// title: string; -// details: string; -// monthly: number; -// backupsMonthly: number | null; -// } -// | undefined; - -// interface Props { -// notice?: Notice; -// linodes: Linode.Linode[]; -// types: ExtendedType[]; -// extendLinodes: (linodes: Linode.Linode[]) => ExtendedLinode[]; -// getBackupsMonthlyPrice: (selectedTypeID: string | null) => number | null; -// getTypeInfo: (selectedTypeID: string | null) => TypeInfo; -// getRegionInfo: (selectedRegionID: string | null) => Info; -// history: any; -// selectedBackupFromQuery?: number; -// selectedLinodeFromQuery?: number; -// selectedRegionIDFromLinode?: string; -// accountBackups: boolean; -// disabled?: boolean; -// } - -// interface State { -// linodesWithBackups: Linode.LinodeWithBackups[] | null; -// isGettingBackups: boolean; -// userHasBackups: boolean; -// selectedLinodeID: number | undefined; -// selectedBackupID: number | undefined; -// selectedDiskSize: number | undefined; -// selectedRegionID: string | null; -// selectedTypeID: string | null; -// label: string; -// errors?: Linode.ApiFieldError[]; -// backups: boolean; -// privateIP: boolean; -// selectedBackupInfo: Info; -// isMakingRequest: boolean; -// backupInfo: Info; -// tags: Tag[]; -// } - -// type CombinedProps = Props & -// LinodeActionsProps & -// InjectedNotistackProps & -// LabelProps & -// WithStyles; - -// interface Notice { -// text: string; -// level: 'warning' | 'error'; // most likely only going to need these two -// } - -// const errorResources = { -// type: 'A plan selection', -// region: 'A region selection', -// label: 'A label', -// root_pass: 'A root password', -// tags: 'Tags for this Linode' -// }; - -// const filterLinodesWithBackups = (linodes: Linode.LinodeWithBackups[]) => { -// return linodes.filter(linode => { -// const hasAutomaticBackups = !!linode.currentBackups.automatic.length; -// const hasSnapshotBackup = !!linode.currentBackups.snapshot.current; -// // backups both need to be enabled and some backups need to exist -// // for the panel to show the Linode -// return linode.backups.enabled && (hasAutomaticBackups || hasSnapshotBackup); -// }); -// }; - -// export class FromBackupsContent extends React.Component { -// state: State = { -// linodesWithBackups: [], -// isGettingBackups: false, -// userHasBackups: false, -// selectedLinodeID: this.props.selectedLinodeFromQuery || undefined, -// selectedBackupID: this.props.selectedBackupFromQuery || undefined, -// selectedDiskSize: undefined, -// selectedRegionID: this.props.selectedRegionIDFromLinode || null, -// selectedTypeID: null, -// label: '', -// backups: false, -// privateIP: false, -// selectedBackupInfo: undefined, -// isMakingRequest: false, -// backupInfo: undefined, -// tags: [] -// }; - -// mounted: boolean = false; - -// getLinodesWithBackups = (linodes: Linode.Linode[]) => { -// this.setState({ isGettingBackups: true }); -// return Promise.map( -// linodes.filter(l => l.backups.enabled), -// (linode: Linode.Linode) => { -// return getLinodeBackups(linode.id).then(backups => { -// return { -// ...linode, -// currentBackups: { -// ...backups -// } -// }; -// }); -// } -// ) -// .then(data => { -// if (!this.mounted) { -// return; -// } -// this.setState({ linodesWithBackups: data, isGettingBackups: false }); -// }) -// .catch(err => this.setState({ isGettingBackups: false })); -// }; - -// userHasBackups = () => { -// const { linodesWithBackups } = this.state; -// return linodesWithBackups!.some((linode: Linode.LinodeWithBackups) => { -// // automatic backups is an array, but snapshots are either null or an object -// // user can have up to 3 automatic backups, but one one snapshot -// return ( -// !!linode.currentBackups.automatic.length || -// !!linode.currentBackups.snapshot.current -// ); -// }); -// }; - -// handleSelectLinode = (linode: Linode.Linode) => { -// if (linode.id !== this.state.selectedLinodeID) { -// this.setState({ -// selectedLinodeID: linode.id, -// selectedTypeID: null, -// selectedRegionID: linode.region, -// selectedDiskSize: linode.specs.disk, -// selectedBackupID: undefined -// }); -// } -// }; - -// handleSelectBackupID = (id: number) => { -// this.setState({ selectedBackupID: id }); -// }; - -// handleSelectBackupInfo = (info: Info) => { -// this.setState({ backupInfo: info }); -// }; - -// handleSelectPlan = (id: string) => { -// this.setState({ selectedTypeID: id }); -// }; - -// handleSelectLabel = (e: any) => { -// this.setState({ label: e.target.value }); -// }; - -// handleChangeTags = (selected: Tag[]) => { -// this.setState({ tags: selected }); -// }; - -// handleToggleBackups = () => { -// this.setState({ backups: !this.state.backups }); -// }; - -// handleTogglePrivateIP = () => { -// this.setState({ privateIP: !this.state.privateIP }); -// }; - -// deployLinode = () => { -// if (!this.state.selectedBackupID) { -// /* a backup selection is also required */ -// this.setState( -// { -// errors: [{ field: 'backup_id', reason: 'You must select a Backup' }] -// }, -// () => { -// scrollErrorIntoView(); -// } -// ); -// return; -// } -// this.createLinode(); -// }; - -// createLinode = () => { -// const { -// history, -// linodeActions: { createLinode } -// } = this.props; -// const { -// selectedRegionID, -// selectedTypeID, -// backups, -// privateIP, -// selectedBackupID, -// tags -// } = this.state; - -// this.setState({ isMakingRequest: true }); - -// const label = this.label(); - -// createLinode({ -// region: selectedRegionID, -// type: selectedTypeID, -// backup_id: Number(selectedBackupID), -// label: label ? label : null /* optional */, -// backups_enabled: backups /* optional */, -// booted: true, -// tags: tags.map((item: Tag) => item.value) -// }) -// .then(linode => { -// if (privateIP) { -// allocatePrivateIP(linode.id); -// } - -// this.props.enqueueSnackbar(`Your Linode ${label} is being created.`, { -// variant: 'success' -// }); - -// resetEventsPolling(); -// history.push('/linodes'); -// }) -// .catch(error => { -// if (!this.mounted) { -// return; -// } - -// this.setState(() => ({ -// errors: getAPIErrorOrDefault(error) -// })); -// }) -// .finally(() => { -// if (!this.mounted) { -// return; -// } -// // regardless of whether request failed or not, change state and enable the submit btn -// this.setState({ isMakingRequest: false }); -// }); -// }; - -// componentWillUnmount() { -// this.mounted = false; -// } - -// componentDidMount() { -// this.mounted = true; -// this.getLinodesWithBackups(this.props.linodes); -// const { selectedLinodeID } = this.state; -// // If there is a selected Linode ID (from props), make sure its information -// // is set to state as if it had been selected manually. -// if (selectedLinodeID) { -// const selectedLinode = getLinodeInfo( -// selectedLinodeID, -// this.props.linodes -// ); -// if (selectedLinode) { -// this.setState({ -// selectedLinodeID: selectedLinode.id, -// selectedTypeID: null, -// selectedRegionID: selectedLinode.region, -// selectedDiskSize: selectedLinode.specs.disk -// }); -// } -// } -// } - -// // Generate a default label name with a selected Linode and/or Backup name IF they are selected -// label = () => { -// const { -// linodesWithBackups, -// selectedBackupID, -// selectedLinodeID -// } = this.state; -// const { getLabel } = this.props; - -// const selectedLinode = -// linodesWithBackups && -// linodesWithBackups.find(l => l.id === selectedLinodeID); - -// if (!selectedLinode) { -// return getLabel(); -// } - -// const selectedBackup = aggregateBackups(selectedLinode.currentBackups).find( -// b => b.id === selectedBackupID -// ); - -// if (!selectedBackup) { -// return getLabel(selectedLinode.label, 'backup'); -// } - -// const backup = -// selectedBackup.type !== 'auto' ? selectedBackup.label : 'auto'; // automatic backups have a label of 'null', so use a custom string for these - -// return getLabel(selectedLinode.label, backup, 'backup'); -// }; - -// render() { -// const { -// errors, -// selectedBackupID, -// selectedDiskSize, -// selectedLinodeID, -// tags, -// selectedTypeID, -// selectedRegionID, -// backups, -// linodesWithBackups, -// privateIP, -// selectedBackupInfo, -// isMakingRequest -// } = this.state; -// const { -// accountBackups, -// extendLinodes, -// getBackupsMonthlyPrice, -// classes, -// notice, -// types, -// getRegionInfo, -// getTypeInfo, -// updateCustomLabel, -// disabled -// } = this.props; -// const hasErrorFor = getAPIErrorsFor(errorResources, errors); -// const generalError = hasErrorFor('none'); - -// const imageInfo = selectedBackupInfo; - -// const regionInfo = selectedRegionID && getRegionInfo(selectedRegionID); - -// const typeInfo = getTypeInfo(selectedTypeID); - -// const hasBackups = backups || accountBackups; - -// const label = this.label(); - -// return ( -// -// -// {this.state.isGettingBackups ? ( -// -// ) : !this.userHasBackups() ? ( -// -// ) : ( -// -// -// {notice && !disabled && ( -// -// )} -// {generalError && } -// -// extendLinodes(linodes), -// filterLinodesWithBackups -// )(linodesWithBackups!)} -// selectedLinodeID={selectedLinodeID} -// handleSelection={this.handleSelectLinode} -// updateFor={[selectedLinodeID, errors]} -// disabled={disabled} -// /> -// { -// return linode.id === +selectedLinodeID!; -// } -// )} -// selectedLinodeID={selectedLinodeID} -// selectedBackupID={selectedBackupID} -// handleChangeBackup={this.handleSelectBackupID} -// handleChangeBackupInfo={this.handleSelectBackupInfo} -// updateFor={[selectedLinodeID, selectedBackupID, errors]} -// /> -// -// -// -// -// )} -// -// {!this.userHasBackups() ? ( -// -// ) : ( -// -// -// {(props: StickyProps) => { -// const displaySections = []; -// if (imageInfo) { -// displaySections.push(imageInfo); -// } - -// if (regionInfo) { -// displaySections.push({ -// title: regionInfo.title, -// details: regionInfo.details -// }); -// } - -// if (typeInfo) { -// displaySections.push(typeInfo); -// } - -// if (hasBackups && typeInfo && typeInfo.backupsMonthly) { -// displaySections.push( -// renderBackupsDisplaySection( -// accountBackups, -// typeInfo.backupsMonthly -// ) -// ); -// } - -// let calculatedPrice = pathOr(0, ['monthly'], typeInfo); -// if (hasBackups && typeInfo && typeInfo.backupsMonthly) { -// calculatedPrice += typeInfo.backupsMonthly; -// } - -// return ( -// -// ); -// }} -// -// -// )} -// -// ); -// } -// } - -// const styled = withStyles(styles); - -// const enhanced = compose( -// styled, -// withSnackbar, -// withLabelGenerator, -// withLinodeActions -// ); - -// export default enhanced(FromBackupsContent); +import * as Promise from 'bluebird'; +import { InjectedNotistackProps, withSnackbar } from 'notistack'; +import { compose as ramdaCompose, pathOr } from 'ramda'; +import * as React from 'react'; +import { Sticky, StickyProps } from 'react-sticky'; +import { compose } from 'recompose'; +import VolumeIcon from 'src/assets/addnewmenu/volume.svg'; +import CheckoutBar from 'src/components/CheckoutBar'; +import CircleProgress from 'src/components/CircleProgress'; +import { + StyleRulesCallback, + withStyles, + WithStyles +} from 'src/components/core/styles'; +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 Placeholder from 'src/components/Placeholder'; +import { getLinodeBackups } from 'src/services/linodes'; +import { + LinodeActionsProps, + withLinodeActions +} from 'src/store/linodes/linode.containers'; +import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import AddonsPanel from '../AddonsPanel'; +import SelectBackupPanel from '../SelectBackupPanel'; +import SelectLinodePanel from '../SelectLinodePanel'; +import SelectPlanPanel from '../SelectPlanPanel'; +import { + BackupFormStateHandlers, + Info, + WithAll, + WithDisplayData +} from '../types'; +import { extendLinodes } from '../utilities'; +import { renderBackupsDisplaySection } from './utils'; + +type ClassNames = 'root' | 'main' | 'sidebar'; + +const styles: StyleRulesCallback = theme => ({ + root: {}, + main: {}, + sidebar: { + [theme.breakpoints.up('lg')]: { + marginTop: -130 + } + } +}); + +interface Props { + notice?: Notice; + linodesData: Linode.Linode[]; + selectedBackupFromQuery?: number; + selectedLinodeFromQuery?: number; + selectedRegionIDFromLinode?: string; + disabled?: boolean; +} + +interface State { + linodesWithBackups: Linode.LinodeWithBackups[] | null; + userHasBackups: boolean; + backups: boolean; + selectedBackupInfo: Info; + backupInfo: Info; + isGettingBackups: boolean; +} + +type CombinedProps = Props & + LinodeActionsProps & + InjectedNotistackProps & + BackupFormStateHandlers & + WithAll & + WithDisplayData & + WithStyles; + +interface Notice { + text: string; + level: 'warning' | 'error'; // most likely only going to need these two +} + +const errorResources = { + type: 'A plan selection', + region: 'A region selection', + label: 'A label', + root_pass: 'A root password', + tags: 'Tags for this Linode' +}; + +const filterLinodesWithBackups = (linodes: Linode.LinodeWithBackups[]) => { + return linodes.filter(linode => { + const hasAutomaticBackups = !!linode.currentBackups.automatic.length; + const hasSnapshotBackup = !!linode.currentBackups.snapshot.current; + // backups both need to be enabled and some backups need to exist + // for the panel to show the Linode + return linode.backups.enabled && (hasAutomaticBackups || hasSnapshotBackup); + }); +}; + +export class FromBackupsContent extends React.Component { + state: State = { + linodesWithBackups: [], + userHasBackups: false, + backups: false, + selectedBackupInfo: undefined, + backupInfo: undefined, + isGettingBackups: false + }; + + mounted: boolean = false; + + getLinodesWithBackups = (linodes: Linode.Linode[]) => { + this.setState({ isGettingBackups: true }); + return Promise.map( + linodes.filter(l => l.backups.enabled), + (linode: Linode.Linode) => { + return getLinodeBackups(linode.id).then(backups => { + return { + ...linode, + currentBackups: { + ...backups + } + }; + }); + } + ) + .then(data => { + if (!this.mounted) { + return; + } + this.setState({ linodesWithBackups: data, isGettingBackups: false }); + }) + .catch(err => this.setState({ isGettingBackups: false })); + }; + + userHasBackups = () => { + const { linodesWithBackups } = this.state; + return linodesWithBackups!.some((linode: Linode.LinodeWithBackups) => { + // automatic backups is an array, but snapshots are either null or an object + // user can have up to 3 automatic backups, but one one snapshot + return ( + !!linode.currentBackups.automatic.length || + !!linode.currentBackups.snapshot.current + ); + }); + }; + + handleSelectBackupInfo = (info: Info) => { + this.setState({ backupInfo: info }); + }; + + createLinode = () => { + const { + backupsEnabled, + privateIPEnabled, + selectedTypeID, + selectedRegionID, + selectedBackupID, + label, + tags + } = this.props; + + const tagsToAdd = tags ? tags.map(item => item.value) : undefined; + + this.props.handleSubmitForm('createFromBackup', { + region: selectedRegionID, + type: selectedTypeID, + private_ip: privateIPEnabled, + backup_id: Number(selectedBackupID), + label, + backups_enabled: backupsEnabled /* optional */, + booted: true, + tags: tagsToAdd + }); + }; + + componentWillUnmount() { + this.mounted = false; + } + + componentDidMount() { + this.mounted = true; + this.getLinodesWithBackups(this.props.linodesData); + // If there is a selected Linode ID (from props), make sure its information + // is set to state as if it had been selected manually. + } + + render() { + const { + backups, + linodesWithBackups, + isGettingBackups, + selectedBackupInfo + } = this.state; + const { + accountBackupsEnabled, + classes, + errors, + notice, + privateIPEnabled, + selectedBackupID, + selectedDiskSize, + selectedLinodeID, + selectedTypeID, + setBackupID, + togglePrivateIPEnabled, + toggleBackupsEnabled, + regionDisplayInfo, + typeDisplayInfo, + disabled, + label, + tags, + typesData, + updateLinodeID, + updateTypeID, + updateTags, + backupsMonthlyPrice, + backupsEnabled, + updateLabel + } = this.props; + const hasErrorFor = getAPIErrorsFor(errorResources, errors); + const generalError = hasErrorFor('none'); + + const imageInfo = selectedBackupInfo; + + const hasBackups = backups || accountBackupsEnabled; + + return ( + + + {this.state.isGettingBackups ? ( + + ) : !this.userHasBackups() ? ( + + ) : ( + + + {notice && !disabled && ( + + )} + {generalError && } + + extendLinodes(linodes), + filterLinodesWithBackups + )(linodesWithBackups!)} + selectedLinodeID={selectedLinodeID} + handleSelection={updateLinodeID} + updateFor={[selectedLinodeID, errors]} + disabled={disabled} + /> + { + return linode.id === +selectedLinodeID!; + } + )} + selectedLinodeID={selectedLinodeID} + selectedBackupID={selectedBackupID} + handleChangeBackup={setBackupID} + handleChangeBackupInfo={this.handleSelectBackupInfo} + updateFor={[selectedLinodeID, selectedBackupID, errors]} + /> + + + + + )} + + {!this.userHasBackups() ? ( + + ) : ( + + + {(props: StickyProps) => { + const displaySections = []; + if (imageInfo) { + displaySections.push(imageInfo); + } + + 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); + +const enhanced = compose( + styled, + withSnackbar, + withLinodeActions +); + +export default enhanced(FromBackupsContent); diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx index 5dbd9fdbd4a..fab6de3ee7c 100644 --- a/src/features/linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx +++ b/src/features/linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx @@ -22,7 +22,7 @@ import SelectLinodePanel from '../SelectLinodePanel'; import SelectPlanPanel from '../SelectPlanPanel'; import { renderBackupsDisplaySection } from './utils'; -import { extendLinodes } from '../utilites'; +import { extendLinodes } from '../utilities'; import { CloneFormStateHandlers, @@ -66,8 +66,13 @@ type CombinedProps = Props & export class FromLinodeContent extends React.PureComponent { /** set the Linode ID and the disk size and reset the plan selection */ - handleSelectLinode = (linode: Linode.Linode) => { - this.props.updateLinodeID(linode.id, linode.specs.disk); + handleSelectLinode = (linodeID: number) => { + const linode = this.props.linodesData.find( + linode => linode.id === linodeID + ); + if (linode) { + this.props.updateLinodeID(linode.id, linode.specs.disk); + } }; cloneLinode = () => { diff --git a/src/features/linodes/LinodesCreate/types.ts b/src/features/linodes/LinodesCreate/types.ts index 7577d4c10c7..20d3db71d05 100644 --- a/src/features/linodes/LinodesCreate/types.ts +++ b/src/features/linodes/LinodesCreate/types.ts @@ -55,7 +55,7 @@ export interface ReduxStateProps { } export type HandleSubmit = ( - type: 'create' | 'clone' | 'createFromStackScript', + type: 'create' | 'clone' | 'createFromStackScript' | 'createFromBackup', payload: CreateLinodeRequest, linodeID?: number ) => void; @@ -123,9 +123,15 @@ export interface StackScriptFormStateHandlers extends BaseFormStateAndHandlers { handleSelectUDFs: (stackScripts: any[]) => void; } +export interface BackupFormStateHandlers extends CloneFormStateHandlers { + selectedBackupID?: number; + setBackupID: (id: number) => void; +} + export type AllFormStateAndHandlers = BaseFormStateAndHandlers & CloneFormStateHandlers & - StackScriptFormStateHandlers; + StackScriptFormStateHandlers & + BackupFormStateHandlers; export type WithLinodesImagesTypesAndRegions = WithImagesProps & WithLinodesProps & diff --git a/src/features/linodes/LinodesCreate/utilites.ts b/src/features/linodes/LinodesCreate/utilities.ts similarity index 100% rename from src/features/linodes/LinodesCreate/utilites.ts rename to src/features/linodes/LinodesCreate/utilities.ts