Skip to content

Commit

Permalink
add snapshots to vm details
Browse files Browse the repository at this point in the history
  • Loading branch information
Gilad Lekner authored and Gilad Lekner committed Jul 16, 2020
1 parent 5970c5d commit 46ec70d
Show file tree
Hide file tree
Showing 12 changed files with 440 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as React from 'react';
import { Alert, AlertVariant, Form, TextInput } from '@patternfly/react-core';
import { prefixedID } from '../../../utils';
import { HandlePromiseProps, withHandlePromise } from '@console/internal/components/utils';
import { getName, getNamespace } from '@console/shared';
import {
createModalLauncher,
ModalBody,
ModalComponentProps,
ModalTitle,
} from '@console/internal/components/factory';
import { k8sCreate } from '@console/internal/module/k8s';
import { VMLikeEntityKind } from '../../../types/vmLike';
import { FormRow } from '../../form/form-row';
import { ADD_SNAPSHOT, SAVE } from '../../../utils/strings';
import { ModalFooter } from '../modal/modal-footer';
import { VMSnapshotWrapper } from '../../../k8s/wrapper/vm/vm-snapshot-wrapper';

const getSnapshotName = (vmName: string) => {
const date = new Date();
const initial = `${vmName}-`;
return initial.concat(
date.getFullYear().toString(),
'-',
(date.getUTCMonth() + 1).toString(),
'-',
date.getDate().toString(),
);
};

const SnapshotsModal = withHandlePromise((props: SnapshotsModalProps) => {
const { vmLikeEntity, inProgress, errorMessage, handlePromise, close, cancel } = props;
const vmName = getName(vmLikeEntity);
const [name, setName] = React.useState<string>(getSnapshotName(vmName));
const asId = prefixedID.bind(null, 'snapshot');

const submit = async (e) => {
e.preventDefault();
const snapshotWrapper = new VMSnapshotWrapper().init({
name,
namespace: getNamespace(vmLikeEntity),
vmName,
});

// eslint-disable-next-line promise/catch-or-return
handlePromise(k8sCreate(snapshotWrapper.getModel(), snapshotWrapper.asResource())).then(close);
};

return (
<div className="modal-content">
<ModalTitle>{ADD_SNAPSHOT}</ModalTitle>
<ModalBody>
<Alert
title="Snapshot only includes disks backed by a snapshot supported storage class"
isInline
variant={AlertVariant.info}
className="co-m-form-row"
/>
<Form onSubmit={submit}>
<FormRow title="Snapshot Name" fieldId={asId('name')} isRequired>
<TextInput
autoFocus
isRequired
id={asId('name')}
value={name}
onChange={(v) => setName(v)}
/>
</FormRow>
</Form>
</ModalBody>
<ModalFooter
id="snapshot"
submitButtonText={SAVE}
errorMessage={errorMessage}
isDisabled={inProgress}
onSubmit={submit}
onCancel={(e) => {
e.stopPropagation();
cancel();
}}
/>
</div>
);
});

export default createModalLauncher(SnapshotsModal);

export type SnapshotsModalProps = {
vmLikeEntity: VMLikeEntityKind;
} & ModalComponentProps &
HandlePromiseProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as React from 'react';
import { TableData, TableRow, RowFunction } from '@console/internal/components/factory';
import {
asAccessReview,
Kebab,
KebabOption,
ResourceLink,
Timestamp,
} from '@console/internal/components/utils';
import { deleteModal } from '@console/internal/components/modals';
import {
getName,
getNamespace,
dimensifyRow,
getCreationTimestamp,
Status,
ErrorStatus,
} from '@console/shared';
import { referenceFor } from '@console/internal/module/k8s';
import { VirtualMachineSnapshotModel } from '../../models';
import { isVMI } from '../../selectors/check-type';
import { VMLikeEntityKind } from '../../types/vmLike';
import { VMSnapshotRowActionOpts, VMSnapshotRowCustomData } from './types';
import { VMSnapshot } from '../../types';
import { getVMSnapshotError, isVMSnapshotReady } from '../../selectors/snapshot/snapshot';

// TODO: add revertMenuAction once implemented

const menuActionDelete = (
snapshot: VMSnapshot,
{ withProgress }: { withProgress: (promise: Promise<any>) => void },
): KebabOption => ({
label: 'Delete',
callback: () =>
withProgress(
deleteModal({
kind: VirtualMachineSnapshotModel,
resource: snapshot,
}),
),
accessReview: asAccessReview(VirtualMachineSnapshotModel, snapshot, 'delete'),
});

const getActions = (
snapshot: VMSnapshot,
vmLikeEntity: VMLikeEntityKind,
opts: VMSnapshotRowActionOpts,
) => {
if (isVMI(vmLikeEntity)) {
return [];
}
const actions = [menuActionDelete];
return actions.map((a) => a(snapshot, opts));
};

export type VMSnapshotSimpleRowProps = {
data: VMSnapshot;
columnClasses: string[];
actionsComponent: React.ReactNode;
index: number;
style: object;
};

export const SnapshotSimpleRow: React.FC<VMSnapshotSimpleRowProps> = ({
data: snapshot,
columnClasses,
actionsComponent,
index,
style,
}) => {
const dimensify = dimensifyRow(columnClasses);
const name = getName(snapshot);
const namespace = getNamespace(snapshot);
const error = getVMSnapshotError(snapshot);
const readyToUse = isVMSnapshotReady(snapshot);

return (
<TableRow id={snapshot?.metadata?.uid} index={index} trKey={name} style={style}>
<TableData className={dimensify()}>
<ResourceLink
kind={referenceFor(VirtualMachineSnapshotModel)}
namespace={namespace}
name={name}
/>
</TableData>
<TableData className={dimensify()}>
<Timestamp timestamp={getCreationTimestamp(snapshot)} />
</TableData>
<TableData className={dimensify()}>
{error ? (
<ErrorStatus>{error?.message}</ErrorStatus>
) : (
<Status status={readyToUse ? 'Ready' : 'Not Ready'} />
)}
</TableData>
<TableData className={dimensify(true)}>{actionsComponent}</TableData>
</TableRow>
);
};

export const SnapshotRow: RowFunction<VMSnapshot, VMSnapshotRowCustomData> = ({
obj: snapshot,
customData: { withProgress, vmLikeEntity, columnClasses },
index,
style,
}) => (
<SnapshotSimpleRow
data={snapshot}
columnClasses={columnClasses}
index={index}
style={style}
actionsComponent={
<Kebab
options={getActions(snapshot, vmLikeEntity, { withProgress })}
id={`kebab-for-${getName(snapshot)}`}
/>
}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { VMLikeEntityKind } from '../../types/vmLike';

export type VMSnapshotRowActionOpts = { withProgress: (promise: Promise<any>) => void };

export type VMSnapshotRowCustomData = {
vmLikeEntity: VMLikeEntityKind;
columnClasses: string[];
isDisabled: boolean;
} & VMSnapshotRowActionOpts;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as classNames from 'classnames';
import { Kebab } from '@console/internal/components/utils';

export const snapshotsTableColumnClasses = [
classNames('col-lg-2'),
classNames('col-lg-2'),
classNames('col-lg-2'),
Kebab.columnClass,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import * as React from 'react';
import { Table, RowFunction } from '@console/internal/components/factory';
import { sortable } from '@patternfly/react-table';
import { getName, getNamespace, dimensifyHeader } from '@console/shared';
import { useSafetyFirst } from '@console/internal/components/safety-first';
import { Button } from '@patternfly/react-core';
import { EmptyBox } from '@console/internal/components/utils';
import {
WatchK8sResource,
useK8sWatchResource,
} from '@console/internal/components/utils/k8s-watch-hook';
import { getVmSnapshotVmName } from '../../selectors/snapshot/snapshot';
import { VMSnapshot } from '../../types';
import { isVMI } from '../../selectors/check-type';
import { wrapWithProgress } from '../../utils/utils';
import { VMLikeEntityTabProps } from '../vms/types';
import { snapshotsTableColumnClasses } from './utils';
import { ADD_SNAPSHOT } from '../../utils/strings';
import { VirtualMachineSnapshotModel } from '../../models';
import { SnapshotRow } from './snapshot-row';
import SnapshotModal from '../modals/snapshots-modal/snapshots-modal';
import { asVM, isVMRunningOrExpectedRunning } from '../../selectors/vm';

export type VMSnapshotsTableProps = {
data?: any[];
customData?: object;
row: RowFunction;
columnClasses: string[];
loadError: any;
loaded: boolean;
};

const NoDataEmptyMsg = () => <EmptyBox label="Snapshots" />;

export const VMSnapshotsTable: React.FC<VMSnapshotsTableProps> = ({
data,
customData,
row: Row,
columnClasses,
loaded,
loadError,
}) => (
<Table
aria-label="VM Snapshots List"
loaded={loaded}
loadError={loadError}
data={data}
NoDataEmptyMsg={NoDataEmptyMsg}
Header={() =>
dimensifyHeader(
[
{
title: 'Name',
sortField: 'metadata.name',
transforms: [sortable],
},
{
title: 'Created',
sortField: 'metadata.creationTimestamp',
transforms: [sortable],
},
{
title: 'Status',
sortField: 'status.readyToUse',
transforms: [sortable],
},
{
title: '',
},
],
columnClasses,
)
}
Row={Row}
customData={{ ...customData, columnClasses }}
virtualize
/>
);

export const VMSnapshotsPage: React.FC<VMLikeEntityTabProps> = ({ obj: vmLikeEntity }) => {
const vmName = getName(vmLikeEntity);
const namespace = getNamespace(vmLikeEntity);

const resource: WatchK8sResource = React.useMemo(
() => ({
isList: true,
kind: VirtualMachineSnapshotModel.kind,
namespaced: true,
namespace,
}),
[namespace],
);

const [snapshots, snapshotsLoaded, snapshotsError] = useK8sWatchResource<VMSnapshot[]>(resource);
const [isLocked, setIsLocked] = useSafetyFirst(false);
const withProgress = wrapWithProgress(setIsLocked);
const filteredSnapshots = snapshots.filter((snap) => getVmSnapshotVmName(snap) === vmName);
const isDisabled = isLocked || isVMRunningOrExpectedRunning(asVM(vmLikeEntity));

return (
<div className="co-m-list">
{!isVMI(vmLikeEntity) && (
<div className="co-m-pane__filter-bar">
<div className="co-m-pane__filter-bar-group">
<Button
variant="primary"
id="add-snapshot"
onClick={() =>
withProgress(
SnapshotModal({
blocking: true,
vmLikeEntity,
}).result,
)
}
isDisabled={isDisabled}
>
{ADD_SNAPSHOT}
</Button>
</div>
</div>
)}
<div className="co-m-pane__body">
<VMSnapshotsTable
loaded={snapshotsLoaded}
loadError={snapshotsError}
data={filteredSnapshots}
customData={{
vmLikeEntity,
withProgress,
isDisabled,
}}
row={SnapshotRow}
columnClasses={snapshotsTableColumnClasses}
/>
</div>
</div>
);
};

0 comments on commit 46ec70d

Please sign in to comment.