diff --git a/frontend/packages/ceph-storage-plugin/src/components/modals/add-capacity-modal/_add-capacity-modal.scss b/frontend/packages/ceph-storage-plugin/src/components/modals/add-capacity-modal/_add-capacity-modal.scss new file mode 100644 index 00000000000..c53c462b975 --- /dev/null +++ b/frontend/packages/ceph-storage-plugin/src/components/modals/add-capacity-modal/_add-capacity-modal.scss @@ -0,0 +1,30 @@ +@import '~@patternfly/patternfly/sass-utilities/colors'; + +.add-capacity-modal--padding { + .co-storage-class-dropdown { + .dropdown { + width: 20.25rem; + button { + width: 100%; + } + } + } + .form-group input { + max-width: 15rem; + margin-right: 5px; + } + .toolTip_dropdown { + button { + position: absolute; + left: 7rem; + top: 8.5rem; + } + } + padding-top: 2em; +} + +.add-capacity-modal__span { + margin-left: 3em; + color: $pf-color-black-500; +} + diff --git a/frontend/packages/ceph-storage-plugin/src/components/modals/add-capacity-modal/add-capacity-modal.tsx b/frontend/packages/ceph-storage-plugin/src/components/modals/add-capacity-modal/add-capacity-modal.tsx new file mode 100644 index 00000000000..8231f3421f2 --- /dev/null +++ b/frontend/packages/ceph-storage-plugin/src/components/modals/add-capacity-modal/add-capacity-modal.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { DashboardCardHelp } from '@console/internal/components/dashboard/dashboard-card/card-help'; +import { RequestSizeInput, withHandlePromise } from '@console/internal/components/utils/index'; +import { + createModalLauncher, + ModalBody, + ModalSubmitFooter, + ModalTitle, +} from '@console/internal/components/factory'; +import { k8sPatch, K8sResourceKind } from '@console/internal/module/k8s'; +import { OCSServiceModel } from '../../../models'; +import './_add-capacity-modal.scss'; +import { OCSStorageClassDropdown } from '../storage-class-dropdown'; + +export const AddCapacityModal = withHandlePromise((props: AddCapacityModalProps) => { + const { ocsConfig, close, cancel } = props; + const dropdownUnits = { + Ti: 'Ti', + }; + const requestSizeUnit = dropdownUnits.Ti; + const [requestSizeValue, setRequestSizeValue] = React.useState(''); + const [storageClass, setStorageClass] = React.useState(''); + const [inProgress, setProgress] = React.useState(false); + const [errorMessage, setError] = React.useState(''); + const storageClassTooltip = + 'The Storage Class will be used to request storage from the underlying infrastructure to create the backing persistent volumes that will be used to provide the OpenShift Container Storage (OCS) service.'; + + const submit = (event: React.FormEvent) => { + event.preventDefault(); + setProgress(true); + const presentCapacity = _.get(ocsConfig, 'spec.storageDeviceSets[0].count'); + const newValue = parseInt(presentCapacity, 10) + parseInt(requestSizeValue, 10) * 3; + const patch = { + op: 'replace', + path: `/spec/storageDeviceSets/0/count`, + value: newValue, + }; + props + .handlePromise(k8sPatch(OCSServiceModel, ocsConfig, [patch])) + .then(() => { + setProgress(false); + close(); + }) + .catch((error) => { + setError(error); + setProgress(false); + throw error; + }); + }; + + const handleRequestSizeInputChange = (capacityObj: any) => { + setRequestSizeValue(capacityObj.value); + }; + + const handleStorageClass = (sc: K8sResourceKind) => { + setStorageClass(sc.metadata.name); + }; + + return ( +
+ Add Capacity + + Increase the capacity of {ocsConfig.metadata.name}. +
+
+ + +
+
+ {storageClassTooltip} +
+ +
+
+ + + ); +}); + +export type AddCapacityModalProps = { + kind?: any; + ocsConfig?: any; + handlePromise: (promise: Promise) => Promise; + cancel?: () => void; + close?: () => void; +}; + +export const addCapacityModal = createModalLauncher(AddCapacityModal); diff --git a/frontend/packages/ceph-storage-plugin/src/components/modals/storage-class-dropdown.tsx b/frontend/packages/ceph-storage-plugin/src/components/modals/storage-class-dropdown.tsx new file mode 100644 index 00000000000..331249cedd3 --- /dev/null +++ b/frontend/packages/ceph-storage-plugin/src/components/modals/storage-class-dropdown.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { Firehose } from '@console/internal/components/utils'; +import { StorageClassDropdownInner } from '@console/internal/components/utils/storage-class-dropdown'; +import { K8sResourceKind } from '@console/internal/module/k8s'; + +const cephStorageProvisioners = ['ceph.rook.io/block', 'cephfs.csi.ceph.com', 'rbd.csi.ceph.com']; + +export const OCSStorageClassDropdown: React.FC = (props) => ( + + + +); + +const StorageClassDropdown = (props: any) => { + const scConfig = _.cloneDeep(props); + /* 'S' of Storage should be Capital as its defined key in resourses object */ + const scLoaded = _.get(scConfig.resources.StorageClass, 'loaded'); + const scData = _.get(scConfig.resources.StorageClass, 'data', []) as K8sResourceKind[]; + + const filteredSCData = scData.filter((sc: K8sResourceKind) => { + return cephStorageProvisioners.every( + (provisioner: string) => !_.get(sc, 'provisioner').includes(provisioner), + ); + }); + + if (scLoaded) { + scConfig.resources.StorageClass.data = filteredSCData; + } + + return ; +}; + +type OCSStorageClassDropdownProps = { + id?: string; + loaded?: boolean; + resources?: any; + name: string; + onChange: (object) => void; + describedBy?: string; + defaultClass: string; + required?: boolean; +}; diff --git a/frontend/packages/ceph-storage-plugin/src/plugin.ts b/frontend/packages/ceph-storage-plugin/src/plugin.ts index 1f9e9904d51..6bd755fe699 100644 --- a/frontend/packages/ceph-storage-plugin/src/plugin.ts +++ b/frontend/packages/ceph-storage-plugin/src/plugin.ts @@ -8,6 +8,7 @@ import { Plugin, DashboardsOverviewQuery, RoutePage, + ClusterServiceVersionAction, } from '@console/plugin-sdk'; import { GridPosition } from '@console/internal/components/dashboard'; import { OverviewQuery } from '@console/internal/components/dashboards-page/overview-dashboard/queries'; @@ -29,7 +30,8 @@ type ConsumedExtensions = | DashboardsCard | DashboardsOverviewHealthPrometheusSubsystem | DashboardsOverviewQuery - | RoutePage; + | RoutePage + | ClusterServiceVersionAction; const CEPH_FLAG = 'CEPH'; // keeping this for testing, will be removed once ocs operator available @@ -196,6 +198,23 @@ const plugin: Plugin = [ required: CEPH_FLAG, }, }, + { + type: 'ClusterServiceVersion/Action', + properties: { + kind: 'StorageCluster', + label: 'Add Capacity', + callback: (kind, ocsConfig) => () => { + const clusterObject = { ocsConfig }; + import( + './components/modals/add-capacity-modal/add-capacity-modal' /* webpackChunkName: "ceph-storage-add-capacity-modal" */ + ) + .then((m) => m.addCapacityModal(clusterObject)) + .catch((e) => { + throw e; + }); + }, + }, + }, ]; export default plugin; diff --git a/frontend/packages/console-plugin-sdk/src/registry.ts b/frontend/packages/console-plugin-sdk/src/registry.ts index 3445054b37d..38722d3071f 100644 --- a/frontend/packages/console-plugin-sdk/src/registry.ts +++ b/frontend/packages/console-plugin-sdk/src/registry.ts @@ -21,6 +21,7 @@ import { isOverviewResourceTab, isOverviewCRD, isGlobalConfig, + isClusterServiceVersionAction, } from './typings'; /** @@ -108,4 +109,8 @@ export class ExtensionRegistry { public getGlobalConfigs() { return this.extensions.filter(isGlobalConfig); } + + public getClusterServiceVersionActions() { + return this.extensions.filter(isClusterServiceVersionAction); + } } diff --git a/frontend/packages/console-plugin-sdk/src/typings/clusterserviceversions.ts b/frontend/packages/console-plugin-sdk/src/typings/clusterserviceversions.ts new file mode 100644 index 00000000000..4eebd84c586 --- /dev/null +++ b/frontend/packages/console-plugin-sdk/src/typings/clusterserviceversions.ts @@ -0,0 +1,22 @@ +import { K8sResourceKindReference } from '@console/internal/module/k8s'; +import { Extension } from './extension'; + +namespace ExtensionProperties { + export interface ClusterServiceVersionAction { + /** the kind this action is for */ + kind: K8sResourceKindReference; + /** label of action */ + label: string; + /** action callback */ + callback: (kind: K8sResourceKindReference, obj: any) => () => any; + } +} + +export interface ClusterServiceVersionAction + extends Extension { + type: 'ClusterServiceVersion/Action'; +} + +export const isClusterServiceVersionAction = ( + e: Extension, +): e is ClusterServiceVersionAction => e.type === 'ClusterServiceVersion/Action'; diff --git a/frontend/packages/console-plugin-sdk/src/typings/index.ts b/frontend/packages/console-plugin-sdk/src/typings/index.ts index a3498203003..4c8ad0b03af 100644 --- a/frontend/packages/console-plugin-sdk/src/typings/index.ts +++ b/frontend/packages/console-plugin-sdk/src/typings/index.ts @@ -11,3 +11,4 @@ export * from './pages'; export * from './perspectives'; export * from './yaml-templates'; export * from './global-configs'; +export * from './clusterserviceversions'; diff --git a/frontend/public/components/operator-lifecycle-manager/operand.tsx b/frontend/public/components/operator-lifecycle-manager/operand.tsx index 96a25063589..d97c9d49327 100644 --- a/frontend/public/components/operator-lifecycle-manager/operand.tsx +++ b/frontend/public/components/operator-lifecycle-manager/operand.tsx @@ -20,38 +20,50 @@ import { apiVersionForReference, kindForReference, K8sResourceKind, OwnerReferen import { ClusterServiceVersionModel } from '../../models'; import { deleteModal } from '../modals'; import { RootState } from '../../redux'; +import * as plugins from '../../plugins'; const csvName = () => location.pathname.split('/').find((part, i, allParts) => allParts[i - 1] === referenceForModel(ClusterServiceVersionModel) || allParts[i - 1] === ClusterServiceVersionModel.plural); -const actions = [ - (kind, obj) => ({ - label: `Edit ${kind.label}`, - href: `/k8s/ns/${obj.metadata.namespace}/${ClusterServiceVersionModel.plural}/${csvName()}/${referenceFor(obj)}/${obj.metadata.name}/yaml`, - accessReview: { - group: kind.apiGroup, - resource: kind.plural, - name: obj.metadata.name, - namespace: obj.metadata.namespace, - verb: 'update', - }, - }), - (kind, obj) => ({ - label: `Delete ${kind.label}`, - callback: () => deleteModal({ - kind, - resource: obj, - namespace: obj.metadata.namespace, - redirectTo: `/k8s/ns/${obj.metadata.namespace}/${ClusterServiceVersionModel.plural}/${csvName()}/${referenceFor(obj)}`, +const getActions = (selectedObj: any) => { + const actions = plugins.registry.getClusterServiceVersionActions().filter(action => + action.properties.kind === selectedObj.kind + ); + const pluginActions = actions.map(action => (kind, ocsObj) => ({ + label: action.properties.label, + callback: action.properties.callback(kind, ocsObj), + })); + return [ + ...pluginActions, + (kind, obj) => ({ + label: `Edit ${kind.label}`, + href: `/k8s/ns/${obj.metadata.namespace}/${ClusterServiceVersionModel.plural}/${csvName()}/${referenceFor(obj)}/${obj.metadata.name}/yaml`, + accessReview: { + group: kind.apiGroup, + resource: kind.plural, + name: obj.metadata.name, + namespace: obj.metadata.namespace, + verb: 'update', + }, }), - accessReview: { - group: kind.apiGroup, - resource: kind.plural, - name: obj.metadata.name, - namespace: obj.metadata.namespace, - verb: 'delete', - }, - }), -] as KebabAction[]; + + (kind, obj) => ({ + label: `Delete ${kind.label}`, + callback: () => deleteModal({ + kind, + resource: obj, + namespace: obj.metadata.namespace, + redirectTo: `/k8s/ns/${obj.metadata.namespace}/${ClusterServiceVersionModel.plural}/${csvName()}/${referenceFor(obj)}`, + }), + accessReview: { + group: kind.apiGroup, + resource: kind.plural, + name: obj.metadata.name, + namespace: obj.metadata.namespace, + verb: 'delete', + }, + }), + ] as KebabAction[]; +}; const tableColumnClasses = [ classNames('col-lg-2', 'col-md-3', 'col-sm-4', 'col-xs-6'), @@ -124,7 +136,7 @@ export const OperandTableRow: React.FC = ({obj, index, key - + ); @@ -263,9 +275,9 @@ export const OperandDetails = connectToModel((props: OperandDetailsProps) => { { currentStatus && -
- -
+
+ +
} { specDescriptors.map((specDescriptor: Descriptor, i) => @@ -312,7 +324,7 @@ export const OperandDetailsPage: React.SFC = (props) => resources={[ {kind: referenceForModel(ClusterServiceVersionModel), name: props.match.params.appName, namespace: props.namespace, isList: false, prop: 'csv'}, ]} - menuActions={actions} + menuActions={getActions(props.kind)} breadcrumbsFor={() => [ {name: 'Installed Operators', path: `/k8s/ns/${props.match.params.ns}/${ClusterServiceVersionModel.plural}`}, {name: props.match.params.appName, path: props.match.url.slice(0, props.match.url.lastIndexOf('/'))}, diff --git a/frontend/public/components/utils/storage-class-dropdown.tsx b/frontend/public/components/utils/storage-class-dropdown.tsx index aaa3dcb23be..d5780e855de 100644 --- a/frontend/public/components/utils/storage-class-dropdown.tsx +++ b/frontend/public/components/utils/storage-class-dropdown.tsx @@ -8,13 +8,13 @@ import { isDefaultClass } from '../storage-class'; /* Component StorageClassDropdown - creates a dropdown list of storage classes */ -class StorageClassDropdown_ extends React.Component { - readonly state: StorageClassDropdownState = { +export class StorageClassDropdownInner extends React.Component { + readonly state: StorageClassDropdownInnerState = { items: {}, name: this.props.name, selectedKey: null, title: , - defaultClass: null, + defaultClass: this.props.defaultClass, }; componentWillMount() { @@ -137,9 +137,9 @@ class StorageClassDropdown_ extends React.Component -

+ {describedBy &&

Storage class for the new claim. -

+

} } ; @@ -148,7 +148,7 @@ class StorageClassDropdown_ extends React.Component { return - + ; }; @@ -173,7 +173,7 @@ const StorageClassDropdownNoStorageClassOption = props => { ; }; -export type StorageClassDropdownState = { +export type StorageClassDropdownInnerState = { items: any; name: string; selectedKey: string; @@ -181,11 +181,13 @@ export type StorageClassDropdownState = { defaultClass: string; }; -export type StorageClassDropdownProps = { +export type StorageClassDropdownInnerProps = { id?: string; loaded?: boolean; resources?: any; name: string; onChange: (object) => void; describedBy: string; + defaultClass: string; + required?: boolean; };