diff --git a/frontend/public/components/daemon-set.jsx b/frontend/public/components/daemon-set.jsx index 3c28546152a..b693c7c62c3 100644 --- a/frontend/public/components/daemon-set.jsx +++ b/frontend/public/components/daemon-set.jsx @@ -120,7 +120,7 @@ const Details = ({obj: daemonset}) =>
- +
; diff --git a/frontend/public/components/deployment-config.tsx b/frontend/public/components/deployment-config.tsx index a63ccf8bcff..7b50d7d1383 100644 --- a/frontend/public/components/deployment-config.tsx +++ b/frontend/public/components/deployment-config.tsx @@ -155,7 +155,7 @@ export const DeploymentConfigsDetails: React.FC<{obj: K8sResourceKind}> = ({obj:
- +
diff --git a/frontend/public/components/deployment.tsx b/frontend/public/components/deployment.tsx index bd20250cf32..6e228e67b79 100644 --- a/frontend/public/components/deployment.tsx +++ b/frontend/public/components/deployment.tsx @@ -114,7 +114,7 @@ const DeploymentDetails: React.FC = ({obj: deployment})
- +
diff --git a/frontend/public/components/modals/index.ts b/frontend/public/components/modals/index.ts index 7c1352c5746..55cd9664bdf 100644 --- a/frontend/public/components/modals/index.ts +++ b/frontend/public/components/modals/index.ts @@ -63,5 +63,8 @@ export const installPlanPreviewModal = (props) => import('./installplan-preview- export const expandPVCModal = (props) => import('./expand-pvc-modal' /* webpackChunkName: "expand-pvc-modal" */) .then(m => m.expandPVCModal(props)); +export const removeVolumeModal = (props) => import('./remove-volume-modal' /* webpackChunkName: "remove-volume-modal" */) + .then(m => m.removeVolumeModal(props)); + export const configureMachineAutoscalerModal = (props) => import('./configure-machine-autoscaler-modal' /* webpackChunkName: "configure-machine-autoscaler-modal" */) .then(m => m.configureMachineAutoscalerModal(props)); diff --git a/frontend/public/components/modals/remove-volume-modal.tsx b/frontend/public/components/modals/remove-volume-modal.tsx new file mode 100644 index 00000000000..6566f19ad4c --- /dev/null +++ b/frontend/public/components/modals/remove-volume-modal.tsx @@ -0,0 +1,86 @@ +import * as _ from 'lodash-es'; +import * as React from 'react'; + +import { createModalLauncher, ModalTitle, ModalBody, ModalSubmitFooter } from '../factory'; +import { ContainerSpec, getVolumeType, K8sKind, k8sPatch, K8sResourceKind, Volume, VolumeMount } from '../../module/k8s/'; +import { RowVolumeData } from '../volumes-table'; + +export const RemoveVolumeModal: React.FC = (props) => { + const [inProgress, setInProgress] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(''); + + const getRemoveVolumePatch = (resource: K8sResourceKind, rowVolumeData: RowVolumeData) => { + const containers: ContainerSpec[] = _.get(resource, 'spec.template.spec.containers', []); + const patches = []; + let allowRemoveVolume = true; + containers.forEach((container: ContainerSpec, i: number) => { + const mounts: VolumeMount[] = _.get(container, 'volumeMounts', []); + mounts.forEach((mount: VolumeMount, j: number) => { + if (mount.name !== rowVolumeData.name) { + return; + } + if (mount.mountPath === rowVolumeData.mountPath) { + patches.push({op: 'remove', path: `/spec/template/spec/containers/${i}/volumeMounts/${j}`}); + } else { + allowRemoveVolume = false; + } + }); + }); + + // if the mountCount is greater than zero, then the volume is still being used at a different mount point or in a different container + // Either way, we cannot give the cmd to remove it + if (allowRemoveVolume) { + const volumes: Volume[] = _.get(resource, 'spec.template.spec.volumes', []); + const volumeIndex = volumes.findIndex((v: Volume) => v.name === rowVolumeData.volumeDetail.name); + patches.push({op: 'remove', path: `/spec/template/spec/volumes/${volumeIndex}`}); + } + return patches; + }; + + const submit = (event: React.FormEvent) => { + event.preventDefault(); + setErrorMessage(''); + setInProgress(true); + const { kind, resource, volume } = props; + k8sPatch(kind, resource, getRemoveVolumePatch(resource, volume)).then(() => { + setInProgress(false); + props.close(); + }).catch(({message: errMessage}) => { + setErrorMessage(errMessage); + setInProgress(false); + }); + }; + + const {kind, resource, volume} = props; + const type: string = _.get(getVolumeType(volume.volumeDetail), 'id', ''); + return
+ Remove Volume + +
+
+
+ + ; +}; + +export const removeVolumeModal = createModalLauncher(RemoveVolumeModal); + +export type RemoveVolumeModalProps = { + cancel: (e: Event) => void; + close: () => void; + volume: RowVolumeData; + kind: K8sKind; + resource: K8sResourceKind; +}; diff --git a/frontend/public/components/pod.tsx b/frontend/public/components/pod.tsx index 31e1d1a2460..30585d55c55 100644 --- a/frontend/public/components/pod.tsx +++ b/frontend/public/components/pod.tsx @@ -276,7 +276,7 @@ const Details: React.FC = ({obj: pod}) => {
- +
; }; diff --git a/frontend/public/components/replicaset.jsx b/frontend/public/components/replicaset.jsx index fda16fe0bb7..21ddc3d903a 100644 --- a/frontend/public/components/replicaset.jsx +++ b/frontend/public/components/replicaset.jsx @@ -50,7 +50,7 @@ const Details = ({obj: replicaSet}) => {
- +
; }; diff --git a/frontend/public/components/replication-controller.jsx b/frontend/public/components/replication-controller.jsx index ed771b54d45..50d2dfab326 100644 --- a/frontend/public/components/replication-controller.jsx +++ b/frontend/public/components/replication-controller.jsx @@ -44,7 +44,7 @@ const Details = ({obj: replicationController}) => {
- +
; }; diff --git a/frontend/public/components/stateful-set.jsx b/frontend/public/components/stateful-set.jsx index c74afd724f0..a5e919c8d25 100644 --- a/frontend/public/components/stateful-set.jsx +++ b/frontend/public/components/stateful-set.jsx @@ -50,7 +50,7 @@ const Details = ({obj: ss}) =>
- +
; diff --git a/frontend/public/components/utils/index.tsx b/frontend/public/components/utils/index.tsx index 876579fd90f..f07b64948b9 100644 --- a/frontend/public/components/utils/index.tsx +++ b/frontend/public/components/utils/index.tsx @@ -10,7 +10,6 @@ export * from './log-window'; export * from './resource-icon'; export * from './resource-link'; export * from './resource-log'; -export * from './volume-icon'; export * from './timestamp'; export * from './horizontal-nav'; export * from './details-page'; @@ -57,6 +56,7 @@ export * from './safe-fetch-hook'; export * from './camel-case-wrap'; export * from './truncate-middle'; export * from './expand-collapse'; +export * from './volume-type'; /* Add the enum for NameValueEditorPair here and not in its namesake file because the editor should always be diff --git a/frontend/public/components/utils/volume-icon.jsx b/frontend/public/components/utils/volume-icon.jsx deleted file mode 100644 index bcaa223a75d..00000000000 --- a/frontend/public/components/utils/volume-icon.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as _ from 'lodash-es'; -import * as React from 'react'; -import { LockIcon, OutlinedCopyIcon, OutlinedFolderOpenIcon } from '@patternfly/react-icons'; - -import {VolumeSource} from '../../module/k8s/pods'; - -export const VolumeIcon = ({kind}) => { - const icons = { - [VolumeSource.emptyDir.id]: , - [VolumeSource.hostPath.id]: , - [VolumeSource.secret.id]: , - }; - const icon = icons[kind]; - - return - {icon} - {_.get(VolumeSource[kind], 'label', '')} - ; -}; diff --git a/frontend/public/components/utils/volume-type.tsx b/frontend/public/components/utils/volume-type.tsx new file mode 100644 index 00000000000..c53f753cea8 --- /dev/null +++ b/frontend/public/components/utils/volume-type.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import * as _ from 'lodash-es'; + +import { Volume } from '../../module/k8s'; +import { getVolumeLocation, getVolumeType } from '../../module/k8s/pods'; +import { ResourceLink } from './'; + +export const VolumeType: React.FC = ({volume, namespace}) => { + if (volume.secret) { + return ; + } + + if (volume.configMap) { + return ; + } + + if (volume.persistentVolumeClaim) { + return ; + } + + const type = getVolumeType(volume); + const loc = _.trim(getVolumeLocation(volume)); + return type + ? <> + {type.label} + {loc && <> ({loc})} + + : null; +}; + +export type VolumeTypeProps = { + volume: Volume; + namespace: string; +}; diff --git a/frontend/public/components/volumes-table.tsx b/frontend/public/components/volumes-table.tsx index cd201fc1ad7..e7bc094d995 100644 --- a/frontend/public/components/volumes-table.tsx +++ b/frontend/public/components/volumes-table.tsx @@ -5,28 +5,45 @@ import * as classNames from 'classnames'; import { ContainerSpec, + K8sKind, + K8sResourceKind, + K8sResourceKindReference, PodKind, PodTemplate, Volume, VolumeMount, } from '../module/k8s'; -import { - getVolumeType, - getVolumeLocation, -} from '../module/k8s/pods'; -import { - VolumeIcon, - ResourceIcon, - EmptyBox, - SectionHeading, -} from './utils'; +import { asAccessReview, EmptyBox, Kebab, KebabOption, ResourceIcon, SectionHeading, VolumeType } from './utils'; +import { Table, TableData, TableRow } from './factory'; +import { sortable } from '@patternfly/react-table'; +import { removeVolumeModal } from './modals'; +import { connectToModel } from '../kinds'; + +const removeVolume = (kind: K8sKind, obj: K8sResourceKind, volume: RowVolumeData): KebabOption => { + return { + label: 'Remove Volume', + callback: () => removeVolumeModal({ + kind, + resource: obj, + volume, + }), + accessReview: asAccessReview(kind, obj, 'patch'), + }; +}; + +const menuActions = [removeVolume]; + +const getPodTemplate = (resource: K8sResourceKind): PodTemplate => { + return resource.kind === 'Pod' ? resource as PodKind : resource.spec.template; +}; -const getVolumeMountsByPermissions = (pod: PodTemplate): VolumeData[] => { +const getRowVolumeData = (resource: K8sResourceKind): RowVolumeData[] => { + const pod: PodTemplate = getPodTemplate(resource); if (!pod || !pod.spec || !pod.spec.volumes) { return []; } - const m = {}; + const m = {}; const volumes = (pod.spec.volumes || []).reduce((p, v: Volume) => { p[v.name] = v; return p; @@ -34,15 +51,12 @@ const getVolumeMountsByPermissions = (pod: PodTemplate): VolumeData[] => { _.forEach(pod.spec.containers, (c: ContainerSpec) => { _.forEach(c.volumeMounts, (v: VolumeMount) => { - const k = `${v.name}_${v.readOnly ? 'ro' : 'rw'}`; + const k = `${v.name}_${v.readOnly ? 'ro' : 'rw'}_${v.mountPath}`; const mount = {container: c.name, mountPath: v.mountPath, subPath: v.subPath}; - if (k in m) { - return m[k].mounts.push(mount); - } - m[k] = {mounts: [mount], name: v.name, readOnly: !!v.readOnly, volume: volumes[v.name]}; + m[k] = {name: v.name, readOnly: !!v.readOnly, volumeDetail: volumes[v.name], + container: mount.container, mountPath: mount.mountPath, subPath: mount.subPath, resource}; }); }); - return _.values(m); }; @@ -52,70 +66,121 @@ const ContainerLink: React.FC = ({name, pod}) => ; ContainerLink.displayName = 'ContainerLink'; -const VolumeRow: React.FC = ({value, pod}) => { - const kind = _.get(getVolumeType(value.volume), 'id', ''); - const loc = getVolumeLocation(value.volume); - const name = value.name; - const permission = value.readOnly ? 'Read-only' : 'Read/Write'; - - return
- {value.mounts.map((m: Mount, i: number) => -
-
{name}
-
{_.get(m, 'mountPath', '-')}
-
- {_.get(m, 'subPath', '-')} -
-
- - {loc && ` (${loc})`} -
-
{permission}
-
- { _.get(pod, 'kind') === 'Pod' - ? - :
{m.container}
- } -
-
-
)} -
; +const volumeRowColumnClasses = [ + classNames('col-lg-2', 'col-md-3', 'col-sm-4', 'col-xs-5'), + classNames('col-lg-2', 'col-md-3', 'col-sm-4', 'col-xs-7'), + classNames('col-lg-2', 'col-md-2', 'col-sm-4', 'hidden-xs'), + classNames('col-lg-2', 'col-md-2', 'hidden-sm', 'hidden-xs'), + classNames('col-lg-2', 'col-md-2', 'hidden-sm', 'hidden-xs'), + classNames('col-lg-2', 'hidden-md', 'hidden-sm', 'hidden-xs'), + Kebab.columnClass, +]; + +const VolumesTableHeader = () => { + return [ + { + title: 'Name', sortField: 'name', transforms: [sortable], + props: { className: volumeRowColumnClasses[0]}, + }, + { + title: 'Mount Path', sortField: 'mountPath', transforms: [sortable], + props: { className: volumeRowColumnClasses[1]}, + }, + { + title: 'SubPath', sortField: 'subPath', transforms: [sortable], + props: { className: volumeRowColumnClasses[2]}, + }, + { + title: 'Type', + props: { className: volumeRowColumnClasses[3]}, + }, + { + title: 'Permissions', sortField: 'readOnly', transforms: [sortable], + props: { className: volumeRowColumnClasses[4]}, + }, + { + title: 'Utilized By', sortField: 'container', transforms: [sortable], + props: { className: volumeRowColumnClasses[5]}, + }, + { + title: '', + props: { className: volumeRowColumnClasses[6]}, + }, + ]; +}; +VolumesTableHeader.displayName = 'VolumesTableHeader'; + +const VolumesTableRow = ({obj: volume, index, key, style}) => { + const { name, resource, readOnly, mountPath, subPath, volumeDetail } = volume; + const permission = readOnly ? 'Read-only' : 'Read/Write'; + const pod: PodTemplate = getPodTemplate(resource); + + return ( + + {name} + {mountPath} + {subPath} + + + + {permission} + + {_.get(pod, 'kind') === 'Pod' ? :
{volume.container}
} +
+ + + +
+ ); }; -VolumeRow.displayName = 'VolumeRow'; +VolumesTableRow.displayName = 'VolumesTableRow'; -export const VolumesTable: React.FC = ({podTemplate, heading}) => ( - - {heading && } - {_.isEmpty(podTemplate.spec.volumes) +export const VolumesTable = props => { + const { resource, ...tableProps } = props; + const data: RowVolumeData[] = getRowVolumeData(resource); + const pod: PodTemplate = getPodTemplate(resource); + return + {props.heading && } + {_.isEmpty(pod.spec.volumes) ? : ( -
-
-
Name
-
Mount Path
-
SubPath
-
Type
-
Permissions
-
Utilized By
-
-
- {getVolumeMountsByPermissions(podTemplate).map((v, i) => )} -
-
+ )} - -); + ; +}; + VolumesTable.displayName = 'VolumesTable'; -type Mount = { - container: string; -} & VolumeMount; +const VolumeKebab = connectToModel((props: VolumeKebabProps) => { + const {actions, kindObj, resource, isDisabled, rowVolumeData} = props; + if (!kindObj || kindObj.kind === 'Pod') { + return null; + } + const options = actions.map(b => b(kindObj, resource, rowVolumeData)); + return ; +}); -type VolumeData = { +type VolumeKebabAction = (kind: K8sKind, obj: K8sResourceKind, rowVolumeData: RowVolumeData) => KebabOption; +type VolumeKebabProps = { + kindObj: K8sKind; + actions: VolumeKebabAction[]; + kind: K8sResourceKindReference; + resource: K8sResourceKind; + isDisabled?: boolean; + rowVolumeData: RowVolumeData; +}; + +export type RowVolumeData = { name: string; readOnly: boolean; - volume: Volume; - mounts: Mount[]; + volumeDetail: Volume; + container: string; + mountPath: string; + subPath?: string; + resource: K8sResourceKind; }; type ContainerLinkProps = { @@ -123,12 +188,3 @@ type ContainerLinkProps = { name: string; }; -type VolumeRowProps = { - pod: PodKind | PodTemplate; - value: VolumeData; -}; - -type VolumesTableProps = { - podTemplate: PodKind | PodTemplate; - heading?: string; -};