diff --git a/src/app/Pages/ImageDetails.tsx b/src/app/Pages/ImageDetails.tsx
index 4edc03f..997e44f 100644
--- a/src/app/Pages/ImageDetails.tsx
+++ b/src/app/Pages/ImageDetails.tsx
@@ -1,16 +1,16 @@
import * as React from 'react'
import {
- PageSection, Bullseye, Text, TextContent, TextVariants,
+ PageSection, Bullseye, Text, TextContent, TextVariants,
Card, CardTitle, CardBody, CardFooter
} from '@patternfly/react-core'
import { useDocumentTitle } from '@app/utils/useDocumentTitle'
import Footer from '@app/components/Footer'
-import DetailsView from '@app/components/DetailsView'
+import { DetailsView } from '@app/components/DetailsView'
import { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import fetch from 'cross-fetch'
-const ImageDetails: React.FunctionComponent<{title: string}> = ({title}) => {
+const ImageDetails: React.FunctionComponent<{ title: string }> = ({ title }) => {
const [details, setDetails] = useState({})
const { provider, region, imageName } = useParams()
useDocumentTitle(title)
@@ -19,24 +19,24 @@ const ImageDetails: React.FunctionComponent<{title: string}> = ({title}) => {
fetch(`https://imagedirectory.cloud/images/v1/${provider}/${region}/${imageName}`, {
method: 'get',
})
- .then(res => res.json())
- .then(details => setDetails(details))
+ .then(res => res.json())
+ .then(details => setDetails(details))
}, [provider, region, imageName])
return (
-
-
-
- {details['imageId'] ? : Loading....
}
-
-
+
+
+
+ {details['imageId'] ? : Loading....
}
+
+
diff --git a/src/app/components/DetailsDrawer.tsx b/src/app/components/DetailsDrawer.tsx
new file mode 100644
index 0000000..aa052ca
--- /dev/null
+++ b/src/app/components/DetailsDrawer.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import {
+ DrawerPanelContent,
+ DrawerHead,
+ DrawerActions,
+ DrawerCloseButton,
+ Title,
+ Flex,
+ FlexItem,
+ Stack,
+ StackItem
+} from '@patternfly/react-core';
+import { MouseEventHandler } from 'react';
+import { DetailsView } from './DetailsView';
+
+export const DetailsDrawer: React.FunctionComponent<{
+ onCloseClick: MouseEventHandler,
+ isExpanded: Boolean,
+ drawerRef: any,
+ details: object,
+}> = ({ onCloseClick, isExpanded, drawerRef, details }) => {
+ return (
+
+
+
+
+
+
+
+
+ {details['name']}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/app/components/DetailsView.tsx b/src/app/components/DetailsView.tsx
index 22ca644..281d05f 100644
--- a/src/app/components/DetailsView.tsx
+++ b/src/app/components/DetailsView.tsx
@@ -5,192 +5,219 @@ import {
CodeBlock,
CodeBlockAction,
CodeBlockCode,
+ DescriptionList,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ DescriptionListDescription,
TextContent,
- TextList,
- TextListItem,
+ Tabs,
+ Tab,
+ TabTitleText,
+ Title,
+ Stack,
+ StackItem
} from '@patternfly/react-core';
import { PlayIcon } from '@patternfly/react-icons';
-interface IImageModalProps {
- details: object;
-}
+export const DetailsView: React.FunctionComponent<{ details: object }> = ({ details }) => {
+ const [activeTabKey, setActiveTabKey] = React.useState
(0);
+ const [copied, setCopied] = React.useState(false);
-interface IImageModalState {
- copied: boolean;
-}
+ const handleTabClick = (
+ event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
+ tabIndex: string | number
+ ) => {
+ setActiveTabKey(tabIndex);
+ };
-export default class DetailsView extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- copied: false,
- };
+ const copyButton = (data: string) => {
+ return (
+
+ {
+ navigator.clipboard.writeText(data);
+ setCopied(true)
+ }}
+ exitDelay={600}
+ maxWidth="110px"
+ variant="plain"
+ >
+ {copied ? 'Successfully copied to clipboard!' : 'Copy to clipboard'}
+
+
+ )
}
- render() {
- const { copied } = this.state;
-
- const { details } = this.props;
-
- let cliCommand;
- let shellUrl;
- let displayItems = [
- {
- Title: 'Name',
- Data: details['name'],
- },
- {
- Title: 'Release Date',
- Data: details['date'],
- },
- {
- Title: 'Version',
- Data: details['version'],
- },
- {
- Title: 'Architecture',
- Data: details['arch'],
- },
- ];
- // Conditionally configure the content of the image details view
- switch (details['provider']) {
- case 'aws':
- const instanceType: string = details['arch'] === 'x86_64' ? 't3.medium' : 't4g.medium';
- cliCommand = `aws ec2 run-instances \\
+ let cliCommand;
+ let shellUrl;
+ let displayItems = [
+ {
+ Title: 'Cloud Provider',
+ Data: details['provider'],
+ },
+ {
+ Title: 'RHEL Version',
+ Data: details['version'],
+ },
+ {
+ Title: 'Release Date',
+ Data: new Date(details['date']).toDateString(),
+ },
+ {
+ Title: 'Architecture',
+ Data: details['arch'],
+ },
+ ];
+ // Conditionally configure the content of the image details view
+ switch (details['provider']) {
+ case 'aws':
+ const instanceType: string = details['arch'] === 'x86_64' ? 't3.medium' : 't4g.medium';
+ cliCommand = `aws ec2 run-instances \\
--image-id ${details['imageId']} \\
--count 1 \\
--instance-type ${instanceType} \\
--key-name \\
--security-group-ids `;
- shellUrl = 'https://console.aws.amazon.com/cloudshell/home';
- displayItems.push(
- {
- Title: 'Image ID',
- Data: details['imageId'],
- },
- {
- Title: 'Region',
- Data: details['region'],
- }
- );
- break;
+ shellUrl = 'https://console.aws.amazon.com/cloudshell/home';
+ displayItems.push(
+ {
+ Title: 'Region',
+ Data: details['region'],
+ },
+ {
+ Title: 'Image ID',
+ Data: {details['imageId']} {copyButton(details['imageId'])},
+ }
+ );
+ break;
- case 'azure':
- const urnParts = details['imageId'].split(':');
- const offer = urnParts[1];
- const sku = urnParts[2];
- cliCommand = `az vm create -n -g --image ${details['imageId']}`;
- shellUrl = 'https://portal.azure.com/';
- displayItems.push(
- {
- Title: 'Urn',
- Data: details['imageId'],
- },
- {
- Title: 'Offer',
- Data: offer,
- },
- {
- Title: 'Sku',
- Data: sku,
- }
- );
- break;
+ case 'azure':
+ const urnParts = details['imageId'].split(':');
+ const offer = urnParts[1];
+ const sku = urnParts[2];
+ cliCommand = `az vm create -n -g --image ${details['imageId']}`;
+ shellUrl = 'https://portal.azure.com/';
+ displayItems.push(
+ {
+ Title: 'Urn',
+ Data: {details['imageId']} {copyButton(details['imageId'])},
+ },
+ {
+ Title: 'Offer',
+ Data: offer,
+ },
+ {
+ Title: 'Sku',
+ Data: sku,
+ }
+ );
+ break;
- case 'google':
- const image_path = details['selflink'].split('projects')[1];
- cliCommand = `gcloud beta compute instances create \\
+ case 'google':
+ cliCommand = `gcloud beta compute instances create \\
--machine-typei=e2-medium \\
--subnet=default \\
- --image="https://www.googleapis.com/compute/v1/projects${image_path}" \\
- --boot-disk-device-name= \\
- --project `;
- shellUrl = 'https://shell.cloud.google.com/?show=terminal';
- displayItems.push({
- Title: 'Image ID',
- Data: details['imageId'],
- });
- break;
+ ---image="${details['imageId']}" \\
+ ---boot-disk-device-name= \\
+ ---project `;
+ shellUrl = 'https://shell.cloud.google.com/?show=terminal';
+ displayItems.push({
+ Title: 'Image ID',
+ Data: {details['imageId']} {copyButton(details['imageId'])},
+ });
+ break;
- default:
- break;
- }
-
- return (
-
-
- Image Details
-
-
-
- {displayItems.map((item, index) => {
- return (
-
-
- {item.Title}
-
-
- {item.Data}
-
-
- );
- })}
-
- {details['provider'] != 'azure' && (
-
- )}
-
+ default:
+ break;
+ }
-
-
-
-
- Optional: Launch from CLI
-
-
-
-
-
-
- {
- navigator.clipboard.writeText(cliCommand);
- }}
- exitDelay={600}
- maxWidth="110px"
- variant="plain"
- >
- {copied ? 'Successfully copied to clipboard!' : 'Copy to clipboard'}
-
-
-
-
- >
- }
- >
-
- {cliCommand}
-
-
-
- );
- }
+ )}
+
+
+
+
+ Launch from CLI} aria-label={`Run RHEL cloud image on ${details['provider']}`}>
+
+
+
+ {'Example CLI Command'}
+
+
+
+
+
+ {copyButton(cliCommand)}
+
+
+
+
+
+
+ >
+ }
+ >
+
+ {cliCommand}
+
+
+
+
+
+
+
+ );
+
}
diff --git a/src/app/components/ImageTable.tsx b/src/app/components/ImageTable.tsx
index ebc84df..edd8e49 100644
--- a/src/app/components/ImageTable.tsx
+++ b/src/app/components/ImageTable.tsx
@@ -1,8 +1,19 @@
import React, { useEffect } from 'react';
-import { SearchInput, Title, Toolbar, ToolbarContent, ToolbarItem, Pagination } from '@patternfly/react-core';
+import {
+ SearchInput,
+ Title,
+ Toolbar,
+ ToolbarContent,
+ ToolbarItem,
+ Pagination,
+ Drawer,
+ DrawerContent,
+ DrawerContentBody,
+ Button
+} from '@patternfly/react-core';
import { Table, Thead, Tr, Th, ThProps, Tbody, Td } from '@patternfly/react-table';
import { fetch } from 'cross-fetch'
-
+import { DetailsDrawer } from './DetailsDrawer';
interface ImageData {
name: string;
version: string;
@@ -22,6 +33,23 @@ export const ImageTable: React.FunctionComponent = () => {
const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc' | undefined>(undefined);
const [page, setPage] = React.useState(1);
const [perPage, setPerPage] = React.useState(20);
+ const [isDrawerExpanded, setIsExpanded] = React.useState(false);
+ const [selectedImage, setSelectedImage] = React.useState({});
+ const drawerRef = React.useRef();
+
+ const onDrawerExpand = () => {
+ drawerRef.current && drawerRef.current.focus();
+ };
+
+ const onDrawerOpenClick = (details: object) => {
+ setIsExpanded(true);
+ setSelectedImage(details)
+ };
+
+ const onDrawerCloseClick = () => {
+ setIsExpanded(false);
+ };
+
const columnNames = {
name: 'Name',
provider: 'Provider',
@@ -45,6 +73,7 @@ export const ImageTable: React.FunctionComponent = () => {
}
const handleSearch = (event) => {
+ setIsExpanded(false);
setSearch(event.target.value);
setPage(1);
};
@@ -67,6 +96,7 @@ export const ImageTable: React.FunctionComponent = () => {
};
const onSetPage = (_event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => {
+ setIsExpanded(false);
setPage(newPage);
};
@@ -105,6 +135,15 @@ export const ImageTable: React.FunctionComponent = () => {
}
});
}
+
+ const panelContent = (
+
+ );
+
return (
Browse Images
@@ -132,33 +171,50 @@ export const ImageTable: React.FunctionComponent = () => {
-
-
-
- {columnNames.name} |
- {columnNames.provider} |
- {columnNames.region} |
- {columnNames.arch} |
- {columnNames.date} |
- |
-
-
-
- {sortedImageData.map((image: ImageData) => (
-
- {image.name} |
- {image.provider} |
- {image.region} |
- {image.arch} |
- {new Date(image.date).toDateString()} |
- {Launch now} |
-
- ))}
-
-
+
+
+
+
+
+
+
+
+ {columnNames.name} |
+ {columnNames.provider} |
+ {columnNames.region} |
+ {columnNames.arch} |
+ {columnNames.date} |
+ |
+
+
+
+ {sortedImageData.map((image: ImageData) => (
+
+ {image.name} |
+ {image.provider} |
+ {image.region} |
+ {image.arch} |
+ {new Date(image.date).toDateString()} |
+
+ onDrawerOpenClick(image)}
+ variant='link'
+ isInline>
+ Launch now
+
+ |
+
+ ))}
+
+
+
+
+
+