Skip to content

Commit

Permalink
Bug 1830080: Prompt user to update traffic on revision deletion
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-phillips-18 committed Apr 30, 2020
1 parent 57b271b commit 5587585
Show file tree
Hide file tree
Showing 16 changed files with 397 additions and 70 deletions.
20 changes: 20 additions & 0 deletions frontend/packages/knative-plugin/src/actions/delete-revision.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { KebabOption } from '@console/internal/components/utils';
import { K8sKind, K8sResourceKind } from '@console/internal/module/k8s';
import { deleteRevisionModal } from '../components/modals';

export const deleteRevision = (model: K8sKind, revision: K8sResourceKind): KebabOption => {
return {
label: `Delete ${model.label}`,
callback: () =>
deleteRevisionModal({
revision,
}),
accessReview: {
group: model.apiGroup,
resource: model.plural,
name: revision.metadata.name,
namespace: revision.metadata.namespace,
verb: 'delete',
},
};
};
18 changes: 18 additions & 0 deletions frontend/packages/knative-plugin/src/actions/getRevisionActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Kebab } from '@console/internal/components/utils';
import { RevisionModel } from '../models';
import { deleteRevision } from './delete-revision';

export const getRevisionActions = () => {
let deleteFound = false;
const commonActions = Kebab.factory.common.map((action) => {
if (action.name === 'Delete') {
deleteFound = true;
return deleteRevision;
}
return action;
});
if (!deleteFound) {
commonActions.push(deleteRevision);
}
return [...Kebab.getExtensionsActionsForKind(RevisionModel), ...commonActions];
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export const setSinkSourceModal = (props) =>
import('../sink-source/SinkSourceController' /* webpackChunkName: "sink-source" */).then((m) =>
m.sinkModalLauncher(props),
);

export const deleteRevisionModal = (props) =>
import(
'../revisions/DeleteRevisionModalController' /* webpackChunkName: "delete-revision" */
).then((m) => m.deleteRevisionModalLauncher(props));
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { KNATIVE_SERVING_APIGROUP } from '../../const';
import { isDynamicEventResourceKind } from '../../utils/fetch-dynamic-eventsources-utils';
import OverviewDetailsKnativeResourcesTab from './OverviewDetailsKnativeResourcesTab';
import KnativeOverview from './KnativeOverview';
import { RevisionModel } from '../../models';
import { getRevisionActions } from '../../actions/getRevisionActions';

interface StateProps {
kindsInFlight?: boolean;
Expand Down Expand Up @@ -45,11 +47,16 @@ export const KnativeResourceOverviewPage: React.ComponentType<KnativeResourceOve
model.apiGroup === apiInfo.group &&
model.apiVersion === apiInfo.version,
);
let actions = [...Kebab.getExtensionsActionsForKind(resourceModel), ...Kebab.factory.common];
if (resourceModel.kind === RevisionModel.kind) {
actions = getRevisionActions();
}

return (
<ResourceOverviewDetails
item={item}
kindObj={resourceModel}
menuActions={[...Kebab.getExtensionsActionsForKind(resourceModel), ...Kebab.factory.common]}
menuActions={actions}
tabs={tabs}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';
import { FormikProps, FormikValues } from 'formik';
import { Alert } from '@patternfly/react-core';
import { YellowExclamationTriangleIcon } from '@console/shared';
import {
ModalTitle,
ModalBody,
ModalSubmitFooter,
} from '@console/internal/components/factory/modal';
import { K8sResourceKind } from '@console/internal/module/k8s';
import { RevisionModel } from '../../models';
import TrafficSplittingFields from '../traffic-splitting/TrafficSplittingFields';

export interface TrafficSplittingDeleteModalProps {
revisionItems: any;
deleteRevision: K8sResourceKind;
}

type Props = FormikProps<FormikValues> & TrafficSplittingDeleteModalProps;

const DeleteRevisionModal: React.FC<Props> = (props) => {
const { deleteRevision, handleSubmit, handleReset, isSubmitting, status } = props;

return (
<form className="modal-content" onSubmit={handleSubmit}>
<ModalTitle>
<YellowExclamationTriangleIcon className="co-icon-space-r" /> Delete {RevisionModel.label}?
</ModalTitle>
<ModalBody>
<p>
Are you sure you want to delete{' '}
<strong className="co-break-word">{deleteRevision.metadata.name}</strong> in namespace{' '}
<strong>{deleteRevision.metadata.namespace}</strong>
</p>
<Alert
isInline
className="co-alert"
variant="default"
title="Update the traffic distribution among the remaining Revisions"
/>
<TrafficSplittingFields {...props} />
</ModalBody>
<ModalSubmitFooter
inProgress={isSubmitting}
submitText="Delete"
cancel={handleReset}
errorMessage={status.error}
submitDanger
/>
</form>
);
};

export default DeleteRevisionModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import * as React from 'react';
import { Formik, FormikHelpers, FormikValues } from 'formik';
import {
k8sKill,
K8sResourceKind,
k8sUpdate,
referenceForModel,
} from '@console/internal/module/k8s';
import { Firehose, FirehoseResult } from '@console/internal/components/utils';
import { createModalLauncher, ModalComponentProps } from '@console/internal/components/factory';
import { errorModal } from '@console/internal/components/modals';
import { KNATIVE_SERVING_LABEL } from '../../const';
import { RevisionModel, ServiceModel } from '../../models';
import {
transformTrafficSplitingData,
knativeServingResourcesTrafficSplitting,
getRevisionItems,
constructObjForUpdate,
} from '../../utils/traffic-splitting-utils';
import { TrafficSplittingType } from '../traffic-splitting/TrafficSplitting';
import DeleteRevisionModal from './DeleteRevisionModal';

type ControllerProps = {
loaded?: boolean;
revision?: K8sResourceKind;
resources?: {
configurations: FirehoseResult;
revisions: FirehoseResult;
services: FirehoseResult;
};
cancel?: () => void;
close?: () => void;
};

const Controller: React.FC<ControllerProps> = ({ loaded, resources, revision, cancel, close }) => {
if (!loaded) {
return null;
}
const service = resources.services.data.find((s: K8sResourceKind) => {
return revision.metadata.labels[KNATIVE_SERVING_LABEL] === s.metadata.name;
});

const revisions = transformTrafficSplitingData(service, resources).filter(
(r) => revision.metadata.uid !== r.metadata.uid,
);

if (revisions.length === 0) {
errorModal({
title: `Unable to delete ${RevisionModel.label}`,
error: `You cannot delete the last ${RevisionModel.label} for the ${ServiceModel.label}.`,
});
close();
}

const revisionItems = getRevisionItems(revisions);

const traffic = service?.status?.traffic ?? [{ percent: 0, tag: '', revisionName: '' }];

const initialValues: TrafficSplittingType = {
trafficSplitting: traffic.reduce((acc, t) => {
if (!t.revisionName || revisions.find((r) => r.metadata.name === t.revisionName)) {
acc.push({
percent: t.percent,
tag: t.tag || '',
revisionName: t.revisionName || '',
});
}
return acc;
}, []),
};

if (initialValues.trafficSplitting.length === 0 && revisions.length > 0) {
initialValues.trafficSplitting.push({
percent: 0,
tag: '',
revisionName: revisions[0].metadata.name,
});
}

const handleSubmit = (values: FormikValues, action: FormikHelpers<FormikValues>) => {
const obj = constructObjForUpdate(values.trafficSplitting, service);
k8sUpdate(ServiceModel, obj)
.then(() => {
action.setSubmitting(false);
action.setStatus({ error: '' });
if (!revision) {
close();
return;
}
// eslint-disable-next-line promise/no-nesting
k8sKill(RevisionModel, revision)
.then(() => {
close();
})
.catch((err) => {
action.setStatus({ error: err.message || 'An error occurred. Please try again' });
});
})
.catch((err) => {
action.setStatus({ error: err.message || 'An error occurred. Please try again' });
});
};

return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
onReset={cancel}
initialStatus={{ error: '' }}
>
{(modalProps) => (
<DeleteRevisionModal
{...modalProps}
revisionItems={revisionItems}
deleteRevision={revision}
/>
)}
</Formik>
);
};

type DeleteRevisionModalControllerProps = {
revision: K8sResourceKind;
};

const DeleteRevisionModalController: React.FC<DeleteRevisionModalControllerProps> = (props) => {
const {
metadata: { namespace },
} = props.revision;
const resources = knativeServingResourcesTrafficSplitting(namespace);
resources.push({
isList: true,
kind: referenceForModel(ServiceModel),
namespace,
prop: 'services',
});

return (
<Firehose resources={resources}>
<Controller {...props} />
</Firehose>
);
};

type Props = DeleteRevisionModalControllerProps & ModalComponentProps;

export const deleteRevisionModalLauncher = createModalLauncher<Props>(
DeleteRevisionModalController,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import { DetailsPage } from '@console/internal/components/factory';
import { navFactory } from '@console/internal/components/utils';
import { DetailsForKind } from '@console/internal/components/default-resource';
import { K8sKind, K8sResourceKind } from '@console/internal/module/k8s';
import { getRevisionActions } from '../../actions/getRevisionActions';

const RevisionsPage: React.FC<React.ComponentProps<typeof DetailsPage>> = (props) => {
const pages = [navFactory.details(DetailsForKind(props.kind)), navFactory.editYaml()];
const menuActionsCreator = (kindObj: K8sKind, obj: K8sResourceKind) =>
getRevisionActions().map((action) => action(kindObj, obj));

return <DetailsPage {...props} pages={pages} menuActions={menuActionsCreator} />;
};

export default RevisionsPage;
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import * as React from 'react';
import * as cx from 'classnames';
import * as _ from 'lodash';
import { TableRow, TableData, RowFunction } from '@console/internal/components/factory';
import { Kebab, ResourceLink, ResourceKebab, Timestamp } from '@console/internal/components/utils';
import { ResourceLink, ResourceKebab, Timestamp } from '@console/internal/components/utils';
import { referenceForModel } from '@console/internal/module/k8s';
import { RevisionModel, ServiceModel } from '../../models';
import { getConditionString, getCondition } from '../../utils/condition-utils';
import { RevisionKind, ConditionTypes } from '../../types';
import { tableColumnClasses } from './revision-table';
import { getRevisionActions } from '../../actions/getRevisionActions';

const revisionReference = referenceForModel(RevisionModel);
const serviceReference = referenceForModel(ServiceModel);
Expand Down Expand Up @@ -53,7 +54,7 @@ const RevisionRow: RowFunction<RevisionKind> = ({ obj, index, key, style }) => {
{(readyCondition && readyCondition.message) || '-'}
</TableData>
<TableData className={tableColumnClasses[7]}>
<ResourceKebab actions={Kebab.factory.common} kind={revisionReference} resource={obj} />
<ResourceKebab actions={getRevisionActions()} kind={revisionReference} resource={obj} />
</TableData>
</TableRow>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface TrafficSplittingType {
percent: number;
tag: string;
revisionName: string;
};
}[];
}

const TrafficSplitting: React.FC<TrafficSplittingProps> = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react';
import { FormikProps, FormikValues } from 'formik';
import { TextInputTypes } from '@patternfly/react-core';
import { MultiColumnField, InputField, DropdownField } from '@console/shared';

export interface TrafficSplittingFieldProps {
revisionItems: any;
}

type Props = FormikProps<FormikValues> & TrafficSplittingFieldProps;

const TrafficSplittingFields: React.FC<Props> = ({ revisionItems, values }) => {
return (
<MultiColumnField
name="trafficSplitting"
addLabel="Add Revision"
headers={['Split', 'Tag', 'Revision']}
emptyValues={{ percent: '', tag: '', revisionName: '' }}
disableDeleteRow={values.trafficSplitting.length === 1}
>
<InputField
name="percent"
type={TextInputTypes.number}
placeholder="100"
style={{ maxWidth: '100%' }}
required
/>
<InputField name="tag" type={TextInputTypes.text} required />
<DropdownField
name="revisionName"
items={revisionItems}
title="Select a revision"
fullWidth
required
/>
</MultiColumnField>
);
};

export default TrafficSplittingFields;

0 comments on commit 5587585

Please sign in to comment.