Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cypress/e2e/console/gateways/create.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ describe('Gateway create', () => {
cy.findByLabelText('Gateway EUI').type(gateway.eui)
cy.findByRole('button', { name: 'Confirm' }).click()
cy.findByTestId('notification').should('be.visible')
cy.findByLabelText('Owner token').type('12345')
cy.get('input[name="authenticated_identifiers.authentication_code"]').type('12345')
cy.findByLabelText('Gateway ID').type(`eui-${gateway.eui}`)
cy.findByText('Frequency plan')
.parents('div[data-test-id="form-field"]')
Expand Down Expand Up @@ -239,7 +239,7 @@ describe('Gateway create', () => {
cy.findByTestId('notification').should('be.visible')
cy.findByLabelText('Frequency plan').selectOption(gateway.frequency_plan)
cy.findByLabelText('Gateway ID').type(`eui-${gateway.eui}`)
cy.findByLabelText('Owner token').type('12345')
cy.get('input[name="authenticated_identifiers.authentication_code"]').type('12345')
cy.findByRole('button', { name: 'Claim gateway' }).click()
cy.wait('@claim-request').its('request.body').should('deep.equal', expectedRequest)
cy.findByTestId('error-notification').should('not.exist')
Expand Down
15 changes: 13 additions & 2 deletions pkg/webui/components/form/field/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Copyright © 2022 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -77,6 +77,7 @@
component: Component,
decode,
description,
descriptionValues,
disabled: inputDisabled,
encode,
fieldWidth,
Expand All @@ -92,6 +93,7 @@
valueSetter,
onChange,
onBlur,
showTitle,
} = props

const {
Expand Down Expand Up @@ -222,7 +224,12 @@
<FieldError content={warning} title={title} warning id={describedBy} />
</div>
) : showDescription ? (
<Message className={style.description} content={description} id={describedBy} />
<Message
className={style.description}
content={description}
values={descriptionValues}
id={describedBy}
/>
) : null

const fieldComponentProps = {
Expand Down Expand Up @@ -253,7 +260,7 @@

return (
<div className={cls} data-needs-focus={showError} data-test-id="form-field">
{hasTitle && (
{hasTitle && showTitle && (
<div className={style.label}>
<Message
component="label"
Expand Down Expand Up @@ -290,6 +297,7 @@
]).isRequired,
decode: PropTypes.func,
description: PropTypes.message,
descriptionValues: PropTypes.shape({}),
disabled: PropTypes.bool,
encode: PropTypes.func,
fieldWidth: PropTypes.oneOf([
Expand All @@ -310,6 +318,7 @@
onChange: PropTypes.func,
readOnly: PropTypes.bool,
required: PropTypes.bool,
showTitle: PropTypes.bool,
title: PropTypes.message,
titleChildren: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
tooltip: PropTypes.message,
Expand Down Expand Up @@ -337,6 +346,8 @@
validate: undefined,
valueSetter: defaultValueSetter,
warning: '',
descriptionValues: {},
showTitle: true,
}

export default FormField
10 changes: 8 additions & 2 deletions pkg/webui/components/qr-modal-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const m = defineMessages({
})

const QRModalButton = props => {
const { message, onApprove, onCancel, onRead, qrData, invalidMessage } = props
const { message, onApprove, onCancel, onRead, qrData, invalidMessage, modalDataChildren } = props

const handleRead = useCallback(
val => {
Expand All @@ -53,7 +53,10 @@ const QRModalButton = props => {
<div style={{ width: '100%' }}>
{qrData.data ? (
qrData.valid ? (
<DataSheet data={qrData.data} />
<>
<DataSheet data={qrData.data} />
{modalDataChildren}
</>
) : (
<ErrorMessage content={invalidMessage} />
)
Expand Down Expand Up @@ -97,17 +100,20 @@ const QRModalButton = props => {
QRModalButton.propTypes = {
invalidMessage: PropTypes.message.isRequired,
message: PropTypes.message.isRequired,
modalDataChildren: PropTypes.node,
onApprove: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onRead: PropTypes.func.isRequired,
qrData: PropTypes.shape({
valid: PropTypes.bool,
data: PropTypes.arrayOf(PropTypes.shape()),
gateway: PropTypes.shape({}),
}),
}

QRModalButton.defaultProps = {
qrData: undefined,
modalDataChildren: null,
}

export default QRModalButton
4 changes: 4 additions & 0 deletions pkg/webui/components/tabs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const Tabs = ({
narrow,
toggleStyle,
disabled,
small,
}) => {
const handleClick = tabName => {
if (!disabled && onTabChange) {
Expand Down Expand Up @@ -67,6 +68,7 @@ const Tabs = ({
className={tabItemClassName}
toggleStyle={toggleStyle}
tooltip={description}
small={small}
>
{icon && <Icon icon={icon} className={style.icon} />}
<Message content={title} />
Expand All @@ -92,6 +94,7 @@ Tabs.propTypes = {
narrow: PropTypes.bool,
/** A list of tabs. */
onTabChange: PropTypes.func,
small: PropTypes.bool,
tabItemClassName: PropTypes.string,
tabs: PropTypes.arrayOf(
PropTypes.shape({
Expand All @@ -115,6 +118,7 @@ Tabs.defaultProps = {
narrow: false,
toggleStyle: false,
disabled: false,
small: false,
}

export default Tabs
4 changes: 4 additions & 0 deletions pkg/webui/components/tabs/tab/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const Tab = props => {
tabClassName,
toggleStyle,
tooltip,
small,
...rest
} = props

Expand All @@ -56,6 +57,7 @@ const Tab = props => {
[style.tabItemDisabled]: disabled,
[style.tabItemToggleStyle]: toggleStyle,
[style.tabItemToggleStyleActive]: toggleStyle && !disabled && active,
[style.small]: small,
})

// There is no support for disabled on anchors in html and hence in
Expand Down Expand Up @@ -113,6 +115,7 @@ Tab.propTypes = {
* name of the new active tab as an argument.
*/
onClick: PropTypes.func,
small: PropTypes.bool,
tabClassName: PropTypes.string,
/** A flag specifying whether the tab should render a toggle style. */
toggleStyle: PropTypes.bool,
Expand All @@ -132,6 +135,7 @@ Tab.defaultProps = {
exact: true,
toggleStyle: false,
tooltip: undefined,
small: false,
}

export default Tab
4 changes: 4 additions & 0 deletions pkg/webui/components/tabs/tab/tab.styl
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@
&-active
pseudo-border(3px, var(--c-text-brand-normal))
color: var(--c-text-neutral-heavy)

&.small
height: 29px
font-weight: $fw.bold
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Copyright © 2024 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import React, { useEffect } from 'react'
import React, { useCallback, useEffect } from 'react'
import { defineMessages } from 'react-intl'
import { useFormikContext } from 'formik'

Expand All @@ -24,6 +24,10 @@
import SubmitBar from '@ttn-lw/components/submit-bar'
import FormSubmit from '@ttn-lw/components/form/submit'
import SubmitButton from '@ttn-lw/components/submit-button'
import Link from '@ttn-lw/components/link'
import Tabs from '@ttn-lw/components/tabs'

import Message from '@ttn-lw/lib/components/message'

import { GsFrequencyPlansSelect as FrequencyPlansSelect } from '@console/containers/freq-plans-select'

Expand All @@ -33,10 +37,12 @@
import getHostFromUrl from '@ttn-lw/lib/host-from-url'

const { enabled: gsEnabled, base_url: gsBaseURL } = selectGsConfig()
const smUrl = 'https://accounts.thethingsindustries.com'

const m = defineMessages({
claimWarning:
'We detected that your gateway is a <strong>Managed Gateway</strong>. To claim this gateway, please use the owner token printed on the inside of the mounting lid or scan the QR code to claim instantly.',
'We detected a Managed gateway. To claim this gateway with a subscription, use the owner token printed on the gateway, or add it to your Gateway Fleet using your <Link>fleet owner token.</Link>',
fleet: 'Fleet',
})

const initialValues = {
Expand All @@ -49,10 +55,38 @@
target_gateway_server_address: gsEnabled ? getHostFromUrl(gsBaseURL) : '',
}

const ownerTokenTypes = [
{ name: 'gateway', title: sharedMessages.gateway },
{ name: 'fleet', title: m.fleet },
]

const GatewayClaimFormSection = () => {
const { values, addToFieldRegistry, removeFromFieldRegistry } = useFormikContext()
const { values, addToFieldRegistry, removeFromFieldRegistry, setValues } = useFormikContext()
const isManaged = values._inputMethod === 'managed'
const withQRdata = values._withQRdata
const isFleet = values._isFleet

const [activeOwnerTokenType, setActiveOwnerTokenType] = React.useState(
isFleet ? 'fleet' : 'gateway',
)

const onOwnerTokenTypeChange = useCallback(
value => {
setActiveOwnerTokenType(value)
setValues(values => ({
...values,
authenticated_identifiers: {
...values.authenticated_identifiers,
authentication_code:
value === 'fleet' && values._fleet_owner_token
? btoa(values._fleet_owner_token)
: value === 'gateway' && values._gtw_owner_token
? btoa(values._gtw_owner_token)
: '',
},
}))
},
[setValues],
)

// Register hidden fields so they don't get cleaned.
useEffect(() => {
Expand All @@ -64,28 +98,76 @@
return (
<>
{isManaged && (
<Form.InfoField>
<Notification
<>
<Form.InfoField>
<Notification
small
info
content={m.claimWarning}
messageValues={{
Link: val => (
<Link.Anchor
secondary
href="https://www.thethingsindustries.com/docs/hardware/gateways/models/thethingsindoorgatewaypro/#finding-your-owner-token"
external
>
{val}
</Link.Anchor>
),
}}
className="mb-0"
/>
</Form.InfoField>
<Message content={sharedMessages.ownerToken} className="fw-bold" />
<Tabs
active={activeOwnerTokenType}
tabs={ownerTokenTypes}
onTabChange={onOwnerTokenTypeChange}
toggleStyle
small
info
content={m.claimWarning}
messageValues={{
strong: txt => <strong>{txt}</strong>,
}}
className="mb-0"
className="w-content p-0 mb-cs-xs mt-cs-xxs border-none br-m gap-0 fs-s"
/>
</Form.InfoField>
{activeOwnerTokenType === 'fleet' && (
<Message
content={sharedMessages.fleetInfo}
values={{
Link: val => (
<Link.DocLink
secondary
path="/hardware/gateways/models/thethingsindoorgatewaypro/#subscription"
>
{val}
</Link.DocLink>
),
}}
className="mb-cs-xs c-text-neutral-light"
component="div"
/>
)}
</>
)}
<Form.Field
required
title={sharedMessages.ownerToken}
showTitle={!isManaged}
name="authenticated_identifiers.authentication_code"
tooltipId={tooltipIds.CLAIM_AUTH_CODE}
component={Input}
description={
isManaged && activeOwnerTokenType === 'fleet' ? sharedMessages.fleetTokenInfo : undefined
}
descriptionValues={{
Link: val => (
<Link.Anchor secondary href={`${smUrl}/dashboard/subscriptions?type=gateway`} external>
{val}
</Link.Anchor>
),
}}
encode={btoa}
decode={atob}
disabled={withQRdata}
sensitive
data-1p-ignore
data-lpignore
autoFocus
/>
<Form.Field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import registerValidationSchema from './gateway-registration-form-section/valida

export const validationSchema = Yup.object({
_owner_id: Yup.string(),
_isFleet: Yup.boolean(),
}).when('._inputMethod', {
is: 'register',
then: schema => schema.concat(registerValidationSchema),
Expand Down
Loading
Loading