Skip to content

Commit

Permalink
console,account: Apply various fixes to make cypress pass
Browse files Browse the repository at this point in the history
  • Loading branch information
kschiffer committed Jun 27, 2023
1 parent 26c307b commit e91742e
Show file tree
Hide file tree
Showing 20 changed files with 149 additions and 210 deletions.
67 changes: 13 additions & 54 deletions pkg/webui/components/prompt/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,68 +13,34 @@
// limitations under the License.

import React from 'react'
import { useNavigate } from 'react-router-dom'
import { unstable_usePrompt } from 'react-router-dom'

import PortalledModal from '@ttn-lw/components/modal/portalled'

import { usePrompt } from '@ttn-lw/lib/hooks/use-prompt'
import PropTypes from '@ttn-lw/lib/prop-types'

/*
* `<Prompt />` is used to prompt the user before navigating from the current page. This is
* helpful to avoid losing the state of the current page because of accidental misclick, for example,
* for half-filled forms.
*/
const Prompt = props => {
const { modal, children, when, shouldBlockNavigation, onApprove, onCancel } = props
const { modal, children, message, when } = props
const [showModal, setShowModal] = React.useState(false)

const navigate = useNavigate()

const [state, setState] = React.useState({
showModal: false,
nextLocation: undefined,
confirmedLocationChange: false,
})
const { showModal, nextLocation, confirmedLocationChange } = state

const handleModalShow = React.useCallback(nextLocation => {
setState(prev => ({ ...prev, showModal: true, nextLocation }))
}, [])

const handleModalHide = React.useCallback(() => {
setState(prev => ({ ...prev, showModal: false }))
}, [])
// The usage of `unstable_usePrompt` might change as the library updates.
const continueNavigation = unstable_usePrompt(when, message)

const handleModalComplete = React.useCallback(
approved => {
setState(prev => ({ ...prev, confirmedLocationChange: approved }))
handleModalHide()
},
[handleModalHide],
)

const handlePromptTrigger = React.useCallback(
location => {
if (!confirmedLocationChange && shouldBlockNavigation(location)) {
handleModalShow(location)

return false
setShowModal(false)
if (approved) {
continueNavigation()
}

return true
},
[handleModalShow, shouldBlockNavigation, confirmedLocationChange],
[continueNavigation],
)

usePrompt(handlePromptTrigger, when)

React.useEffect(() => {
if (confirmedLocationChange) {
onApprove(nextLocation, navigate)
} else {
onCancel(nextLocation, navigate)
if (when) {
setShowModal(true)
}
}, [confirmedLocationChange, navigate, nextLocation, onApprove, onCancel])
}, [when])

return (
<PortalledModal visible={showModal} {...modal} approval onComplete={handleModalComplete}>
Expand All @@ -85,20 +51,13 @@ const Prompt = props => {

Prompt.propTypes = {
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
message: PropTypes.string.isRequired,
modal: PropTypes.shape({ ...PortalledModal.Modal.propTypes }).isRequired,
onApprove: PropTypes.func,
onCancel: PropTypes.func,
shouldBlockNavigation: PropTypes.func,
when: PropTypes.bool.isRequired,
}

Prompt.defaultProps = {
children: undefined,
shouldBlockNavigation: () => true,
onApprove: (location, navigate) => {
navigate(location)
},
onCancel: () => null,
}

export default Prompt
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,9 @@ const validationSchema = Yup.object().shape({
.min(3, Yup.passValues(sharedMessages.validateTooShort))
.max(50, Yup.passValues(sharedMessages.validateTooLong)),
description: Yup.string().max(150, Yup.passValues(sharedMessages.validateTooLong)),
attributes: Yup.array()
attributes: Yup.object()
.nullable()
.max(10, Yup.passValues(sharedMessages.attributesValidateTooMany))
.test(
'has no empty string values',
sharedMessages.attributesValidateRequired,
attributeValidCheck,
)
.test('has no null values', sharedMessages.attributesValidateRequired, attributeValidCheck)
.test(
'has key length longer than 2',
sharedMessages.attributeKeyValidateTooShort,
Expand Down
15 changes: 11 additions & 4 deletions pkg/webui/console/components/payload-formatters-form/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { Col, Row } from 'react-grid-system'

import TYPES from '@console/constants/formatter-types'

import Prompt from '@ttn-lw/components/prompt'
import Select from '@ttn-lw/components/select'
import Form from '@ttn-lw/components/form'
import SubmitButton from '@ttn-lw/components/submit-button'
Expand Down Expand Up @@ -104,9 +103,10 @@ const validationSchema = Yup.object().shape({
.matches(addressRegexp, Yup.passValues(sharedMessages.validateAddressFormat))
.when(FIELD_NAMES.SELECT, {
is: TYPES.GRPC,
then: Yup.string()
.required(sharedMessages.validateRequired)
.max(40960, Yup.passValues(sharedMessages.validateTooLong)),
then: schema =>
schema
.required(sharedMessages.validateRequired)
.max(40960, Yup.passValues(sharedMessages.validateTooLong)),
}),
})

Expand Down Expand Up @@ -422,6 +422,12 @@ class PayloadFormattersForm extends React.Component {
/>
)}
{this.formatter}
{/*
// TODO: Refactor to use data API and re-enable prompt.
// NOTE: Unfortunately react router v6 requires us to do further
// refactoring to use the data API to be able to use `usePrompt`
// again, which is required to make the Prompt component work.
// For now we will disable the prompt.
<Prompt
when={Boolean(touched['javascript-formatter'] || touched['grpc-formatter'])}
modal={{
Expand All @@ -430,6 +436,7 @@ class PayloadFormattersForm extends React.Component {
buttonMessage: m.confirmNavigationTitle,
}}
/>
*/}
</>
)}
</Form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import { qosLevels } from './qos-options'
import providers from './providers'

export default Yup.object().shape({
pub_sub_id: Yup.string(),
pub_sub_id: Yup.string()
.matches(idRegexp, Yup.passValues(sharedMessages.validateIdFormat))
.required(sharedMessages.validateRequired)
.min(2, Yup.passValues(sharedMessages.validateTooShort))
.max(36, Yup.passValues(sharedMessages.validateTooLong)),
format: Yup.string().required(sharedMessages.validateRequired),
base_topic: Yup.string(),
nats: Yup.object().when('_provider', {
Expand Down
2 changes: 1 addition & 1 deletion pkg/webui/console/containers/api-key-form/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const CreateForm = ({ entity, entityId }) => {
const handleModalApprove = useCallback(async () => {
setModal(null)
// Navigate back to list
navigate('../')
navigate('..')
}, [navigate])

const handleCreate = useCallback(
Expand Down
2 changes: 1 addition & 1 deletion pkg/webui/console/containers/api-key-form/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const ApiKeyEditForm = ({ entity, entityId }) => {
message: m.deleteSuccess,
type: toast.types.SUCCESS,
})
navigate('../')
navigate('..')
}, [navigate])

const handleEditSuccess = useCallback(async () => {
Expand Down
50 changes: 23 additions & 27 deletions pkg/webui/console/lib/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import { id as idRegexp } from '@ttn-lw/lib/regexp'

export const mapFormValueToAttributes = formValue =>
export const encodeAttributes = formValue =>
(Array.isArray(formValue) &&
formValue.reduce(
(result, { key, value }) => ({
Expand All @@ -23,9 +23,9 @@ export const mapFormValueToAttributes = formValue =>
}),
{},
)) ||
null
undefined

export const mapAttributesToFormValue = attributesType =>
export const decodeAttributes = attributesType =>
(attributesType &&
Object.keys(attributesType).reduce(
(result, key) =>
Expand All @@ -37,30 +37,26 @@ export const mapAttributesToFormValue = attributesType =>
)) ||
[]

export const attributeValidCheck = attributes =>
attributes === undefined ||
attributes === null ||
(attributes instanceof Array &&
(attributes.length === 0 ||
attributes.every(attribute => Boolean(attribute.key) && Boolean(attribute.value))))
export const attributesCountCheck = object =>
object === undefined ||
object === null ||
(object instanceof Object && Object.keys(object).length <= 10)
export const attributeValidCheck = object =>
object === undefined ||
object === null ||
(object instanceof Object && Object.values(object).every(attribute => Boolean(attribute)))

export const attributeTooShortCheck = attributes =>
attributes === undefined ||
attributes === null ||
(attributes instanceof Array &&
(attributes.length === 0 ||
attributes.every(attribute => RegExp(idRegexp).test(attribute.key))))
export const attributeTooShortCheck = object =>
object === undefined ||
object === null ||
(object instanceof Object && Object.keys(object).every(key => RegExp(idRegexp).test(key)))

export const attributeKeyTooLongCheck = attributes =>
attributes === undefined ||
attributes === null ||
(attributes instanceof Array &&
(attributes.length === 0 ||
attributes.every(attribute => attribute.key && attribute.key.length <= 36)))
export const attributeKeyTooLongCheck = object =>
object === undefined ||
object === null ||
(object instanceof Object && Object.keys(object).every(key => key.length <= 36))

export const attributeValueTooLongCheck = attributes =>
attributes === undefined ||
attributes === null ||
(attributes instanceof Array &&
(attributes.length === 0 ||
attributes.every(attribute => attribute.value && attribute.value.length <= 200)))
export const attributeValueTooLongCheck = object =>
object === undefined ||
object === null ||
(object instanceof Object && Object.values(object).every(value => value.length <= 200))
2 changes: 1 addition & 1 deletion pkg/webui/console/views/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class ConsoleApp extends React.PureComponent {
<Route path="/gateways/*" Component={Gateways} />
<Route path="/organizations/*" Component={Organizations} />
<Route path="/admin/*" Component={Admin} />
<Route path="/user/" Component={User} />
<Route path="/user/*" Component={User} />
<Route path="*" Component={GenericNotFound} />
</Routes>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,10 @@ const webhookEntitySelector = [
const ApplicationWebhookEdit = () => {
const { appId, webhookId } = useParams()

const healthStatusEnabled = useSelector(selectWebhooksHealthStatusEnabled)
const hasUnhealthyWebhookConfig = useSelector(selectWebhookHasUnhealthyConfig)

return (
<Require featureCheck={healthStatusEnabled && !hasUnhealthyWebhookConfig}>
<RequireRequest requestAction={getWebhook(appId, webhookId, webhookEntitySelector)}>
<ApplicationWebhookEditInner />
</RequireRequest>
</Require>
<RequireRequest requestAction={getWebhook(appId, webhookId, webhookEntitySelector)}>
<ApplicationWebhookEditInner />
</RequireRequest>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ const ApplicationWebhooksInner = () => {
<Route index Component={ApplicationWebhooksList} />
<Route path="add" Component={ApplicationWebhookAdd} />
<Route
exact
path=":webhookId"
component={
element={
<ValidateRouteParam
check={{ webhookId: pathIdRegexp }}
Component={ApplicationWebhookEdit}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import PropTypes from '@ttn-lw/lib/prop-types'
import sharedMessages from '@ttn-lw/lib/shared-messages'
import tooltipIds from '@ttn-lw/lib/constants/tooltip-ids'

import { mapFormValueToAttributes, mapAttributesToFormValue } from '@console/lib/attributes'
import { encodeAttributes, decodeAttributes } from '@console/lib/attributes'
import { parseLorawanMacVersion } from '@console/lib/device-utils'

import { hasExternalJs, isDeviceOTAA } from '../utils'
Expand Down Expand Up @@ -86,7 +86,7 @@ const IdentityServerForm = React.memo(props => {
const initialValues = {
...device,
_external_js: hasExternalJs(device),
attributes: mapAttributesToFormValue(device.attributes),
attributes: device.attributes,
}

return validationSchema.cast(initialValues, { context: validationContext })
Expand Down Expand Up @@ -115,7 +115,7 @@ const IdentityServerForm = React.memo(props => {
const onFormSubmit = React.useCallback(
async (values, { resetForm, setSubmitting }) => {
const castedValues = validationSchema.cast(values, { context: validationContext })
const attributes = mapFormValueToAttributes(values.attributes)
const { attributes } = values

if (isEqual(initialValues.attributes || {}, attributes)) {
delete castedValues.attributes
Expand Down Expand Up @@ -286,6 +286,8 @@ const IdentityServerForm = React.memo(props => {
addMessage={sharedMessages.addAttributes}
component={KeyValueMap}
description={sharedMessages.attributeDescription}
encode={encodeAttributes}
decode={decodeAttributes}
/>
<SubmitBar>
<Form.Submit component={SubmitButton} message={sharedMessages.saveChanges} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
attributeTooShortCheck,
attributeKeyTooLongCheck,
attributeValueTooLongCheck,
attributesCountCheck,
} from '@console/lib/attributes'
import { address as addressRegexp } from '@console/lib/regexp'
import { parseLorawanMacVersion, generate16BytesKey } from '@console/lib/device-utils'
Expand Down Expand Up @@ -110,13 +111,14 @@ const validationSchema = Yup.object()
})
},
),
attributes: Yup.array()
.max(10, Yup.passValues(sharedMessages.attributesValidateTooMany))
attributes: Yup.object()
.nullable()
.test(
'has no empty string values',
sharedMessages.attributesValidateRequired,
attributeValidCheck,
'has no more than 10 keys',
sharedMessages.attributesValidateTooMany,
attributesCountCheck,
)
.test('has no null values', sharedMessages.attributesValidateRequired, attributeValidCheck)
.test(
'has key length longer than 2',
sharedMessages.attributeKeyValidateTooShort,
Expand Down
Loading

0 comments on commit e91742e

Please sign in to comment.