From f20f58b9c0f72499abc0dc04e023fb99469a71a9 Mon Sep 17 00:00:00 2001 From: Neha Date: Tue, 9 Jul 2019 14:32:59 +0900 Subject: [PATCH] OCS install --- .../components/ocs-install/ocs-install.scss | 18 + .../components/ocs-install/ocs-install.tsx | 443 ++++++++++++++++++ .../ceph-storage-plugin/src/models.ts | 13 + .../ceph-storage-plugin/src/plugin.ts | 38 +- frontend/packages/console-app/package.json | 3 +- .../public/components/factory/list-page.jsx | 1 + frontend/public/components/factory/table.tsx | 11 +- 7 files changed, 502 insertions(+), 25 deletions(-) create mode 100644 frontend/packages/ceph-storage-plugin/src/components/ocs-install/ocs-install.scss create mode 100644 frontend/packages/ceph-storage-plugin/src/components/ocs-install/ocs-install.tsx diff --git a/frontend/packages/ceph-storage-plugin/src/components/ocs-install/ocs-install.scss b/frontend/packages/ceph-storage-plugin/src/components/ocs-install/ocs-install.scss new file mode 100644 index 00000000000..c91f35cc3fe --- /dev/null +++ b/frontend/packages/ceph-storage-plugin/src/components/ocs-install/ocs-install.scss @@ -0,0 +1,18 @@ +.ceph-ocs-info__alert { + box-shadow: var(--pf-global--BoxShadow--sm); + height: 2.6em; + .ceph-ocs-desc__legend { + font-weight: 450; + } + .pf-c-alert__icon { + height: 1.6em; + padding: 0.3em; + } + .pf-c-alert__title { + font-size: 0.8rem; + padding: 1.1em; + } +} +.ceph-ocs-install__form { + max-width: 66%; +} diff --git a/frontend/packages/ceph-storage-plugin/src/components/ocs-install/ocs-install.tsx b/frontend/packages/ceph-storage-plugin/src/components/ocs-install/ocs-install.tsx new file mode 100644 index 00000000000..7207744c9a2 --- /dev/null +++ b/frontend/packages/ceph-storage-plugin/src/components/ocs-install/ocs-install.tsx @@ -0,0 +1,443 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import * as classNames from 'classnames'; +import { match } from 'react-router'; +import { safeDump } from 'js-yaml'; +import { sortable } from '@patternfly/react-table'; +import { Alert } from '@patternfly/react-core'; + +import { ButtonBar } from '@console/internal/components/utils/button-bar'; +import { history } from '@console/internal/components/utils/router'; +import { RadioInput } from '@console/internal/components/radio'; +import { + referenceForModel, + getNodeRoles, + K8sResourceKind, + K8sKind, + k8sGet, + k8sCreate, + K8sResourceKindReference, + Status, + k8sList, +} from '@console/internal/module/k8s'; +import { + ResourceLink, + BreadCrumbs, + humanizeBinaryBytes, +} from '@console/internal/components/utils/index'; +import { Table, TableRow, TableData, ListPage } from '@console/internal/components/factory'; +import { ConfigMapModel, NodeModel, ClusterServiceVersionModel } from '@console/internal/models'; +import { ClusterServiceVersionKind } from '@console/internal/components/operator-lifecycle-manager/index'; +import { CreateYAML } from '@console/internal/components/create-yaml'; +import { OCSServiceModel } from '../../models'; + +import './ocs-install.scss'; + +const tableColumnClasses = [ + classNames('col-md-1', 'col-sm-1', 'col-xs-1'), + classNames('col-md-3', 'col-sm-4', 'col-xs-7'), + classNames('col-md-1', 'col-sm-3', 'hidden-xs'), + classNames('col-md-1', 'hidden-sm', 'hidden-xs'), + classNames('col-md-2', 'hidden-sm', 'hidden-xs'), + classNames('col-md-2', 'hidden-sm', 'hidden-xs'), + classNames('col-md-2', 'col-sm-4', 'col-xs-4'), +]; + +const NodeTableHeader = () => { + return [ + { + title: 'Name', + sortField: 'metadata.name', + transforms: [sortable], + props: { className: tableColumnClasses[1] }, + }, + { + title: 'Role', + props: { className: tableColumnClasses[2] }, + }, + { + title: 'CPU', + props: { className: tableColumnClasses[3] }, + }, + { + title: 'Memory', + props: { className: tableColumnClasses[4] }, + }, + { + title: 'Capacity', + props: { className: tableColumnClasses[5] }, + }, + { + title: 'Devices', + props: { className: tableColumnClasses[6] }, + }, + ]; +}; +NodeTableHeader.displayName = 'NodeTableHeader'; +const configMapsForAllNodes = {}; + +const getConfigMaps = () => { + k8sList(ConfigMapModel, { + ns: 'openshift-storage', + }).then((configMaps) => { + configMaps.forEach((config) => { + const nodeName = config.metadata.labels && config.metadata.labels['rook.io/node']; + + if (typeof nodeName !== 'undefined') { + try { + configMapsForAllNodes[nodeName] = JSON.parse(config.data.devices).length; + } catch (e) { + // ignore + // eslint-disable-next-line no-console + console.warn('Invalid JSON'); + return; + } + } + }); + }); +}; + +const getConvertedUnits = (value, initialUnit, preferredUnit) => { + return ( + humanizeBinaryBytes(_.slice(value, 0, value.length - 2).join(''), initialUnit, preferredUnit) + .string || '-' + ); +}; + +const NodeTableRow: React.FC = ({ + obj: node, + index, + key, + style, + customData, + onSelect, +}) => { + const roles = getNodeRoles(node).sort(); + const deviceCount = configMapsForAllNodes[node.metadata.name] || 0; + const isChecked = customData.length > index ? customData[index].selected : false; + + return ( + + + { + onSelect(e, e.target.checked, index); + }} + /> + + + + + {roles.join(', ') || '-'} + + {_.get(node.status, 'capacity.cpu') || '-'} CPU + + + {getConvertedUnits(_.get(node.status, 'allocatable.memory'), 'KiB', 'GiB')} + + + {getConvertedUnits(_.get(node.status, 'capacity.memory'), 'KiB', 'GiB')} + + {deviceCount} Selected + + ); +}; + +export const NodeList: React.FC = (props) => { + const [selectedNodeData, setSelectedNodeData] = React.useState(props.customData); + + const onSelect = (event, isSelected, virtualRowIndex, data) => { + event.preventDefault(); + let newSelectedRowData = _.cloneDeep(selectedNodeData); + + // clone `data` in case previous Firehose updates added elements, preserve existing selection state + _.each(data, (row, index: number) => { + if (index < newSelectedRowData.length) { + // preserve existing selection state + newSelectedRowData[index] = { + ...row, + uid: row.metadata.uid, + selected: newSelectedRowData[index].selected, + }; + } else { + // set initial selection state from storage here if necessary...for now, initialize it false + newSelectedRowData.push({ ...row, uid: row.metadata.uid, selected: false }); + } + }); + + if (virtualRowIndex !== -1) { + // set the selection based on virtualRowIndex node, it should exist in the array now + newSelectedRowData[virtualRowIndex].selected = isSelected; + } else { + // selectAll + newSelectedRowData = _.map(selectedNodeData, (row) => ({ ...row, selected: isSelected })); + } + setSelectedNodeData(newSelectedRowData); + props.onSelect(newSelectedRowData); + }; + + const onSingleSelect = (event, isSelected, virtualRowIndex) => { + event.preventDefault(); + onSelect(event, isSelected, virtualRowIndex, props.data); + }; + + const onSelectAll = (event, isSelected, virtualRowIndex) => { + event.preventDefault(); + onSelect(event, isSelected, virtualRowIndex, props.data); + }; + + return ( + } // this is the correct select callback for single row + aria-label="Nodes" + virtualize + onSelect={onSelectAll} // this is the selectAll callback for virtualized tables + /> + ); +}; + +export const CreateOCSServiceForm: React.FC = React.memo((props) => { + const title = 'Create New OCS Service'; + const [error, setError] = React.useState(''); + const [inProgress, setProgress] = React.useState(false); + const [ipiInstallationMode, setIpiInstallationMode] = React.useState(true); + + // must initialize like this w/ at least one item, current bug in pf-react row.every for selectAll + // setting a dummy value for now + const initialSelectionState = [{ selected: false }]; + const [selectedNodeData, setSelectedNodeData] = React.useState(initialSelectionState); + + const onSelect = (selectedRow) => { + // `newSelectedRowData` can be persisted somewhere after it is passed back up... + setSelectedNodeData(selectedRow); + }; + + React.useEffect(() => { + if (!ipiInstallationMode) { + getConfigMaps(); + } + }, [ipiInstallationMode]); + + const updateMode = () => { + const mode = !ipiInstallationMode; + setIpiInstallationMode(mode); + }; + + const submit = (event: React.FormEvent) => { + event.preventDefault(); + event.stopPropagation(); + + setProgress(true); + setError(''); + + k8sCreate(OCSServiceModel, props.sample, { ns: 'openshift-storage' }) + .then(() => { + history.push( + `/k8s/ns/${props.namespace}/clusterserviceversions/${ + props.clusterServiceVersion.metadata.name + }/${referenceForModel(OCSServiceModel)}/${props.sample.metadata.name}`, + ); + setProgress(false); + setError(''); + }) + .catch((err: Status) => setError(err.message)); + }; + return ( +
+

+
{title}
+

+

+ OCS runs as a cloud-native service for optimal integration with applications in need of + storage, and handles the scenes such as provisioning and management. +

+
+
+ Deployment Type +
+
+ +
+
+
+
+ +
+
+ +
+
+ {!ipiInstallationMode && ( +
+ +

+ Select at least 3 nodes you wish to use. +

+
+ )} + {!ipiInstallationMode && ( + ( + + )} + /> + )} +
+ + + + + +
+ ); +}); + +export const CreateOCSServiceYAML: React.FC = (props) => { + const template = _.attempt(() => safeDump(props.sample)); + if (_.isError(template)) { + // eslint-disable-next-line no-console + console.error('Error parsing example JSON from annotation. Falling back to default.'); + } + + return ( + + ); +}; + +/** + * Component which wraps the YAML editor and form together + */ +export const CreateOCSService: React.FC = (props) => { + const [sample, setSample] = React.useState(null); + const [method, setMethod] = React.useState<'yaml' | 'form'>('form'); + const [clusterServiceVersion, setClusterServiceVersion] = React.useState(null); + + React.useEffect(() => { + k8sGet(ClusterServiceVersionModel, props.match.params.appName, props.match.params.ns).then( + (clusterServiceVersionObj) => { + try { + setSample( + JSON.parse(_.get(clusterServiceVersionObj.metadata.annotations, 'alm-examples'))[0], + ); + setClusterServiceVersion(clusterServiceVersionObj); + } catch (e) { + setClusterServiceVersion(null); + return; + } + }, + ); + }, [props.match.params.appName, props.match.params.ns]); + + const changeToYAMLMethod = (event) => { + event.preventDefault(); + setMethod('yaml'); + }; + return ( + +
+
+ {clusterServiceVersion !== null && ( + + )} +
+
+ {(method === 'form' && ( + + )) || + (method === 'yaml' && )} +
+ ); +}; + +type CreateOCSServiceProps = { + match: match<{ appName: string; ns: string; plural: K8sResourceKindReference }>; + operandModel: K8sKind; + sample?: K8sResourceKind; + namespace: string; + loadError?: any; + clusterServiceVersion: ClusterServiceVersionKind; +}; + +type CreateOCSServiceFormProps = { + operandModel: K8sKind; + sample?: K8sResourceKind; + namespace: string; + clusterServiceVersion: ClusterServiceVersionKind; + changeToYAMLMethod: (event: React.MouseEvent) => void; +}; + +type CreateOCSServiceYAMLProps = { + sample?: K8sResourceKind; + match: match<{ appName: string; ns: string; plural: K8sResourceKindReference }>; +}; + +type NodeTableRowProps = { + obj: K8sResourceKind; + index: number; + key?: string; + style: object; + customData?: any; + onSelect?: Function; +}; + +type NodeListProps = { + customData?: any; + onSelect?: Function; + data: Record[]; +}; diff --git a/frontend/packages/ceph-storage-plugin/src/models.ts b/frontend/packages/ceph-storage-plugin/src/models.ts index c6159d33c5e..34fac5611be 100644 --- a/frontend/packages/ceph-storage-plugin/src/models.ts +++ b/frontend/packages/ceph-storage-plugin/src/models.ts @@ -12,3 +12,16 @@ export const CephClusterModel: K8sKind = { id: 'cephcluster', crd: true, }; + +export const OCSServiceModel: K8sKind = { + label: 'OCS Cluster Service', + labelPlural: 'OCS Cluster Services', + apiVersion: 'v1alpha1', + apiGroup: 'ocs.openshift.io', + plural: 'ocsclusters', + abbr: 'OCS', + namespaced: true, + kind: 'StorageCluster', + id: 'ipiocscluster', + crd: true, +}; diff --git a/frontend/packages/ceph-storage-plugin/src/plugin.ts b/frontend/packages/ceph-storage-plugin/src/plugin.ts index 5c72dc1242f..5ebb1ca41dc 100644 --- a/frontend/packages/ceph-storage-plugin/src/plugin.ts +++ b/frontend/packages/ceph-storage-plugin/src/plugin.ts @@ -6,14 +6,25 @@ import { ModelFeatureFlag, ModelDefinition, Plugin, + RoutePage, } from '@console/plugin-sdk'; import { GridPosition } from '@console/internal/components/dashboard'; +import { ClusterServiceVersionModel } from '@console/internal/models'; +import { referenceForModel } from '@console/internal/module/k8s'; import * as models from './models'; -type ConsumedExtensions = ModelFeatureFlag | ModelDefinition | DashboardsTab | DashboardsCard; +type ConsumedExtensions = + | ModelFeatureFlag + | ModelDefinition + | DashboardsTab + | DashboardsCard + | RoutePage; const CEPH_FLAG = 'CEPH'; +// keeping this for testing, will be removed once ocs operator available +const apiObjectRef = 'core.libopenstorage.org~v1alpha1~StorageCluster'; +// const apiObjectRef = referenceForModel(models.OCSServiceModel); const plugin: Plugin = [ { @@ -43,30 +54,19 @@ const plugin: Plugin = [ position: GridPosition.MAIN, loader: () => import( - './components/dashboard-page/storage-dashboard/health-card' /* webpackChunkName: "ceph-storage-health-card" */ - ).then((m) => m.default), - }, - }, - { - type: 'Dashboards/Card', - properties: { - tab: 'persistent-storage', - position: GridPosition.LEFT, - loader: () => - import( - './components/dashboard-page/storage-dashboard/details-card' /* webpackChunkName: "ceph-storage-details-card" */ - ).then((m) => m.default), + './components/dashboard-page/storage-dashboard/data-resiliency/data-resiliency' /* webpackChunkName: "ceph-data-resiliency-card" */ + ).then((m) => m.DataResiliencyWithResources), }, }, { - type: 'Dashboards/Card', + type: 'Page/Route', properties: { - tab: 'persistent-storage', - position: GridPosition.MAIN, + exact: true, + path: `/k8s/ns/:ns/${ClusterServiceVersionModel.plural}/:appName/${apiObjectRef}/~new`, loader: () => import( - './components/dashboard-page/storage-dashboard/data-resiliency/data-resiliency' /* webpackChunkName: "ceph-storage-data-resiliency-card" */ - ).then((m) => m.DataResiliencyWithResources), + './components/ocs-install/ocs-install' /* webpackChunkName: "ceph-ocs-service" */ + ).then((m) => m.CreateOCSService), }, }, { diff --git a/frontend/packages/console-app/package.json b/frontend/packages/console-app/package.json index e95c106f3b5..b1848f9e6e1 100644 --- a/frontend/packages/console-app/package.json +++ b/frontend/packages/console-app/package.json @@ -13,7 +13,8 @@ "@console/knative-plugin": "0.0.0-fixed", "@console/metal3-plugin": "0.0.0-fixed", "@console/plugin-sdk": "0.0.0-fixed", - "@console/shared": "0.0.0-fixed" + "@console/shared": "0.0.0-fixed", + "@console/ceph-storage-plugin": "0.0.0-fixed" }, "consolePlugin": { "entry": "src/plugin.tsx" diff --git a/frontend/public/components/factory/list-page.jsx b/frontend/public/components/factory/list-page.jsx index aa3220e9f95..f1319baed79 100644 --- a/frontend/public/components/factory/list-page.jsx +++ b/frontend/public/components/factory/list-page.jsx @@ -301,6 +301,7 @@ FireMan_.propTypes = { /** @type {React.SFC<{ListComponent: React.ComponentType, kind: string, helpText?: any, namespace?: string, filterLabel?: string, textFilter?: string, title?: string, showTitle?: boolean, rowFilters?: any[], selector?: any, fieldSelector?: string, canCreate?: boolean, createButtonText?: string, createProps?: any, mock?: boolean}>} */ export const ListPage = withFallback(props => { + console.log(props, "list component"); const { autoFocus, canCreate, diff --git a/frontend/public/components/factory/table.tsx b/frontend/public/components/factory/table.tsx index 757f8983433..14974c881e5 100644 --- a/frontend/public/components/factory/table.tsx +++ b/frontend/public/components/factory/table.tsx @@ -220,7 +220,7 @@ const VirtualBody: React.SFC = (props) => { ); }; -export type RowFunctionArgs = {obj: object, index: number, columns: [], isScrolling: boolean, key: string, style: object, customData?: object}; +export type RowFunctionArgs = {obj: object, index: number, columns: [], isScrolling: boolean, key: string, style: object, customData?: any}; export type RowFunction = (args: RowFunctionArgs) => JSX.Element; export type VirtualBodyProps = { @@ -239,7 +239,7 @@ export type VirtualBodyProps = { } type TableOwnProps = { - customData?: object; + customData?: any; data?: any[]; defaultSortFunc?: string; defaultSortField?: string; @@ -254,6 +254,7 @@ type TableOwnProps = { loaded?: boolean; reduxID?: string; reduxIDs?: string[]; + onSelect?: Function; } type TablePropsFromState = {}; @@ -272,7 +273,7 @@ export const Table = connect { static propTypes = { - customData: PropTypes.object, + customData: PropTypes.any, data: PropTypes.array, EmptyMsg: PropTypes.func, expand: PropTypes.bool, @@ -406,7 +407,7 @@ export const Table = connect