diff --git a/package.json b/package.json index 0f454bb0..c612350d 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "react-router-dom-v5-compat": "6.22.3", "sort-package-json": "^2.10.0", "style-loader": "^3.3.1", + "css-loader": "^6.7.1", "stylelint": "^15.3.0", "stylelint-config-prettier": "9.0.3", "stylelint-config-standard": "^31.0.0", diff --git a/src/brokers/add-broker/AddBroker.component.test.tsx b/src/brokers/add-broker/AddBroker.component.test.tsx new file mode 100644 index 00000000..757e8e1a --- /dev/null +++ b/src/brokers/add-broker/AddBroker.component.test.tsx @@ -0,0 +1,129 @@ +import { FC, useReducer } from 'react'; +import { + BrokerCreationFormDispatch, + BrokerCreationFormState, + artemisCrReducer, + newArtemisCRState, +} from '../../reducers/7.12/reducer'; +import { fireEvent, render, screen } from '../../test-utils'; +import { AddBroker } from './AddBroker.component'; +import { useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; + +jest.mock('@openshift-console/dynamic-plugin-sdk', () => ({ + useAccessReview: jest.fn(() => []), +})); + +const mockUseAccessReview = useAccessReview as jest.Mock; + +const SimplifiedCreaterBrokerPage: FC = () => { + const onSubmit = () => { + return 0; + }; + const onCancel = () => { + return 0; + }; + const initialValues = newArtemisCRState('default'); + const [brokerModel, dispatch] = useReducer(artemisCrReducer, initialValues); + return ( + + + + + + ); +}; + +const SimplifiedUpdateBrokerPage: FC = () => { + const onSubmit = () => { + return 0; + }; + const onCancel = () => { + return 0; + }; + const reloadExisting = () => { + return 0; + }; + const initialValues = newArtemisCRState('default'); + const [brokerModel, dispatch] = useReducer(artemisCrReducer, initialValues); + return ( + + + + + + ); +}; + +describe('create broker', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseAccessReview.mockReturnValue([true, false]); + }); + it('clicking on cancel after making some changes displays a warning', async () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /plus/i })); + fireEvent.click(screen.getByRole('button', { name: /cancel/i })); + expect( + screen.getByText( + "You are about to quit the editor, the broker won't get created", + ), + ).toBeInTheDocument(); + }); + it("clicking on cancel immediately after opening the editor shouldn't display a warning", async () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /cancel/i })); + expect( + screen.queryByText( + "You are about to quit the editor, the broker won't get created", + ), + ).not.toBeInTheDocument(); + }); +}); + +describe('update broker', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockUseAccessReview.mockReturnValue([true, false]); + }); + it('clicking on cancel after making some changes displays a warning', async () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /plus/i })); + fireEvent.click(screen.getByRole('button', { name: /cancel/i })); + expect( + screen.queryByText( + 'You are about to quit the editor, configuration that is not applied will be lost', + ), + ).toBeInTheDocument(); + }); + it("clicking on cancel immediately after opening the editor shouldn't display a warning", async () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /cancel/i })); + expect( + screen.queryByText( + 'You are about to quit the editor, configuration that is not applied will be lost', + ), + ).not.toBeInTheDocument(); + }); + it('clicking on reload after making some changes displays a warning', async () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /plus/i })); + fireEvent.click(screen.getByRole('button', { name: /reload/i })); + expect( + screen.queryByText('Upon reloading, these modifications will be lost.'), + ).toBeInTheDocument(); + }); + it("clicking on reload immediately after opening the editor shouldn't display a warning", async () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /reload/i })); + expect( + screen.queryByText('Upon reloading, these modifications will be lost.'), + ).not.toBeInTheDocument(); + }); +}); diff --git a/src/brokers/add-broker/AddBroker.component.tsx b/src/brokers/add-broker/AddBroker.component.tsx index 321b95bb..bc1aa7b0 100644 --- a/src/brokers/add-broker/AddBroker.component.tsx +++ b/src/brokers/add-broker/AddBroker.component.tsx @@ -1,68 +1,212 @@ -import { FC, useContext } from 'react'; -import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; -import { AlertVariant, Divider } from '@patternfly/react-core'; +import { FC, useContext, useState } from 'react'; +import { + ActionGroup, + Alert, + AlertVariant, + Button, + ButtonVariant, + Divider, + Form, + FormFieldGroup, + Modal, + ModalVariant, +} from '@patternfly/react-core'; import { ArtemisReducerOperations, BrokerCreationFormDispatch, BrokerCreationFormState, EditorType, } from '../../reducers/7.12/reducer'; -import { useLocation } from 'react-router-dom-v5-compat'; import { FormView } from '../../shared-components/FormView/FormView'; -import { YamlEditorView } from '../../shared-components/YamlEditorView/YamlEditorView'; import { EditorToggle } from './components/EditorToggle/EditorToggle'; +import { Loading } from '../../shared-components/Loading/Loading'; +import { useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; +import { AMQBrokerModel } from '../../k8s/models'; +import { useTranslation } from '../../i18n/i18n'; +import { YamlEditorView } from '../../shared-components/YamlEditorView/YamlEditorView'; -type AddBrokerProps = { - onCreateBroker: (data?: K8sResourceCommon) => void; - notification: { - title: string; - variant: AlertVariant; - }; - isUpdate?: boolean; +type AddBrokerPropTypes = { + onSubmit: () => void; + onCancel: () => void; + isUpdatingExisting?: boolean; + reloadExisting?: () => void; }; -export const AddBroker: FC = ({ - onCreateBroker, - notification, - isUpdate, +export const AddBroker: FC = ({ + onSubmit, + onCancel: doQuit, + isUpdatingExisting, + reloadExisting, }) => { const formValues = useContext(BrokerCreationFormState); const dispatch = useContext(BrokerCreationFormDispatch); - const location = useLocation(); - const params = new URLSearchParams(location.search); - const returnUrl = params.get('returnUrl') || '/k8s/all-namespaces/brokers'; const { editorType } = formValues; + const [userWantsToQuit, setUserWantsToQuit] = useState(false); + const [userWantsToReload, setUserWantsToReload] = useState(false); + + const [pendingActionQuittingYAMLView, setPendingActionQuittingYAMLView] = + useState<'switch' | 'submit'>('switch'); + const [wantsToQuitYamlView, setWantsToQuitYamlView] = useState(false); const onSelectEditorType = (editorType: EditorType) => { - dispatch({ - operation: ArtemisReducerOperations.setEditorType, - payload: editorType, - }); + if (formValues.editorType === EditorType.YAML) { + if (editorType === EditorType.BROKER) { + setWantsToQuitYamlView(true); + } + setPendingActionQuittingYAMLView('switch'); + } else { + dispatch({ + operation: ArtemisReducerOperations.setEditorType, + payload: EditorType.YAML, + }); + } + }; + const [triggerDelayedSubmit, setTriggerDelayedSubmit] = useState(false); + const [prevTriggerDelayedSubmit, setPrevTriggerDelayedSubmit] = + useState(triggerDelayedSubmit); + const onQuittingYamlView = () => { + if (pendingActionQuittingYAMLView === 'switch') { + setWantsToQuitYamlView(false); + dispatch({ + operation: ArtemisReducerOperations.setEditorType, + payload: EditorType.BROKER, + }); + } + if (pendingActionQuittingYAMLView === 'submit') { + setWantsToQuitYamlView(false); + setTriggerDelayedSubmit(true); + } }; + if (triggerDelayedSubmit !== prevTriggerDelayedSubmit) { + if (triggerDelayedSubmit) { + setTriggerDelayedSubmit(false); + onSubmit(); + } + setPrevTriggerDelayedSubmit(triggerDelayedSubmit); + } + const { t } = useTranslation(); + const namespace = formValues.cr.metadata.namespace; + const [canCreateBroker, loadingAccessReview] = useAccessReview({ + group: AMQBrokerModel.apiGroup, + resource: AMQBrokerModel.plural, + namespace, + verb: 'create', + }); + if (loadingAccessReview) return ; + if (!canCreateBroker) { + return ( + + {t('you_do_not_have_write_access')} + + ); + } return ( <> + setUserWantsToQuit(false)} + actions={[ + , + , + ]} + > + You are about to quit the editor,{' '} + {isUpdatingExisting + ? 'configuration that is not applied will be lost' + : "the broker won't get created"} + + setUserWantsToReload(false)} + actions={[ + , + , + ]} + > + Upon reloading, these modifications will be lost. + - {editorType === EditorType.BROKER && ( - - )} + {editorType === EditorType.BROKER && } {editorType === EditorType.YAML && ( setWantsToQuitYamlView(false)} /> )} +
+ + + + {isUpdatingExisting && ( + + )} + + + +
); }; diff --git a/src/brokers/add-broker/AddBroker.container.tsx b/src/brokers/add-broker/AddBroker.container.tsx index 4b7883b5..7202d8d1 100644 --- a/src/brokers/add-broker/AddBroker.container.tsx +++ b/src/brokers/add-broker/AddBroker.container.tsx @@ -1,6 +1,6 @@ import { FC, useReducer, useState } from 'react'; import { k8sCreate } from '@openshift-console/dynamic-plugin-sdk'; -import { AlertVariant } from '@patternfly/react-core'; +import { Alert, AlertVariant } from '@patternfly/react-core'; import { AddBroker } from './AddBroker.component'; import { AMQBrokerModel } from '../../k8s/models'; import { BrokerCR } from '../../k8s/types'; @@ -23,26 +23,32 @@ export const AddBrokerPage: FC = () => { const navigate = useNavigate(); const { ns: namespace } = useParams<{ ns?: string }>(); - const defaultNotification = { title: '', variant: AlertVariant.info }; - const initialValues = newArtemisCRState(namespace); //states const [brokerModel, dispatch] = useReducer(artemisCrReducer, initialValues); - const [notification, setNotification] = useState(defaultNotification); + const params = new URLSearchParams(location.search); + const returnUrl = params.get('returnUrl') || '/k8s/all-namespaces/brokers'; const handleRedirect = () => { - navigate('/k8s/all-namespaces/brokers'); + navigate(returnUrl); }; + const [hasBrokerUpdated, setHasBrokerUpdated] = useState(false); + const [alert, setAlert] = useState(''); const k8sCreateBroker = (content: BrokerCR) => { k8sCreate({ model: AMQBrokerModel, data: content }) - .then(() => { - setNotification(defaultNotification); - handleRedirect(); - }) + .then( + () => { + setAlert(''); + setHasBrokerUpdated(true); + }, + (reason: Error) => { + setAlert(reason.message); + }, + ) .catch((e) => { - setNotification({ title: e.message, variant: AlertVariant.danger }); + setAlert(e.message); }); }; @@ -60,18 +66,33 @@ export const AddBrokerPage: FC = () => { if (!isLoading && !isDomainSet) { dispatch({ operation: ArtemisReducerOperations.setIngressDomain, - payload: clusterDomain, + payload: { + ingressUrl: clusterDomain, + isSetByUser: false, + }, }); setIsDomainSet(true); } + if (hasBrokerUpdated && alert === '') { + handleRedirect(); + } + return ( + {alert !== '' && ( + + )} k8sCreateBroker(brokerModel.cr)} + onCancel={handleRedirect} /> diff --git a/src/brokers/update-broker/UpdateBroker.container.tsx b/src/brokers/update-broker/UpdateBroker.container.tsx index 7c09aea4..412bb773 100644 --- a/src/brokers/update-broker/UpdateBroker.container.tsx +++ b/src/brokers/update-broker/UpdateBroker.container.tsx @@ -1,6 +1,6 @@ import { FC, useState, useEffect, useReducer } from 'react'; import { k8sGet, k8sUpdate } from '@openshift-console/dynamic-plugin-sdk'; -import { AlertVariant } from '@patternfly/react-core'; +import { Alert, AlertVariant } from '@patternfly/react-core'; import { AddBroker } from '../add-broker/AddBroker.component'; import { Loading } from '../../shared-components/Loading/Loading'; import { AMQBrokerModel } from '../../k8s/models'; @@ -13,20 +13,26 @@ import { artemisCrReducer, getArtemisCRState, } from '../../reducers/7.12/reducer'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useNavigate, useParams } from 'react-router-dom-v5-compat'; export const UpdateBrokerPage: FC = () => { + const navigate = useNavigate(); const { ns: namespace, name } = useParams<{ ns?: string; name?: string }>(); - const defaultNotification = { title: '', variant: AlertVariant.info }; //states - const [notification, setNotification] = useState(defaultNotification); const [loadingBrokerCR, setLoading] = useState(false); const crState = getArtemisCRState(name, namespace); const [brokerModel, dispatch] = useReducer(artemisCrReducer, crState); + const [hasBrokerUpdated, setHasBrokerUpdated] = useState(false); + const [alert, setAlert] = useState(''); + const params = new URLSearchParams(location.search); + const returnUrl = params.get('returnUrl') || '/k8s/all-namespaces/brokers'; + const handleRedirect = () => { + navigate(returnUrl); + }; const k8sUpdateBroker = (content: BrokerCR) => { k8sUpdate({ model: AMQBrokerModel, @@ -34,16 +40,17 @@ export const UpdateBrokerPage: FC = () => { ns: namespace, name: name, }) - .then((response: BrokerCR) => { - const name = response.metadata.name; - const resourceVersion = response.metadata.resourceVersion; - setNotification({ - title: `${name} has been updated to version ${resourceVersion}`, - variant: AlertVariant.success, - }); - }) - .catch((e) => { - setNotification({ title: e.message, variant: AlertVariant.danger }); + .then( + () => { + setAlert(''); + setHasBrokerUpdated(true); + }, + (reason: Error) => { + setAlert(reason.message); + }, + ) + .catch((e: Error) => { + setAlert(e.message); }); }; @@ -53,13 +60,11 @@ export const UpdateBrokerPage: FC = () => { .then((broker: BrokerCR) => { dispatch({ operation: ArtemisReducerOperations.setModel, - payload: { - model: broker, - }, + payload: { model: broker, isSetByUser: false }, }); }) .catch((e) => { - setNotification({ title: e.message, variant: AlertVariant.danger }); + setAlert(e.message); }) .finally(() => { setLoading(false); @@ -76,24 +81,41 @@ export const UpdateBrokerPage: FC = () => { if (!loadingBrokerCR && !isLoadingClusterDomain && !isDomainSet) { dispatch({ operation: ArtemisReducerOperations.setIngressDomain, - payload: clusterDomain, + payload: { + ingressUrl: clusterDomain, + isSetByUser: false, + }, }); setIsDomainSet(true); } - if (loadingBrokerCR && !notification.title) return ; + if (loadingBrokerCR && !alert) return ; if (!brokerModel.cr.spec?.deploymentPlan) { return ; } + if (hasBrokerUpdated && alert === '') { + handleRedirect(); + } + return ( + {alert !== '' && ( + + )} k8sUpdateBroker(brokerModel.cr)} + onCancel={handleRedirect} + isUpdatingExisting + reloadExisting={k8sGetBroker} /> diff --git a/src/reducers/7.12/import-types.ts b/src/reducers/7.12/import-types.ts index 9a04df6a..0d064441 100644 --- a/src/reducers/7.12/import-types.ts +++ b/src/reducers/7.12/import-types.ts @@ -4,5 +4,7 @@ import { EditorType } from './reducer'; export interface AddBrokerResourceValues { shouldShowYAMLMessage?: boolean; editorType?: EditorType; + yamlHasUnsavedChanges?: boolean; + hasChanges?: boolean; cr?: BrokerCR; } diff --git a/src/reducers/7.12/reducer.test.ts b/src/reducers/7.12/reducer.test.ts index 75ec89c4..96d9e1b5 100644 --- a/src/reducers/7.12/reducer.test.ts +++ b/src/reducers/7.12/reducer.test.ts @@ -1104,4 +1104,40 @@ describe('test the creation broker reducer', () => { }); expect(stateExposeModeIngress.cr.spec.acceptors[0].expose).toBe(true); }); + it('test setYamlHasUnsavedChanges,', () => { + const initialState = newArtemisCRState('namespace'); + const updatedState = artemisCrReducer(initialState, { + operation: ArtemisReducerOperations.setYamlHasUnsavedChanges, + }); + expect(updatedState.yamlHasUnsavedChanges).toBe(true); + expect(updatedState.hasChanges).toBe(false); + }); + it('test machine controlled model update resets the changed flags,', () => { + const initialState = newArtemisCRState('namespace'); + const updatedState = artemisCrReducer(initialState, { + operation: ArtemisReducerOperations.setModel, + payload: { + model: initialState.cr, + isSetByUser: false, + }, + }); + expect(updatedState.yamlHasUnsavedChanges).toBe(false); + expect(updatedState.hasChanges).toBe(false); + }); + it('test user controlled model update updates the flags correctly', () => { + const initialState = newArtemisCRState('namespace'); + let updatedState = artemisCrReducer(initialState, { + operation: ArtemisReducerOperations.setYamlHasUnsavedChanges, + }); + expect(updatedState.hasChanges).toBe(false); + updatedState = artemisCrReducer(updatedState, { + operation: ArtemisReducerOperations.setModel, + payload: { + model: initialState.cr, + isSetByUser: true, + }, + }); + expect(updatedState.yamlHasUnsavedChanges).toBe(false); + expect(updatedState.hasChanges).toBe(true); + }); }); diff --git a/src/reducers/7.12/reducer.ts b/src/reducers/7.12/reducer.ts index e9fa8d81..b296b058 100644 --- a/src/reducers/7.12/reducer.ts +++ b/src/reducers/7.12/reducer.ts @@ -36,7 +36,10 @@ export const getArtemisCRState = (name: string, ns: string): FormState => { const key = name + ns; let formState = artemisCRStateMap.get(key); if (!formState) { - formState = {}; + formState = { + yamlHasUnsavedChanges: false, + hasChanges: false, + }; formState.shouldShowYAMLMessage = true; formState.editorType = EditorType.BROKER; artemisCRStateMap.set(key, formState); @@ -73,6 +76,8 @@ export const newArtemisCRState = (namespace: string): FormState => { shouldShowYAMLMessage: true, editorType: EditorType.BROKER, cr: initialCr, + hasChanges: false, + yamlHasUnsavedChanges: false, }; }; @@ -178,6 +183,11 @@ export enum ArtemisReducerOperations { setNamespace, /** update the total number of replicas */ setReplicasNumber, + /** + * Tells that the yaml editor has unsaved changes, when the setModel is + * invoked, the flag is reset to false. + */ + setYamlHasUnsavedChanges, /** Updates the configuration's factory Class */ updateAcceptorFactoryClass, /** Update the issuer of an annotation */ @@ -229,10 +239,14 @@ type ArtemisReducerActions = | SetModelAction | SetNamespaceAction | SetReplicasNumberAction + | SetYamlHasUnsavedChanges | UpdateAcceptorFactoryClassAction | UpdateAnnotationIssuerAction | UpdateConnectorFactoryClassAction; +interface SetYamlHasUnsavedChanges extends ArtemisReducerActionBase { + operation: ArtemisReducerOperations.setYamlHasUnsavedChanges; +} interface UpdateAnnotationIssuerAction extends ArtemisReducerActionBase { operation: ArtemisReducerOperations.updateAnnotationIssuer; payload: { @@ -406,6 +420,9 @@ interface SetModelAction extends ArtemisReducerActionBase { operation: ArtemisReducerOperations.setModel; payload: { model: BrokerCR; + /** setting this to true means that form state will get considered as + * modified, setting to false reset that status.*/ + isSetByUser?: boolean; }; } @@ -569,8 +586,16 @@ interface SetReplicasNumberAction extends ArtemisReducerActionBase { interface SetIngressDomainAction extends ArtemisReducerActionBase { operation: ArtemisReducerOperations.setIngressDomain; - // the domain of the cluster - payload: string; + /** the domain of the cluster. Only passing the string is equivalent as saying + * that the value is set by the user. Otherwise this value can be customized. + * Setting isSetByUser to false has for effect to state that the form doesn't + * have changes, since the change is done by the system instead of the user.*/ + payload: + | string + | { + ingressUrl: string; + isSetByUser?: boolean; + }; } /** * @@ -584,9 +609,18 @@ export const artemisCrReducer: React.Reducer< ArtemisReducerActions > = (prevFormState, action) => { const formState = { ...prevFormState }; + if ( + action.operation !== ArtemisReducerOperations.setEditorType && + action.operation !== ArtemisReducerOperations.setYamlHasUnsavedChanges + ) { + formState.hasChanges = true; + } // set the individual fields switch (action.operation) { + case ArtemisReducerOperations.setYamlHasUnsavedChanges: + formState.yamlHasUnsavedChanges = true; + break; case ArtemisReducerOperations.updateAnnotationIssuer: updateAnnotationIssuer( formState.cr, @@ -612,6 +646,9 @@ export const artemisCrReducer: React.Reducer< break; case ArtemisReducerOperations.setEditorType: formState.editorType = action.payload; + if (formState.editorType === EditorType.BROKER) { + formState.yamlHasUnsavedChanges = false; + } break; case ArtemisReducerOperations.setNamespace: updateNamespace(formState.cr, action.payload); @@ -842,9 +879,16 @@ export const artemisCrReducer: React.Reducer< break; case ArtemisReducerOperations.setModel: setModel(formState, action.payload.model); + formState.yamlHasUnsavedChanges = false; + formState.hasChanges = action.payload.isSetByUser; break; case ArtemisReducerOperations.setIngressDomain: - updateIngressDomain(formState.cr, action.payload); + if (typeof action.payload === 'string') { + updateIngressDomain(formState.cr, action.payload); + } else { + updateIngressDomain(formState.cr, action.payload.ingressUrl); + formState.hasChanges = action.payload.isSetByUser; + } break; default: throw Error('Unknown action: ' + action); diff --git a/src/shared-components/FormView/FormView.tsx b/src/shared-components/FormView/FormView.tsx index 2bb039c3..b7c22c66 100644 --- a/src/shared-components/FormView/FormView.tsx +++ b/src/shared-components/FormView/FormView.tsx @@ -1,12 +1,5 @@ -import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; import { - ActionGroup, - Alert, - AlertGroup, - AlertVariant, Banner, - Button, - ButtonVariant, Form, FormFieldGroup, FormGroup, @@ -20,8 +13,7 @@ import { TextInput, InputGroupItem, } from '@patternfly/react-core'; -import { FC, useContext, useEffect, useState } from 'react'; -import { useTranslation } from '../../i18n/i18n'; +import { FC, useContext, useState } from 'react'; import { ArtemisReducerOperations, BrokerCreationFormDispatch, @@ -31,68 +23,13 @@ import { BrokerProperties, BrokerPropertiesList, } from './BrokerProperties/BrokerProperties'; -import { useNavigate } from 'react-router-dom-v5-compat'; - -type FormViewProps = { - onCreateBroker: (formValues: K8sResourceCommon) => void; - notification: { - title: string; - variant: AlertVariant; - }; - isUpdate: boolean; - returnUrl: string; -}; - -export const FormView: FC = ({ - onCreateBroker, - notification: serverNotification, - isUpdate, - returnUrl, -}) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const defaultNotification = { title: '', variant: AlertVariant.custom }; - - //states - const [notification, setNotification] = useState(defaultNotification); +export const FormView: FC = () => { const formState = useContext(BrokerCreationFormState); const { cr } = useContext(BrokerCreationFormState); const targetNs = cr.metadata.namespace; const dispatch = useContext(BrokerCreationFormDispatch); - useEffect(() => { - setNotification(serverNotification); - }, [serverNotification]); - - const validateFormFields = (formValues: K8sResourceCommon) => { - const name = formValues.metadata.name; - const regex = - /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/i; - if (!regex.test(name)) { - setNotification({ - title: t('form_view_validation_info'), - variant: AlertVariant.danger, - }); - return false; - } else { - setNotification({ title: 'ok', variant: AlertVariant.success }); - return true; - } - }; - - const onSubmit = () => { - const isValid = validateFormFields(formState.cr); - if (isValid) { - onCreateBroker(formState.cr); - navigate(returnUrl); - } - }; - - const onCancel = () => { - navigate(returnUrl); - }; - const handleNameChange = (name: string) => { dispatch({ operation: ArtemisReducerOperations.setBrokerName, @@ -125,17 +62,6 @@ export const FormView: FC = ({ return ( <>
- {notification.title && ( - - - - )} = ({ )}
-
- - - - - - -
); }; diff --git a/src/shared-components/YamlEditorView/YamlEditorView.css b/src/shared-components/YamlEditorView/YamlEditorView.css new file mode 100644 index 00000000..467b0e5b --- /dev/null +++ b/src/shared-components/YamlEditorView/YamlEditorView.css @@ -0,0 +1,7 @@ +#reload-object{ + display: none; +} + +#cancel{ + display: none; +} diff --git a/src/shared-components/YamlEditorView/YamlEditorView.tsx b/src/shared-components/YamlEditorView/YamlEditorView.tsx index 9bb50ca1..17bb458d 100644 --- a/src/shared-components/YamlEditorView/YamlEditorView.tsx +++ b/src/shared-components/YamlEditorView/YamlEditorView.tsx @@ -1,149 +1,194 @@ -import { FC, Suspense, useContext } from 'react'; +import { FC, Suspense, useContext, useState } from 'react'; import { Alert, - AlertVariant, + AlertActionCloseButton, AlertGroup, - Page, - ActionGroup, + AlertProps, + AlertVariant, Button, + Hint, + HintBody, + Modal, + ModalVariant, + Page, + useInterval, } from '@patternfly/react-core'; -import { - CodeEditor, - useAccessReview, -} from '@openshift-console/dynamic-plugin-sdk'; -import { AMQBrokerModel } from '../../k8s/models'; -import { BrokerCR } from '../../k8s/types'; +import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; import { Loading } from '../../shared-components/Loading/Loading'; -import { useTranslation } from '../../i18n/i18n'; import { ArtemisReducerOperations, BrokerCreationFormDispatch, BrokerCreationFormState, } from '../../reducers/7.12/reducer'; -import YAML from 'yaml'; -import { useNavigate } from 'react-router-dom-v5-compat'; +import YAML, { YAMLParseError } from 'yaml'; +import './YamlEditorView.css'; -export type YamlEditorViewProps = { - onCreateBroker: (content: any) => void; - initialResourceYAML: BrokerCR; - notification: { - title: string; - variant: AlertVariant; - }; - isUpdate: boolean; - returnUrl: string; +type YamlEditorViewPropTypes = { + isAskingPermissionToClose: boolean; + permissionGranted: () => void; + permissionDenied: () => void; }; - -interface BrokerActionGroupProps { - isUpdate: boolean; - onSubmit: () => void; - onCancel: () => void; -} - -const BrokerActionGroup: FC = ({ - isUpdate, - onSubmit, - onCancel, +export const YamlEditorView: FC = ({ + isAskingPermissionToClose, + permissionGranted: permissionGranted, + permissionDenied, }) => { - const { t } = useTranslation(); + const formState = useContext(BrokerCreationFormState); + const dispatch = useContext(BrokerCreationFormDispatch); - return ( - - - - - ); -}; + const [isModalVisible, setIsModalVisible] = useState(false); -const YamlEditorView: FC = ({ - onCreateBroker, - notification, - isUpdate, - returnUrl, -}) => { - const { t } = useTranslation(); - const navigate = useNavigate(); + const [prevIsAskingPermissionToClose, setPrevIsAskingPermissionToClose] = + useState(isAskingPermissionToClose); + if (isAskingPermissionToClose !== prevIsAskingPermissionToClose) { + if (isAskingPermissionToClose) { + if (formState.yamlHasUnsavedChanges) { + setIsModalVisible(true); + } else { + permissionGranted(); + } + } + setPrevIsAskingPermissionToClose(isAskingPermissionToClose); + } - const fromState = useContext(BrokerCreationFormState); - const namespace = fromState.cr.metadata.namespace; - const dispatch = useContext(BrokerCreationFormDispatch); + const [currentYaml, setCurrentYaml] = useState(); + const [yamlParseError, setYamlParserError] = useState(); - const [canCreateBroker, loadingAccessReview] = useAccessReview({ - group: AMQBrokerModel.apiGroup, - resource: AMQBrokerModel.plural, - namespace, - verb: 'create', - }); + const getUniqueId = () => new Date().getTime(); - const onSave = () => { - const yamlData: BrokerCR = fromState.cr; - onCreateBroker(yamlData); - navigate(returnUrl); + const updateModel = (content: string) => { + try { + dispatch({ + operation: ArtemisReducerOperations.setModel, + payload: { model: YAML.parse(content), isSetByUser: true }, + }); + setYamlParserError(undefined); + addAlert('changes saved', 'success', getUniqueId()); + return true; + } catch (e) { + setYamlParserError(e as YAMLParseError); + return false; + } }; - const onCancel = () => { - navigate(returnUrl); - }; + const stringedFormState = YAML.stringify(formState.cr, null, ' '); + const [alerts, setAlerts] = useState[]>([]); - //event: contains information of changes - //value: contains full yaml model - const onChanges = (newValue: any, _event: any) => { - dispatch({ - operation: ArtemisReducerOperations.setModel, - payload: { model: YAML.parse(newValue) }, - }); + const addAlert = ( + title: string, + variant: AlertProps['variant'], + key: React.Key, + ) => { + setAlerts((prevAlerts) => [...prevAlerts, { title, variant, key }]); }; - if (loadingAccessReview) return ; - + const removeAlert = (key: React.Key) => { + const newAlerts = alerts.filter((alert) => alert.key !== key); + setAlerts(newAlerts); + }; + const removeLastAlert = () => { + const newAlerts = alerts.filter( + (_alert, i, alerts) => i !== alerts.length - 1, + ); + setAlerts(newAlerts); + }; + useInterval(removeLastAlert, alerts.length > 0 ? 2000 : null); return ( <> - {canCreateBroker ? ( - - {notification.title && ( - - - - )} - }> - - - - - ) : ( - + {yamlParseError !== undefined && ( + + )} + setIsModalVisible(false)} + actions={[ + , + , + , + ]} > - {t('you_do_not_have_write_access')} - - )} + The YAML editor contains pending modifications, manual saving is + required. + + {formState.yamlHasUnsavedChanges && ( + + + Any changes in the YAML view has to be manually saved to get taken + into consideration. + + + )} + + {alerts.map(({ key, variant, title }) => ( + removeAlert(key)} + /> + } + key={key} + /> + ))} + + }> + { + setCurrentYaml(newContent); + if (stringedFormState !== newContent) { + dispatch({ + operation: ArtemisReducerOperations.setYamlHasUnsavedChanges, + }); + } + }} + /> + + ); }; -export { YamlEditorView }; diff --git a/yarn.lock b/yarn.lock index bcef0973..d2c4c9f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3854,6 +3854,20 @@ css-functions-list@^3.2.1: resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.2.tgz#9a54c6dd8416ed25c1079cd88234e927526c1922" integrity sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ== +css-loader@^6.7.1: + version "6.11.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" + integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.33" + postcss-modules-extract-imports "^3.1.0" + postcss-modules-local-by-default "^4.0.5" + postcss-modules-scope "^3.2.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.5.4" + css-select@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" @@ -5926,6 +5940,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + ignore@^3.3.3, ignore@^3.3.5: version "3.3.10" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" @@ -8509,6 +8528,34 @@ postcss-media-query-parser@^0.2.3: resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" integrity sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig== +postcss-modules-extract-imports@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== + +postcss-modules-local-by-default@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f" + integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5" + integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + postcss-reporter@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-5.0.0.tgz#a14177fd1342829d291653f2786efd67110332c3" @@ -8568,12 +8615,20 @@ postcss-selector-parser@^6.0.13: cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.2.0: +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -8606,6 +8661,15 @@ postcss@^8.4.28: picocolors "^1.0.1" source-map-js "^1.2.0" +postcss@^8.4.33: + version "8.4.45" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.45.tgz#538d13d89a16ef71edbf75d895284ae06b79e603" + integrity sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -9582,6 +9646,11 @@ semver@^7.3.2, semver@^7.3.4, semver@^7.3.7, semver@^7.5.3, semver@^7.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== +semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"