From 01fe2ae79ded831114729f6693d5d0a95e6d3dfd Mon Sep 17 00:00:00 2001 From: Jared Date: Mon, 4 Mar 2019 17:07:35 -0500 Subject: [PATCH] CLAPPS: Add Images subtab (#4599) * WIP * WIP 2 * Fix styling * Fix StackScripts to use new SelectImagePanel * Fix tab labels * Add empty state for private images * Review feedback - Combine renderPublicImages and renderOlderPublicImages into a single function - Use instead of for placeholder message - Only pass needed props to FromImageContent * Pass fewer props to other FromImagesContent instance * Clean up post-rebase --- .../linodes/LinodesCreate/CALinodeCreate.tsx | 47 ++++- src/features/linodes/LinodesCreate/Panel.tsx | 48 +++++ .../linodes/LinodesCreate/PrivateImages.tsx | 51 ++++++ .../linodes/LinodesCreate/PublicImages.tsx | 89 ++++++++++ .../LinodesCreate/SelectImagePanel.tsx | 165 ++++-------------- .../TabbedContent/FromImageContent.tsx | 28 ++- .../TabbedContent/FromStackScriptContent.tsx | 2 +- .../linodeCreate/linodeCreate.reducer.ts | 2 - 8 files changed, 287 insertions(+), 145 deletions(-) create mode 100644 src/features/linodes/LinodesCreate/Panel.tsx create mode 100644 src/features/linodes/LinodesCreate/PrivateImages.tsx create mode 100644 src/features/linodes/LinodesCreate/PublicImages.tsx diff --git a/src/features/linodes/LinodesCreate/CALinodeCreate.tsx b/src/features/linodes/LinodesCreate/CALinodeCreate.tsx index 1f05f8d80bd..d243849e532 100644 --- a/src/features/linodes/LinodesCreate/CALinodeCreate.tsx +++ b/src/features/linodes/LinodesCreate/CALinodeCreate.tsx @@ -93,11 +93,21 @@ export class LinodeCreate extends React.PureComponent< linodesData, linodesError, linodesLoading, + handleSelectUDFs, + selectedUDFs, + updateStackScript, + availableStackScriptImages, + availableUserDefinedFields, + selectedStackScriptID, + selectedDiskSize, + selectedStackScriptUsername, + selectedStackScriptLabel, + selectedLinodeID, ...rest } = this.props; return ( @@ -136,14 +146,45 @@ export class LinodeCreate extends React.PureComponent< myImagesTabs = (): Tab[] => [ { - title: 'Backups and My Images', + title: 'Images', + type: 'fromImage', + render: () => { + const { + history, + linodesData, + linodesError, + linodesLoading, + handleSelectUDFs, + selectedUDFs, + updateStackScript, + availableStackScriptImages, + availableUserDefinedFields, + selectedStackScriptID, + selectedDiskSize, + selectedStackScriptUsername, + selectedStackScriptLabel, + selectedLinodeID, + ...rest + } = this.props; + + return ( + + ); + } + }, + { + title: 'Backups', type: 'fromBackup', render: () => { return ; } }, { - title: 'Clone from Existing Linode', + title: 'Clone Linode', type: 'fromLinode', render: () => { /** diff --git a/src/features/linodes/LinodesCreate/Panel.tsx b/src/features/linodes/LinodesCreate/Panel.tsx new file mode 100644 index 00000000000..7c85cece145 --- /dev/null +++ b/src/features/linodes/LinodesCreate/Panel.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; + +import { + StyleRulesCallback, + withStyles, + WithStyles +} from 'src/components/core/styles'; + +import Paper from 'src/components/core/Paper'; +import Typography from 'src/components/core/Typography'; +import Notice from 'src/components/Notice'; + +type ClassNames = 'root' | 'flatImagePanel'; + +const styles: StyleRulesCallback = theme => ({ + flatImagePanel: { + padding: theme.spacing.unit * 3 + }, + root: {} +}); + +interface Props { + children: React.ReactElement; + error?: string; + title?: string; +} + +type CombinedProps = Props & WithStyles; + +const Panel: React.StatelessComponent = (props) => { + const { classes, children, error, title } = props; + return ( + + {error && } + + {title || 'Select an Image'} + + {children} + + ) +} + +const styled = withStyles(styles); + +export default styled(Panel); \ No newline at end of file diff --git a/src/features/linodes/LinodesCreate/PrivateImages.tsx b/src/features/linodes/LinodesCreate/PrivateImages.tsx new file mode 100644 index 00000000000..1ba1de7edf4 --- /dev/null +++ b/src/features/linodes/LinodesCreate/PrivateImages.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; + +import { + StyleRulesCallback, + withStyles, + WithStyles +} from 'src/components/core/styles'; +import Grid from 'src/components/Grid'; +import SelectionCard from 'src/components/SelectionCard'; + +type ClassNames = 'root' | 'flatImagePanelSelections'; + +const styles: StyleRulesCallback = theme => ({ + flatImagePanelSelections: { + marginTop: theme.spacing.unit * 2, + padding: `${theme.spacing.unit}px 0` + }, + root: {} +}); +interface Props { + images: Linode.Image[]; + disabled?: boolean; + selectedImageID?: string; + handleSelection: (id: string) => void; +} + +type CombinedProps = Props & WithStyles; + +const PrivateImages: React.StatelessComponent = (props) => { + const { classes, disabled, handleSelection, images, selectedImageID } = props; + return ( + + {images && + images.map((image: Linode.Image, idx: number) => ( + handleSelection(image.id)} + renderIcon={() => } + heading={image.label as string} + subheadings={[image.description as string]} + disabled={disabled} + /> + ))} + + ) +} + +const styled = withStyles(styles); + +export default styled(PrivateImages); \ No newline at end of file diff --git a/src/features/linodes/LinodesCreate/PublicImages.tsx b/src/features/linodes/LinodesCreate/PublicImages.tsx new file mode 100644 index 00000000000..a6d4335b22c --- /dev/null +++ b/src/features/linodes/LinodesCreate/PublicImages.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; + +import { + StyleRulesCallback, + withStyles, + WithStyles +} from 'src/components/core/styles'; +import Grid from 'src/components/Grid'; +import SelectionCard from 'src/components/SelectionCard'; +import ShowMoreExpansion from 'src/components/ShowMoreExpansion'; + +type ClassNames = 'root' | 'flatImagePanelSelections'; + +const styles: StyleRulesCallback = theme => ({ + flatImagePanelSelections: { + marginTop: theme.spacing.unit * 2, + padding: `${theme.spacing.unit}px 0` + }, + root: {} +}); +interface Props { + images: Linode.Image[]; + oldImages: Linode.Image[]; + selectedImageID?: string; + disabled?: boolean; + handleSelection: (id: string) => void; +} + +type CombinedProps = Props & WithStyles; + +const distroIcons = { + Arch: 'archlinux', + CentOS: 'centos', + CoreOS: 'coreos', + Debian: 'debian', + Fedora: 'fedora', + Gentoo: 'gentoo', + openSUSE: 'opensuse', + Slackware: 'slackware', + Ubuntu: 'ubuntu' +}; + +const PublicImages: React.StatelessComponent = props => { + const { + classes, + disabled, + images, + handleSelection, + oldImages, + selectedImageID + } = props; + const renderImages = (images: Linode.Image[]) => + images.length && + images.map((image: Linode.Image, idx: number) => ( + handleSelection(image.id)} + renderIcon={() => { + return ( + + ); + }} + heading={image.vendor as string} + subheadings={[image.label]} + data-qa-selection-card + disabled={disabled} + /> + )); + + return ( + <> + + {renderImages(images)} + + {oldImages.length > 0 && ( + + + {renderImages(oldImages)} + + + )} + + ); +}; + +const styled = withStyles(styles); + +export default styled(PublicImages); diff --git a/src/features/linodes/LinodesCreate/SelectImagePanel.tsx b/src/features/linodes/LinodesCreate/SelectImagePanel.tsx index dd09c768d88..7f1acb84cc4 100644 --- a/src/features/linodes/LinodesCreate/SelectImagePanel.tsx +++ b/src/features/linodes/LinodesCreate/SelectImagePanel.tsx @@ -15,44 +15,12 @@ import { values } from 'ramda'; import * as React from 'react'; -import Paper from 'src/components/core/Paper'; -import { - StyleRulesCallback, - withStyles, - WithStyles -} from 'src/components/core/styles'; -import Typography from 'src/components/core/Typography'; -import Grid from 'src/components/Grid'; -import Notice from 'src/components/Notice'; import RenderGuard from 'src/components/RenderGuard'; -import SelectionCard from 'src/components/SelectionCard'; -import ShowMoreExpansion from 'src/components/ShowMoreExpansion'; import TabbedPanel from 'src/components/TabbedPanel'; -type ClassNames = 'root' | 'flatImagePanel' | 'flatImagePanelSelections'; - -const styles: StyleRulesCallback = theme => ({ - flatImagePanel: { - padding: theme.spacing.unit * 3 - }, - flatImagePanelSelections: { - marginTop: theme.spacing.unit * 2, - padding: `${theme.spacing.unit}px 0` - }, - root: {} -}); - -const distroIcons = { - Arch: 'archlinux', - CentOS: 'centos', - CoreOS: 'coreos', - Debian: 'debian', - Fedora: 'fedora', - Gentoo: 'gentoo', - openSUSE: 'opensuse', - Slackware: 'slackware', - Ubuntu: 'ubuntu' -}; +import Panel from './Panel'; +import PrivateImages from './PrivateImages'; +import PublicImages from './PublicImages'; interface Props { images: Linode.Image[]; @@ -60,7 +28,7 @@ interface Props { error?: string; selectedImageID?: string; handleSelection: (id: string) => void; - hideMyImages?: boolean; + variant?: 'public' | 'private' | 'all'; initTab?: number; disabled?: boolean; } @@ -105,130 +73,57 @@ export const getMyImages = compose( filter(propSatisfies(startsWith('private'), 'id')) ); -type CombinedProps = Props & WithStyles; +type CombinedProps = Props; const CreateFromImage: React.StatelessComponent = props => { - const { images, error, handleSelection, disabled } = props; + const { images, error, handleSelection, disabled, title, variant, selectedImageID } = props; const publicImages = getPublicImages(images); const olderPublicImages = getOlderPublicImages(images); const myImages = getMyImages(images); - const renderPublicImages = () => - publicImages.length && - publicImages.map((image: Linode.Image, idx: number) => ( - handleSelection(image.id)} - renderIcon={() => { - return ( - - ); - }} - heading={image.vendor as string} - subheadings={[image.label]} - data-qa-selection-card - disabled={disabled} - /> - )); + const Public = ( + + + + ) - const renderOlderPublicImages = () => - olderPublicImages.length && - olderPublicImages.map((image: Linode.Image, idx: number) => ( - handleSelection(image.id)} - renderIcon={() => { - return ( - - ); - }} - heading={image.vendor as string} - subheadings={[image.label]} - disabled={disabled} - /> - )); + const Private = ( + + + + ) const tabs = [ { title: 'Public Images', render: () => ( - - - {renderPublicImages()} - - - - {renderOlderPublicImages()} - - - + Public ) }, { title: 'My Images', render: () => ( - - {myImages && - myImages.map((image: Linode.Image, idx: number) => ( - handleSelection(image.id)} - renderIcon={() => } - heading={image.label as string} - subheadings={[image.description as string]} - disabled={disabled} - /> - ))} - + Private ) } ]; - const renderTabs = () => { - const { hideMyImages } = props; - if (hideMyImages) { - return tabs; - } - return tabs; - }; - - return ( - - {props.hideMyImages !== true ? ( // if we have no olderPublicImage, hide the dropdown + switch (variant) { + case 'private': + return Private; + case 'public': + return Public; + case 'all': + default: + return ( - ) : ( - - {error && } - - {props.title || 'Select an Image'} - - - {renderPublicImages()} - - {olderPublicImages.length > 0 && ( - - - {renderOlderPublicImages()} - - - )} - - )} - - ); + ) + } }; -const styled = withStyles(styles); - -export default styled(RenderGuard(CreateFromImage)); +export default (RenderGuard(CreateFromImage)); diff --git a/src/features/linodes/LinodesCreate/TabbedContent/FromImageContent.tsx b/src/features/linodes/LinodesCreate/TabbedContent/FromImageContent.tsx index a06b1ce0180..cb328d13e53 100644 --- a/src/features/linodes/LinodesCreate/TabbedContent/FromImageContent.tsx +++ b/src/features/linodes/LinodesCreate/TabbedContent/FromImageContent.tsx @@ -1,5 +1,6 @@ import { pathOr } from 'ramda'; import * as React from 'react'; +import { Link } from 'react-router-dom'; import { Sticky, StickyProps } from 'react-sticky'; import { compose } from 'recompose'; import AccessPanel from 'src/components/AccessPanel'; @@ -13,6 +14,7 @@ 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 SelectRegionPanel from 'src/components/SelectRegionPanel'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; import AddonsPanel from '../AddonsPanel'; @@ -41,7 +43,7 @@ interface Notice { interface Props extends BaseFormStateAndHandlers { notice?: Notice; - publicOnly?: boolean; + variant?: 'public' | 'private' | 'all'; imagePanelTitle?: string; } @@ -97,17 +99,35 @@ export class FromImageContent extends React.PureComponent { regionDisplayInfo, typeDisplayInfo, backupsMonthlyPrice, - publicOnly, userSSHKeys, userCannotCreateLinode, errors, - imagePanelTitle + imagePanelTitle, + variant } = this.props; const hasErrorFor = getAPIErrorsFor(errorResources, errors); const generalError = hasErrorFor('none'); const hasBackups = this.props.backupsEnabled || accountBackupsEnabled; + const privateImages = images.filter(image => !image.is_public); + + if (variant === 'private' && privateImages.length === 0) { + return ( + + + You don't have any private Images. Visit the{' '} + Images section to create an Image from + one of your Linode's disks. + + } + /> + + ); + } return ( @@ -122,7 +142,7 @@ export class FromImageContent extends React.PureComponent { {generalError && } { updateFor={[selectedImageID, compatibleImages, errors]} selectedImageID={selectedImageID} error={hasErrorFor('image')} - hideMyImages={true} + variant="public" /> ) : ( diff --git a/src/store/linodeCreate/linodeCreate.reducer.ts b/src/store/linodeCreate/linodeCreate.reducer.ts index ebc2ab45e8c..a7bb1990a55 100644 --- a/src/store/linodeCreate/linodeCreate.reducer.ts +++ b/src/store/linodeCreate/linodeCreate.reducer.ts @@ -54,8 +54,6 @@ const reducer: Reducer = reducerWithInitialState( ).caseWithAction(handleChangeCreateType, (state, action) => { const { payload } = action; - console.log(payload); - return { ...state, type: payload