Skip to content

Commit c0357c0

Browse files
authored
Merge pull request #7776 from TheThingsNetwork/feature/gateway-fleets-os
Add Gateway Fleet support in the Console OS
2 parents eebc896 + e59135d commit c0357c0

15 files changed

Lines changed: 341 additions & 40 deletions

File tree

cypress/e2e/console/gateways/create.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ describe('Gateway create', () => {
190190
cy.findByLabelText('Gateway EUI').type(gateway.eui)
191191
cy.findByRole('button', { name: 'Confirm' }).click()
192192
cy.findByTestId('notification').should('be.visible')
193-
cy.findByLabelText('Owner token').type('12345')
193+
cy.get('input[name="authenticated_identifiers.authentication_code"]').type('12345')
194194
cy.findByLabelText('Gateway ID').type(`eui-${gateway.eui}`)
195195
cy.findByText('Frequency plan')
196196
.parents('div[data-test-id="form-field"]')
@@ -239,7 +239,7 @@ describe('Gateway create', () => {
239239
cy.findByTestId('notification').should('be.visible')
240240
cy.findByLabelText('Frequency plan').selectOption(gateway.frequency_plan)
241241
cy.findByLabelText('Gateway ID').type(`eui-${gateway.eui}`)
242-
cy.findByLabelText('Owner token').type('12345')
242+
cy.get('input[name="authenticated_identifiers.authentication_code"]').type('12345')
243243
cy.findByRole('button', { name: 'Claim gateway' }).click()
244244
cy.wait('@claim-request').its('request.body').should('deep.equal', expectedRequest)
245245
cy.findByTestId('error-notification').should('not.exist')

pkg/webui/components/form/field/index.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const FormField = props => {
7777
component: Component,
7878
decode,
7979
description,
80+
descriptionValues,
8081
disabled: inputDisabled,
8182
encode,
8283
fieldWidth,
@@ -92,6 +93,7 @@ const FormField = props => {
9293
valueSetter,
9394
onChange,
9495
onBlur,
96+
showTitle,
9597
} = props
9698

9799
const {
@@ -222,7 +224,12 @@ const FormField = props => {
222224
<FieldError content={warning} title={title} warning id={describedBy} />
223225
</div>
224226
) : showDescription ? (
225-
<Message className={style.description} content={description} id={describedBy} />
227+
<Message
228+
className={style.description}
229+
content={description}
230+
values={descriptionValues}
231+
id={describedBy}
232+
/>
226233
) : null
227234

228235
const fieldComponentProps = {
@@ -253,7 +260,7 @@ const FormField = props => {
253260

254261
return (
255262
<div className={cls} data-needs-focus={showError} data-test-id="form-field">
256-
{hasTitle && (
263+
{hasTitle && showTitle && (
257264
<div className={style.label}>
258265
<Message
259266
component="label"
@@ -290,6 +297,7 @@ FormField.propTypes = {
290297
]).isRequired,
291298
decode: PropTypes.func,
292299
description: PropTypes.message,
300+
descriptionValues: PropTypes.shape({}),
293301
disabled: PropTypes.bool,
294302
encode: PropTypes.func,
295303
fieldWidth: PropTypes.oneOf([
@@ -310,6 +318,7 @@ FormField.propTypes = {
310318
onChange: PropTypes.func,
311319
readOnly: PropTypes.bool,
312320
required: PropTypes.bool,
321+
showTitle: PropTypes.bool,
313322
title: PropTypes.message,
314323
titleChildren: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
315324
tooltip: PropTypes.message,
@@ -337,6 +346,8 @@ FormField.defaultProps = {
337346
validate: undefined,
338347
valueSetter: defaultValueSetter,
339348
warning: '',
349+
descriptionValues: {},
350+
showTitle: true,
340351
}
341352

342353
export default FormField

pkg/webui/components/qr-modal-button/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const m = defineMessages({
4040
})
4141

4242
const QRModalButton = props => {
43-
const { message, onApprove, onCancel, onRead, qrData, invalidMessage } = props
43+
const { message, onApprove, onCancel, onRead, qrData, invalidMessage, modalDataChildren } = props
4444

4545
const handleRead = useCallback(
4646
val => {
@@ -53,7 +53,10 @@ const QRModalButton = props => {
5353
<div style={{ width: '100%' }}>
5454
{qrData.data ? (
5555
qrData.valid ? (
56-
<DataSheet data={qrData.data} />
56+
<>
57+
<DataSheet data={qrData.data} />
58+
{modalDataChildren}
59+
</>
5760
) : (
5861
<ErrorMessage content={invalidMessage} />
5962
)
@@ -97,17 +100,20 @@ const QRModalButton = props => {
97100
QRModalButton.propTypes = {
98101
invalidMessage: PropTypes.message.isRequired,
99102
message: PropTypes.message.isRequired,
103+
modalDataChildren: PropTypes.node,
100104
onApprove: PropTypes.func.isRequired,
101105
onCancel: PropTypes.func.isRequired,
102106
onRead: PropTypes.func.isRequired,
103107
qrData: PropTypes.shape({
104108
valid: PropTypes.bool,
105109
data: PropTypes.arrayOf(PropTypes.shape()),
110+
gateway: PropTypes.shape({}),
106111
}),
107112
}
108113

109114
QRModalButton.defaultProps = {
110115
qrData: undefined,
116+
modalDataChildren: null,
111117
}
112118

113119
export default QRModalButton

pkg/webui/components/tabs/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const Tabs = ({
3636
narrow,
3737
toggleStyle,
3838
disabled,
39+
small,
3940
}) => {
4041
const handleClick = tabName => {
4142
if (!disabled && onTabChange) {
@@ -67,6 +68,7 @@ const Tabs = ({
6768
className={tabItemClassName}
6869
toggleStyle={toggleStyle}
6970
tooltip={description}
71+
small={small}
7072
>
7173
{icon && <Icon icon={icon} className={style.icon} />}
7274
<Message content={title} />
@@ -92,6 +94,7 @@ Tabs.propTypes = {
9294
narrow: PropTypes.bool,
9395
/** A list of tabs. */
9496
onTabChange: PropTypes.func,
97+
small: PropTypes.bool,
9598
tabItemClassName: PropTypes.string,
9699
tabs: PropTypes.arrayOf(
97100
PropTypes.shape({
@@ -115,6 +118,7 @@ Tabs.defaultProps = {
115118
narrow: false,
116119
toggleStyle: false,
117120
disabled: false,
121+
small: false,
118122
}
119123

120124
export default Tabs

pkg/webui/components/tabs/tab/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const Tab = props => {
3838
tabClassName,
3939
toggleStyle,
4040
tooltip,
41+
small,
4142
...rest
4243
} = props
4344

@@ -56,6 +57,7 @@ const Tab = props => {
5657
[style.tabItemDisabled]: disabled,
5758
[style.tabItemToggleStyle]: toggleStyle,
5859
[style.tabItemToggleStyleActive]: toggleStyle && !disabled && active,
60+
[style.small]: small,
5961
})
6062

6163
// There is no support for disabled on anchors in html and hence in
@@ -113,6 +115,7 @@ Tab.propTypes = {
113115
* name of the new active tab as an argument.
114116
*/
115117
onClick: PropTypes.func,
118+
small: PropTypes.bool,
116119
tabClassName: PropTypes.string,
117120
/** A flag specifying whether the tab should render a toggle style. */
118121
toggleStyle: PropTypes.bool,
@@ -132,6 +135,7 @@ Tab.defaultProps = {
132135
exact: true,
133136
toggleStyle: false,
134137
tooltip: undefined,
138+
small: false,
135139
}
136140

137141
export default Tab

pkg/webui/components/tabs/tab/tab.styl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,7 @@
6868
&-active
6969
pseudo-border(3px, var(--c-text-brand-normal))
7070
color: var(--c-text-neutral-heavy)
71+
72+
&.small
73+
height: 29px
74+
font-weight: $fw.bold

pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/gateway-claim-form-section/index.js

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import React, { useEffect } from 'react'
15+
import React, { useCallback, useEffect } from 'react'
1616
import { defineMessages } from 'react-intl'
1717
import { useFormikContext } from 'formik'
1818

@@ -24,6 +24,10 @@ import Notification from '@ttn-lw/components/notification'
2424
import SubmitBar from '@ttn-lw/components/submit-bar'
2525
import FormSubmit from '@ttn-lw/components/form/submit'
2626
import SubmitButton from '@ttn-lw/components/submit-button'
27+
import Link from '@ttn-lw/components/link'
28+
import Tabs from '@ttn-lw/components/tabs'
29+
30+
import Message from '@ttn-lw/lib/components/message'
2731

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

@@ -33,10 +37,12 @@ import tooltipIds from '@ttn-lw/lib/constants/tooltip-ids'
3337
import getHostFromUrl from '@ttn-lw/lib/host-from-url'
3438

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

3742
const m = defineMessages({
3843
claimWarning:
39-
'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.',
44+
'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>',
45+
fleet: 'Fleet',
4046
})
4147

4248
const initialValues = {
@@ -49,10 +55,38 @@ const initialValues = {
4955
target_gateway_server_address: gsEnabled ? getHostFromUrl(gsBaseURL) : '',
5056
}
5157

58+
const ownerTokenTypes = [
59+
{ name: 'gateway', title: sharedMessages.gateway },
60+
{ name: 'fleet', title: m.fleet },
61+
]
62+
5263
const GatewayClaimFormSection = () => {
53-
const { values, addToFieldRegistry, removeFromFieldRegistry } = useFormikContext()
64+
const { values, addToFieldRegistry, removeFromFieldRegistry, setValues } = useFormikContext()
5465
const isManaged = values._inputMethod === 'managed'
55-
const withQRdata = values._withQRdata
66+
const isFleet = values._isFleet
67+
68+
const [activeOwnerTokenType, setActiveOwnerTokenType] = React.useState(
69+
isFleet ? 'fleet' : 'gateway',
70+
)
71+
72+
const onOwnerTokenTypeChange = useCallback(
73+
value => {
74+
setActiveOwnerTokenType(value)
75+
setValues(values => ({
76+
...values,
77+
authenticated_identifiers: {
78+
...values.authenticated_identifiers,
79+
authentication_code:
80+
value === 'fleet' && values._fleet_owner_token
81+
? btoa(values._fleet_owner_token)
82+
: value === 'gateway' && values._gtw_owner_token
83+
? btoa(values._gtw_owner_token)
84+
: '',
85+
},
86+
}))
87+
},
88+
[setValues],
89+
)
5690

5791
// Register hidden fields so they don't get cleaned.
5892
useEffect(() => {
@@ -64,28 +98,76 @@ const GatewayClaimFormSection = () => {
6498
return (
6599
<>
66100
{isManaged && (
67-
<Form.InfoField>
68-
<Notification
101+
<>
102+
<Form.InfoField>
103+
<Notification
104+
small
105+
info
106+
content={m.claimWarning}
107+
messageValues={{
108+
Link: val => (
109+
<Link.Anchor
110+
secondary
111+
href="https://www.thethingsindustries.com/docs/hardware/gateways/models/thethingsindoorgatewaypro/#finding-your-owner-token"
112+
external
113+
>
114+
{val}
115+
</Link.Anchor>
116+
),
117+
}}
118+
className="mb-0"
119+
/>
120+
</Form.InfoField>
121+
<Message content={sharedMessages.ownerToken} className="fw-bold" />
122+
<Tabs
123+
active={activeOwnerTokenType}
124+
tabs={ownerTokenTypes}
125+
onTabChange={onOwnerTokenTypeChange}
126+
toggleStyle
69127
small
70-
info
71-
content={m.claimWarning}
72-
messageValues={{
73-
strong: txt => <strong>{txt}</strong>,
74-
}}
75-
className="mb-0"
128+
className="w-content p-0 mb-cs-xs mt-cs-xxs border-none br-m gap-0 fs-s"
76129
/>
77-
</Form.InfoField>
130+
{activeOwnerTokenType === 'fleet' && (
131+
<Message
132+
content={sharedMessages.fleetInfo}
133+
values={{
134+
Link: val => (
135+
<Link.DocLink
136+
secondary
137+
path="/hardware/gateways/models/thethingsindoorgatewaypro/#subscription"
138+
>
139+
{val}
140+
</Link.DocLink>
141+
),
142+
}}
143+
className="mb-cs-xs c-text-neutral-light"
144+
component="div"
145+
/>
146+
)}
147+
</>
78148
)}
79149
<Form.Field
80150
required
81151
title={sharedMessages.ownerToken}
152+
showTitle={!isManaged}
82153
name="authenticated_identifiers.authentication_code"
83154
tooltipId={tooltipIds.CLAIM_AUTH_CODE}
84155
component={Input}
156+
description={
157+
isManaged && activeOwnerTokenType === 'fleet' ? sharedMessages.fleetTokenInfo : undefined
158+
}
159+
descriptionValues={{
160+
Link: val => (
161+
<Link.Anchor secondary href={`${smUrl}/dashboard/subscriptions?type=gateway`} external>
162+
{val}
163+
</Link.Anchor>
164+
),
165+
}}
85166
encode={btoa}
86167
decode={atob}
87-
disabled={withQRdata}
88168
sensitive
169+
data-1p-ignore
170+
data-lpignore
89171
autoFocus
90172
/>
91173
<Form.Field

pkg/webui/console/containers/gateway-onboarding-form/gateway-provisioning-form/validation-schema.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import registerValidationSchema from './gateway-registration-form-section/valida
1919

2020
export const validationSchema = Yup.object({
2121
_owner_id: Yup.string(),
22+
_isFleet: Yup.boolean(),
2223
}).when('._inputMethod', {
2324
is: 'register',
2425
then: schema => schema.concat(registerValidationSchema),

0 commit comments

Comments
 (0)