diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 203f295ed0..da49a6094b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -675,7 +675,7 @@ To decide whether a component is a container component, ask yourself: - Is this component more concerned with how things work, rather than how things look? - Does this component connect to the store? - Does this component fetch or send data? -- Is the component generated by higher order components (e.g. `withFeatureRequirement`)? +- Is the component generated by higher order components? - Does this component render simple nodes, like a single presentational component? If you can answer more than 2 questions with yes, then you likely have a container component. @@ -692,7 +692,6 @@ View components always represent a single view of the application, represented b - Fetching necessary data (via `withRequest` HOC), if not done by a container - Unavailable "catch-all"-routes are caught by `` component, including subviews - Errors should be caught by the `` error boundary component -- `withFeatureRequirement` HOC is used to prevent access to routes that the user has no rights for - Ensured responsiveness and usage of the grid system #### Utility components diff --git a/cypress/e2e/console/integrations/pub-subs/create.spec.js b/cypress/e2e/console/integrations/pub-subs/create.spec.js index 3d8983ffb6..78f4616d4e 100644 --- a/cypress/e2e/console/integrations/pub-subs/create.spec.js +++ b/cypress/e2e/console/integrations/pub-subs/create.spec.js @@ -323,4 +323,85 @@ describe('Application Pub/Sub create', () => { }) }) }) + + describe('Disabled Providers', () => { + const description = 'Changing the Pub/Sub provider has been disabled by an administrator' + + describe('NATS disabled', () => { + const response = { + configuration: { + pubsub: { + providers: { + nats: 'DISABLED', + }, + }, + }, + } + + beforeEach(() => { + cy.loginConsole({ user_id: userId, password: user.password }) + cy.visit( + `${Cypress.config('consoleRootPath')}/applications/${appId}/integrations/pubsubs/add`, + ) + + cy.intercept('GET', `/api/v3/as/configuration`, response) + }) + it('succeeds setting MQTT as default provider', () => { + cy.findByLabelText('NATS').should('be.disabled') + cy.findByText(description).should('be.visible') + }) + }) + + describe('MQTT disabled', () => { + const description = 'Changing the Pub/Sub provider has been disabled by an administrator' + const response = { + configuration: { + pubsub: { + providers: { + mqtt: 'DISABLED', + }, + }, + }, + } + + beforeEach(() => { + cy.loginConsole({ user_id: userId, password: user.password }) + cy.visit( + `${Cypress.config('consoleRootPath')}/applications/${appId}/integrations/pubsubs/add`, + ) + cy.intercept('GET', `/api/v3/as/configuration`, response) + }) + + it('succeeds setting NATS as default provider', () => { + cy.findByLabelText('MQTT').should('be.disabled') + cy.findByText(description).should('be.visible') + }) + }) + + describe('MQTT and NATS disabled', () => { + const response = { + configuration: { + pubsub: { + providers: { + mqtt: 'DISABLED', + nats: 'DISABLED', + }, + }, + }, + } + + beforeEach(() => { + cy.loginConsole({ user_id: userId, password: user.password }) + cy.on('uncaught:exception', () => false) + cy.visit( + `${Cypress.config('consoleRootPath')}/applications/${appId}/integrations/pubsubs/add`, + ) + cy.intercept('GET', `/api/v3/as/configuration`, response) + }) + + it('succeeds showing not found page', () => { + cy.findByRole('heading', { name: /Not found/ }).should('be.visible') + }) + }) + }) }) diff --git a/pkg/webui/console/components/downlink-form/connect.js b/pkg/webui/console/components/downlink-form/connect.js deleted file mode 100644 index a9c006373b..0000000000 --- a/pkg/webui/console/components/downlink-form/connect.js +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import tts from '@console/api/tts' - -import { - selectSelectedApplicationId, - selectApplicationLinkSkipPayloadCrypto, -} from '@console/store/selectors/applications' -import { selectSelectedDeviceId, selectSelectedDevice } from '@console/store/selectors/devices' - -const mapStateToProps = state => { - const appId = selectSelectedApplicationId(state) - const devId = selectSelectedDeviceId(state) - const device = selectSelectedDevice(state) - const skipPayloadCrypto = selectApplicationLinkSkipPayloadCrypto(state) - - return { - appId, - devId, - device, - downlinkQueue: tts.Applications.Devices.DownlinkQueue, - skipPayloadCrypto, - } -} - -export default DownlinkForm => connect(mapStateToProps)(DownlinkForm) diff --git a/pkg/webui/console/components/downlink-form/downlink-form.js b/pkg/webui/console/components/downlink-form/downlink-form.js deleted file mode 100644 index 5c1d84b3ab..0000000000 --- a/pkg/webui/console/components/downlink-form/downlink-form.js +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React, { useState, useCallback } from 'react' -import { defineMessages } from 'react-intl' - -import Notification from '@ttn-lw/components/notification' -import SubmitButton from '@ttn-lw/components/submit-button' -import RadioButton from '@ttn-lw/components/radio-button' -import Checkbox from '@ttn-lw/components/checkbox' -import Input from '@ttn-lw/components/input' -import SubmitBar from '@ttn-lw/components/submit-bar' -import toast from '@ttn-lw/components/toast' -import Form from '@ttn-lw/components/form' -import CodeEditor from '@ttn-lw/components/code-editor' - -import IntlHelmet from '@ttn-lw/lib/components/intl-helmet' - -import Yup from '@ttn-lw/lib/yup' -import PropTypes from '@ttn-lw/lib/prop-types' -import sharedMessages from '@ttn-lw/lib/shared-messages' - -import { hexToBase64 } from '@console/lib/bytes' - -const m = defineMessages({ - insertMode: 'Insert Mode', - payloadType: 'Payload type', - bytes: 'Bytes', - replace: 'Replace downlink queue', - push: 'Push to downlink queue (append)', - scheduleDownlink: 'Schedule downlink', - downlinkSuccess: 'Downlink scheduled', - bytesPayloadDescription: 'The desired payload bytes of the downlink message', - jsonPayloadDescription: 'The decoded payload of the downlink message', - invalidSessionWarning: - 'Downlinks can only be scheduled for end devices with a valid session. Please make sure your end device is properly connected to the network.', -}) - -const validationSchema = Yup.object({ - _mode: Yup.string().oneOf(['replace', 'push']).required(sharedMessages.validateRequired), - _payload_type: Yup.string().oneOf(['bytes', 'json']), - f_port: Yup.number() - .min(1, Yup.passValues(sharedMessages.validateNumberGte)) - .max(223, Yup.passValues(sharedMessages.validateNumberLte)) - .required(sharedMessages.validateRequired), - confirmed: Yup.bool().required(), - frm_payload: Yup.string().when('_payload_type', { - is: type => type === 'bytes', - then: schema => - schema.test( - 'len', - Yup.passValues(sharedMessages.validateHexLength), - val => !Boolean(val) || val.length % 3 === 0, - ), - otherwise: schema => schema.strip(), - }), - decoded_payload: Yup.string().when('_payload_type', { - is: type => type === 'json', - then: schema => - schema.test('valid-json', sharedMessages.validateJson, json => { - try { - JSON.parse(json) - return true - } catch (e) { - return false - } - }), - otherwise: schema => schema.strip(), - }), -}) - -const initialValues = { - _mode: 'replace', - _payload_type: 'bytes', - f_port: 1, - confirmed: false, - frm_payload: '', - decoded_payload: '', -} - -const DownlinkForm = ({ appId, devId, device, downlinkQueue, skipPayloadCrypto }) => { - const [payloadType, setPayloadType] = React.useState('bytes') - const [error, setError] = useState('') - - const handleSubmit = useCallback( - async (vals, { setSubmitting, resetForm }) => { - const { _mode, _payload_type, ...values } = validationSchema.cast(vals) - try { - if (_payload_type === 'bytes') { - values.frm_payload = hexToBase64(values.frm_payload) - } - - if (_payload_type === 'json') { - values.decoded_payload = JSON.parse(values.decoded_payload) - } - - await downlinkQueue[_mode](appId, devId, [values]) - toast({ - title: sharedMessages.success, - type: toast.types.SUCCESS, - message: m.downlinkSuccess, - }) - setSubmitting(false) - } catch (err) { - setError(err) - resetForm({ values: vals }) - } - }, - [appId, devId, downlinkQueue], - ) - - const validSession = device.session || device.pending_session - const payloadCryptoSkipped = device.skip_payload_crypto_override ?? skipPayloadCrypto - const deviceSimulationDisabled = !validSession || payloadCryptoSkipped - - return ( - <> - {payloadCryptoSkipped && ( - - )} - {!validSession && } - -
- - - - - - - - - - - {payloadType === 'bytes' ? ( - - ) : ( - - )} - - - - - - - ) -} - -DownlinkForm.propTypes = { - appId: PropTypes.string.isRequired, - devId: PropTypes.string.isRequired, - device: PropTypes.device.isRequired, - downlinkQueue: PropTypes.shape({ - list: PropTypes.func.isRequired, - push: PropTypes.func.isRequired, - replace: PropTypes.func.isRequired, - }).isRequired, - skipPayloadCrypto: PropTypes.bool.isRequired, -} - -export default DownlinkForm diff --git a/pkg/webui/console/components/downlink-form/index.js b/pkg/webui/console/components/downlink-form/index.js index de4f69e297..413aba6b65 100644 --- a/pkg/webui/console/components/downlink-form/index.js +++ b/pkg/webui/console/components/downlink-form/index.js @@ -12,9 +12,199 @@ // See the License for the specific language governing permissions and // limitations under the License. -import DownlinkForm from './downlink-form' -import connect from './connect' +import React, { useState, useCallback } from 'react' +import { defineMessages } from 'react-intl' +import { useSelector } from 'react-redux' -const ConnectedDownlinkForm = connect(DownlinkForm) +import tts from '@console/api/tts' -export { ConnectedDownlinkForm as default, DownlinkForm } +import Notification from '@ttn-lw/components/notification' +import SubmitButton from '@ttn-lw/components/submit-button' +import RadioButton from '@ttn-lw/components/radio-button' +import Checkbox from '@ttn-lw/components/checkbox' +import Input from '@ttn-lw/components/input' +import SubmitBar from '@ttn-lw/components/submit-bar' +import toast from '@ttn-lw/components/toast' +import Form from '@ttn-lw/components/form' +import CodeEditor from '@ttn-lw/components/code-editor' + +import IntlHelmet from '@ttn-lw/lib/components/intl-helmet' + +import Yup from '@ttn-lw/lib/yup' +import sharedMessages from '@ttn-lw/lib/shared-messages' + +import { hexToBase64 } from '@console/lib/bytes' + +import { + selectApplicationLinkSkipPayloadCrypto, + selectSelectedApplicationId, +} from '@console/store/selectors/applications' +import { selectSelectedDevice, selectSelectedDeviceId } from '@console/store/selectors/devices' + +const m = defineMessages({ + insertMode: 'Insert Mode', + payloadType: 'Payload type', + bytes: 'Bytes', + replace: 'Replace downlink queue', + push: 'Push to downlink queue (append)', + scheduleDownlink: 'Schedule downlink', + downlinkSuccess: 'Downlink scheduled', + bytesPayloadDescription: 'The desired payload bytes of the downlink message', + jsonPayloadDescription: 'The decoded payload of the downlink message', + invalidSessionWarning: + 'Downlinks can only be scheduled for end devices with a valid session. Please make sure your end device is properly connected to the network.', +}) + +const validationSchema = Yup.object({ + _mode: Yup.string().oneOf(['replace', 'push']).required(sharedMessages.validateRequired), + _payload_type: Yup.string().oneOf(['bytes', 'json']), + f_port: Yup.number() + .min(1, Yup.passValues(sharedMessages.validateNumberGte)) + .max(223, Yup.passValues(sharedMessages.validateNumberLte)) + .required(sharedMessages.validateRequired), + confirmed: Yup.bool().required(), + frm_payload: Yup.string().when('_payload_type', { + is: type => type === 'bytes', + then: schema => + schema.test( + 'len', + Yup.passValues(sharedMessages.validateHexLength), + val => !Boolean(val) || val.length % 3 === 0, + ), + otherwise: schema => schema.strip(), + }), + decoded_payload: Yup.string().when('_payload_type', { + is: type => type === 'json', + then: schema => + schema.test('valid-json', sharedMessages.validateJson, json => { + try { + JSON.parse(json) + return true + } catch (e) { + return false + } + }), + otherwise: schema => schema.strip(), + }), +}) + +const initialValues = { + _mode: 'replace', + _payload_type: 'bytes', + f_port: 1, + confirmed: false, + frm_payload: '', + decoded_payload: '', +} + +const DownlinkForm = () => { + const [payloadType, setPayloadType] = React.useState('bytes') + const [error, setError] = useState('') + const appId = useSelector(selectSelectedApplicationId) + const devId = useSelector(selectSelectedDeviceId) + const device = useSelector(selectSelectedDevice) + const skipPayloadCrypto = useSelector(selectApplicationLinkSkipPayloadCrypto) + + const handleSubmit = useCallback( + async (vals, { setSubmitting, resetForm }) => { + const { _mode, _payload_type, ...values } = validationSchema.cast(vals) + try { + if (_payload_type === 'bytes') { + values.frm_payload = hexToBase64(values.frm_payload) + } + + if (_payload_type === 'json') { + values.decoded_payload = JSON.parse(values.decoded_payload) + } + + await tts.Applications.Devices.DownlinkQueue[_mode](appId, devId, [values]) + toast({ + title: sharedMessages.success, + type: toast.types.SUCCESS, + message: m.downlinkSuccess, + }) + setSubmitting(false) + } catch (err) { + setError(err) + resetForm({ values: vals }) + } + }, + [appId, devId], + ) + + const validSession = device.session || device.pending_session + const payloadCryptoSkipped = device.skip_payload_crypto_override ?? skipPayloadCrypto + const deviceSimulationDisabled = !validSession || payloadCryptoSkipped + + return ( + <> + {payloadCryptoSkipped && ( + + )} + {!validSession && } + +
+ + + + + + + + + + + {payloadType === 'bytes' ? ( + + ) : ( + + )} + + + + + + + ) +} + +export default DownlinkForm diff --git a/pkg/webui/console/components/pubsub-form/index.js b/pkg/webui/console/components/pubsub-form/index.js index 3d62c6961d..081214a6fb 100644 --- a/pkg/webui/console/components/pubsub-form/index.js +++ b/pkg/webui/console/components/pubsub-form/index.js @@ -475,6 +475,7 @@ const PubsubForm = props => { title={sharedMessages.provider} name="_provider" component={Radio.Group} + description={natsDisabled || mqttDisabled ? m.providerDescription : undefined} disabled={natsDisabled || mqttDisabled} > diff --git a/pkg/webui/console/components/pubsub-form/messages.js b/pkg/webui/console/components/pubsub-form/messages.js index a817da0d22..81457da324 100644 --- a/pkg/webui/console/components/pubsub-form/messages.js +++ b/pkg/webui/console/components/pubsub-form/messages.js @@ -31,6 +31,7 @@ export default defineMessages({ mqttClientIdPlaceholder: 'my-client-id', mqttServerUrlPlaceholder: 'mqtts://example.com', subscribeQos: 'Subscribe QoS', + providerDescription: 'Changing the Pub/Sub provider has been disabled by an administrator', publishQos: 'Publish QoS', tlsCa: 'Root CA certificate', tlsClientCert: 'Client certificate', diff --git a/pkg/webui/console/components/uplink-form/connect.js b/pkg/webui/console/components/uplink-form/connect.js deleted file mode 100644 index dac3cdf691..0000000000 --- a/pkg/webui/console/components/uplink-form/connect.js +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import tts from '@console/api/tts' - -import { - selectSelectedApplicationId, - selectApplicationLinkSkipPayloadCrypto, -} from '@console/store/selectors/applications' -import { selectSelectedDeviceId, selectSelectedDevice } from '@console/store/selectors/devices' - -const mapStateToProps = state => { - const appId = selectSelectedApplicationId(state) - const devId = selectSelectedDeviceId(state) - const device = selectSelectedDevice(state) - const skipPayloadCrypto = selectApplicationLinkSkipPayloadCrypto(state) - - return { - appId, - devId, - device, - simulateUplink: uplink => tts.Applications.Devices.simulateUplink(appId, devId, uplink), - skipPayloadCrypto, - } -} - -export default UplinkForm => connect(mapStateToProps)(UplinkForm) diff --git a/pkg/webui/console/components/uplink-form/index.js b/pkg/webui/console/components/uplink-form/index.js index ffad4d8f9a..bda4643f75 100644 --- a/pkg/webui/console/components/uplink-form/index.js +++ b/pkg/webui/console/components/uplink-form/index.js @@ -12,9 +12,134 @@ // See the License for the specific language governing permissions and // limitations under the License. -import UplinkForm from './uplink-form' -import connect from './connect' +import React, { useCallback } from 'react' +import { defineMessages } from 'react-intl' +import { useSelector } from 'react-redux' -const ConnectedUplinkForm = connect(UplinkForm) +import tts from '@console/api/tts' -export { ConnectedUplinkForm as default, UplinkForm } +import Notification from '@ttn-lw/components/notification' +import SubmitButton from '@ttn-lw/components/submit-button' +import Input from '@ttn-lw/components/input' +import SubmitBar from '@ttn-lw/components/submit-bar' +import toast from '@ttn-lw/components/toast' +import Form from '@ttn-lw/components/form' + +import IntlHelmet from '@ttn-lw/lib/components/intl-helmet' + +import Yup from '@ttn-lw/lib/yup' +import sharedMessages from '@ttn-lw/lib/shared-messages' + +import { hexToBase64 } from '@console/lib/bytes' + +import { + selectApplicationLinkSkipPayloadCrypto, + selectSelectedApplicationId, +} from '@console/store/selectors/applications' +import { selectSelectedDevice, selectSelectedDeviceId } from '@console/store/selectors/devices' + +const m = defineMessages({ + simulateUplink: 'Simulate uplink', + payloadDescription: 'The desired payload bytes of the uplink message', + uplinkSuccess: 'Uplink sent', +}) + +const validationSchema = Yup.object({ + f_port: Yup.number() + .min(1, Yup.passValues(sharedMessages.validateNumberGte)) + .max(223, Yup.passValues(sharedMessages.validateNumberLte)) + .required(sharedMessages.validateRequired), + frm_payload: Yup.string().test( + 'len', + Yup.passValues(sharedMessages.validateHexLength), + payload => !Boolean(payload) || payload.length % 3 === 0, + ), +}) + +const initialValues = { f_port: 1, frm_payload: '' } + +const UplinkForm = () => { + const [error, setError] = React.useState('') + + const appId = useSelector(selectSelectedApplicationId) + const devId = useSelector(selectSelectedDeviceId) + const device = useSelector(selectSelectedDevice) + const skipPayloadCrypto = useSelector(selectApplicationLinkSkipPayloadCrypto) + + const simulateUplink = useCallback( + async uplink => await tts.Applications.Devices.simulateUplink(appId, devId, uplink), + [appId, devId], + ) + + const handleSubmit = React.useCallback( + async (values, { setSubmitting, resetForm }) => { + try { + await simulateUplink({ + f_port: values.f_port, + frm_payload: hexToBase64(values.frm_payload), + // `rx_metadata` and `settings` fields are required by the validation middleware in AS. + // These fields won't affect the result of simulating an uplink message. + rx_metadata: [ + { gateway_ids: { gateway_id: 'test' }, rssi: 42, channel_rssi: 42, snr: 4.2 }, + ], + settings: { + data_rate: { lora: { bandwidth: 125000, spreading_factor: 7 } }, + frequency: 868000000, + }, + }) + toast({ + title: sharedMessages.success, + type: toast.types.SUCCESS, + message: m.uplinkSuccess, + }) + setSubmitting(false) + } catch (error) { + setError(error) + resetForm({ values }) + } + }, + [simulateUplink], + ) + + const deviceSimulationDisabled = device.skip_payload_crypto_override ?? skipPayloadCrypto + + return ( + <> + {deviceSimulationDisabled && ( + + )} + +
+ + + + + + + + + ) +} + +export default UplinkForm diff --git a/pkg/webui/console/components/uplink-form/uplink-form.js b/pkg/webui/console/components/uplink-form/uplink-form.js deleted file mode 100644 index 827bfca706..0000000000 --- a/pkg/webui/console/components/uplink-form/uplink-form.js +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' -import { defineMessages } from 'react-intl' - -import Notification from '@ttn-lw/components/notification' -import SubmitButton from '@ttn-lw/components/submit-button' -import Input from '@ttn-lw/components/input' -import SubmitBar from '@ttn-lw/components/submit-bar' -import toast from '@ttn-lw/components/toast' -import Form from '@ttn-lw/components/form' - -import IntlHelmet from '@ttn-lw/lib/components/intl-helmet' - -import Yup from '@ttn-lw/lib/yup' -import PropTypes from '@ttn-lw/lib/prop-types' -import sharedMessages from '@ttn-lw/lib/shared-messages' - -import { hexToBase64 } from '@console/lib/bytes' - -const m = defineMessages({ - simulateUplink: 'Simulate uplink', - payloadDescription: 'The desired payload bytes of the uplink message', - uplinkSuccess: 'Uplink sent', -}) - -const validationSchema = Yup.object({ - f_port: Yup.number() - .min(1, Yup.passValues(sharedMessages.validateNumberGte)) - .max(223, Yup.passValues(sharedMessages.validateNumberLte)) - .required(sharedMessages.validateRequired), - frm_payload: Yup.string().test( - 'len', - Yup.passValues(sharedMessages.validateHexLength), - payload => !Boolean(payload) || payload.length % 3 === 0, - ), -}) - -const initialValues = { f_port: 1, frm_payload: '' } - -const UplinkForm = props => { - const { simulateUplink, device, skipPayloadCrypto } = props - - const [error, setError] = React.useState('') - - const handleSubmit = React.useCallback( - async (values, { setSubmitting, resetForm }) => { - try { - await simulateUplink({ - f_port: values.f_port, - frm_payload: hexToBase64(values.frm_payload), - // `rx_metadata` and `settings` fields are required by the validation middleware in AS. - // These fields won't affect the result of simulating an uplink message. - rx_metadata: [ - { gateway_ids: { gateway_id: 'test' }, rssi: 42, channel_rssi: 42, snr: 4.2 }, - ], - settings: { - data_rate: { lora: { bandwidth: 125000, spreading_factor: 7 } }, - frequency: 868000000, - }, - }) - toast({ - title: sharedMessages.success, - type: toast.types.SUCCESS, - message: m.uplinkSuccess, - }) - setSubmitting(false) - } catch (error) { - setError(error) - resetForm({ values }) - } - }, - [simulateUplink], - ) - - const deviceSimulationDisabled = device.skip_payload_crypto_override ?? skipPayloadCrypto - - return ( - <> - {deviceSimulationDisabled && ( - - )} - -
- - - - - - - - - ) -} - -UplinkForm.propTypes = { - device: PropTypes.device.isRequired, - simulateUplink: PropTypes.func.isRequired, - skipPayloadCrypto: PropTypes.bool, -} - -UplinkForm.defaultProps = { - skipPayloadCrypto: false, -} - -export default UplinkForm diff --git a/pkg/webui/console/containers/application-events/index.js b/pkg/webui/console/containers/application-events/index.js index 8cd3854e74..f39f2d0349 100644 --- a/pkg/webui/console/containers/application-events/index.js +++ b/pkg/webui/console/containers/application-events/index.js @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React from 'react' -import { connect } from 'react-redux' +import React, { useCallback, useMemo } from 'react' +import { useDispatch, useSelector } from 'react-redux' import Events from '@console/components/events' -import withFeatureRequirement from '@console/lib/components/with-feature-requirement' +import Require from '@console/lib/components/require' import PropTypes from '@ttn-lw/lib/prop-types' @@ -38,75 +38,68 @@ import { } from '@console/store/selectors/applications' const ApplicationEvents = props => { - const { - appId, - events, - widget, - paused, - onClear, - onPauseToggle, - truncated, - onFilterChange, - filter, - } = props - - if (widget) { + const { appId, widget } = props + + const events = useSelector(state => selectApplicationEvents(state, appId)) + const paused = useSelector(state => selectApplicationEventsPaused(state, appId)) + const truncated = useSelector(state => selectApplicationEventsTruncated(state, appId)) + const filter = useSelector(state => selectApplicationEventsFilter(state, appId)) + + const dispatch = useDispatch() + + const onClear = useCallback(() => { + dispatch(clearApplicationEventsStream(appId)) + }, [appId, dispatch]) + + const onPauseToggle = useCallback( + paused => { + if (paused) { + dispatch(resumeApplicationEventsStream(appId)) + return + } + dispatch(pauseApplicationEventsStream(appId)) + }, + [appId, dispatch], + ) + + const onFilterChange = useCallback( + filterId => { + dispatch(setApplicationEventsFilter(appId, filterId)) + }, + [appId, dispatch], + ) + + const content = useMemo(() => { + if (widget) { + return ( + + ) + } + return ( - + ) - } - - return ( - - ) + }, [appId, events, filter, onClear, onFilterChange, onPauseToggle, paused, truncated, widget]) + + return {content} } ApplicationEvents.propTypes = { appId: PropTypes.string.isRequired, - events: PropTypes.events, - filter: PropTypes.eventFilter, - onClear: PropTypes.func.isRequired, - onFilterChange: PropTypes.func.isRequired, - onPauseToggle: PropTypes.func.isRequired, - paused: PropTypes.bool.isRequired, - truncated: PropTypes.bool.isRequired, widget: PropTypes.bool, } ApplicationEvents.defaultProps = { widget: false, - events: [], - filter: undefined, } -export default withFeatureRequirement(mayViewApplicationEvents)( - connect( - (state, props) => { - const { appId } = props - - return { - events: selectApplicationEvents(state, appId), - paused: selectApplicationEventsPaused(state, appId), - truncated: selectApplicationEventsTruncated(state, appId), - filter: selectApplicationEventsFilter(state, appId), - } - }, - (dispatch, ownProps) => ({ - onClear: () => dispatch(clearApplicationEventsStream(ownProps.appId)), - onPauseToggle: paused => - paused - ? dispatch(resumeApplicationEventsStream(ownProps.appId)) - : dispatch(pauseApplicationEventsStream(ownProps.appId)), - onFilterChange: filterId => dispatch(setApplicationEventsFilter(ownProps.appId, filterId)), - }), - )(ApplicationEvents), -) +export default ApplicationEvents diff --git a/pkg/webui/console/containers/device-events/index.js b/pkg/webui/console/containers/device-events/index.js index c415b47f55..9b93c57c08 100644 --- a/pkg/webui/console/containers/device-events/index.js +++ b/pkg/webui/console/containers/device-events/index.js @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React from 'react' -import { connect } from 'react-redux' +import React, { useCallback } from 'react' +import { useDispatch, useSelector } from 'react-redux' import Events from '@console/components/events' @@ -35,18 +35,40 @@ import { } from '@console/store/selectors/devices' const DeviceEvents = props => { - const { - appId, - devId, - events, - widget, - paused, - onClear, - onPauseToggle, - onFilterChange, - truncated, - filter, - } = props + const { devIds, widget } = props + + const appId = getApplicationId(devIds) + const devId = getDeviceId(devIds) + const combinedId = combineDeviceIds(appId, devId) + + const events = useSelector(state => selectDeviceEvents(state, combinedId)) + const paused = useSelector(state => selectDeviceEventsPaused(state, combinedId)) + const truncated = useSelector(state => selectDeviceEventsTruncated(state, combinedId)) + const filter = useSelector(state => selectDeviceEventsFilter(state, combinedId)) + + const dispatch = useDispatch() + + const onClear = useCallback(() => { + dispatch(clearDeviceEventsStream(devIds)) + }, [devIds, dispatch]) + + const onPauseToggle = useCallback( + paused => { + if (paused) { + dispatch(resumeDeviceEventsStream(devIds)) + return + } + dispatch(pauseDeviceEventsStream(devIds)) + }, + [devIds, dispatch], + ) + + const onFilterChange = useCallback( + filterId => { + dispatch(setDeviceEventsFilter(devIds, filterId)) + }, + [devIds, dispatch], + ) if (widget) { return ( @@ -76,57 +98,17 @@ const DeviceEvents = props => { } DeviceEvents.propTypes = { - appId: PropTypes.string.isRequired, - devId: PropTypes.string.isRequired, devIds: PropTypes.shape({ device_id: PropTypes.string, application_ids: PropTypes.shape({ application_id: PropTypes.string, }), }).isRequired, - events: PropTypes.events, - filter: PropTypes.eventFilter, - onClear: PropTypes.func.isRequired, - onFilterChange: PropTypes.func.isRequired, - onPauseToggle: PropTypes.func.isRequired, - paused: PropTypes.bool.isRequired, - truncated: PropTypes.bool.isRequired, widget: PropTypes.bool, } DeviceEvents.defaultProps = { widget: false, - events: [], - filter: undefined, } -export default connect( - (state, props) => { - const { devIds } = props - - const appId = getApplicationId(devIds) - const devId = getDeviceId(devIds) - const combinedId = combineDeviceIds(appId, devId) - - return { - devId, - appId, - events: selectDeviceEvents(state, combinedId), - paused: selectDeviceEventsPaused(state, combinedId), - truncated: selectDeviceEventsTruncated(state, combinedId), - filter: selectDeviceEventsFilter(state, combinedId), - } - }, - (dispatch, ownProps) => { - const { devIds } = ownProps - - return { - onClear: () => dispatch(clearDeviceEventsStream(devIds)), - onPauseToggle: paused => - paused - ? dispatch(resumeDeviceEventsStream(devIds)) - : dispatch(pauseDeviceEventsStream(devIds)), - onFilterChange: filterId => dispatch(setDeviceEventsFilter(devIds, filterId)), - } - }, -)(DeviceEvents) +export default DeviceEvents diff --git a/pkg/webui/console/containers/device-profile-section/device-card/connect.js b/pkg/webui/console/containers/device-profile-section/device-card/connect.js deleted file mode 100644 index 0ab4bb6764..0000000000 --- a/pkg/webui/console/containers/device-profile-section/device-card/connect.js +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import { selectDeviceModelById } from '@console/store/selectors/device-repository' - -const mapStateToProps = (state, props) => { - const { brandId, modelId } = props - - return { - model: selectDeviceModelById(state, brandId, modelId), - } -} - -export default DeviceCard => connect(mapStateToProps)(DeviceCard) diff --git a/pkg/webui/console/containers/device-profile-section/device-card/device-card.js b/pkg/webui/console/containers/device-profile-section/device-card/device-card.js deleted file mode 100644 index 4f1df2165c..0000000000 --- a/pkg/webui/console/containers/device-profile-section/device-card/device-card.js +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' -import { defineMessages, useIntl } from 'react-intl' - -import devicePlaceholder from '@assets/misc/end-device-placeholder.svg' - -import Link from '@ttn-lw/components/link' - -import Message from '@ttn-lw/lib/components/message' - -import PropTypes from '@ttn-lw/lib/prop-types' -import sharedMessages from '@ttn-lw/lib/shared-messages' - -import { getLorawanVersionLabel, getLorawanPhyVersionLabel } from '@console/lib/device-utils' - -import style from './device-card.styl' - -const m = defineMessages({ - productWebsite: 'Product website', - dataSheet: 'Data sheet', - classA: 'Class A', - classB: 'Class B', - classC: 'Class C', -}) - -const DeviceCard = props => { - const { model, template } = props - const { name, description, product_url, datasheet_url, photos = {} } = model - const { end_device: device } = template - const { formatMessage } = useIntl() - - const deviceImage = photos.main || devicePlaceholder - const lwVersionLabel = getLorawanVersionLabel(device) - const lwPhyVersionLabel = getLorawanPhyVersionLabel(device) - const modeTitleLabel = device.supports_join - ? sharedMessages.otaa - : device.multicast - ? sharedMessages.multicast - : sharedMessages.abp - const deviceClassTitleLabel = device.supports_class_c - ? m.classC - : device.supports_class_b - ? m.classB - : m.classA - const hasLinks = Boolean(product_url || datasheet_url) - - return ( -
- -
-
-

{name}

- {Boolean(lwVersionLabel) && ( - - {lwVersionLabel} - - )} - {Boolean(lwPhyVersionLabel) && ( - - {lwPhyVersionLabel} - - )} - - -
- {description &&

{description}

} - {hasLinks && ( -
- {product_url && ( - - - - )} - {product_url && datasheet_url && ( - | - )} - {datasheet_url && ( - - - - )} -
- )} -
-
- ) -} - -DeviceCard.propTypes = { - model: PropTypes.shape({ - name: PropTypes.string, - description: PropTypes.string, - product_url: PropTypes.string, - datasheet_url: PropTypes.string, - photos: PropTypes.shape({ - main: PropTypes.string, - }), - }), - template: PropTypes.deviceTemplate.isRequired, -} -DeviceCard.defaultProps = { - model: { - name: undefined, - }, -} - -export default DeviceCard diff --git a/pkg/webui/console/containers/device-profile-section/device-card/index.js b/pkg/webui/console/containers/device-profile-section/device-card/index.js index 5d85032ccb..cd719906eb 100644 --- a/pkg/webui/console/containers/device-profile-section/device-card/index.js +++ b/pkg/webui/console/containers/device-profile-section/device-card/index.js @@ -1,4 +1,4 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,9 +12,110 @@ // See the License for the specific language governing permissions and // limitations under the License. -import connect from './connect' -import DeviceCard from './device-card' +import React from 'react' +import { defineMessages, useIntl } from 'react-intl' +import { useSelector } from 'react-redux' -const ConnectedDeviceCard = connect(DeviceCard) +import devicePlaceholder from '@assets/misc/end-device-placeholder.svg' -export { ConnectedDeviceCard as default, DeviceCard } +import Link from '@ttn-lw/components/link' + +import Message from '@ttn-lw/lib/components/message' + +import PropTypes from '@ttn-lw/lib/prop-types' +import sharedMessages from '@ttn-lw/lib/shared-messages' + +import { getLorawanVersionLabel, getLorawanPhyVersionLabel } from '@console/lib/device-utils' + +import { selectDeviceModelById } from '@console/store/selectors/device-repository' + +import style from './device-card.styl' + +const m = defineMessages({ + productWebsite: 'Product website', + dataSheet: 'Data sheet', + classA: 'Class A', + classB: 'Class B', + classC: 'Class C', +}) + +const DeviceCard = props => { + const { brandId, modelId, template } = props + const { end_device: device } = template + const { formatMessage } = useIntl() + const model = useSelector(state => selectDeviceModelById(state, brandId, modelId)) + const { name, description, product_url, datasheet_url, photos = {} } = model + const deviceImage = photos.main || devicePlaceholder + const lwVersionLabel = getLorawanVersionLabel(device) + const lwPhyVersionLabel = getLorawanPhyVersionLabel(device) + const modeTitleLabel = device.supports_join + ? sharedMessages.otaa + : device.multicast + ? sharedMessages.multicast + : sharedMessages.abp + const deviceClassTitleLabel = device.supports_class_c + ? m.classC + : device.supports_class_b + ? m.classB + : m.classA + const hasLinks = Boolean(product_url || datasheet_url) + + return ( +
+ +
+
+

{name}

+ {Boolean(lwVersionLabel) && ( + + {lwVersionLabel} + + )} + {Boolean(lwPhyVersionLabel) && ( + + {lwPhyVersionLabel} + + )} + + +
+ {description &&

{description}

} + {hasLinks && ( +
+ {product_url && ( + + + + )} + {product_url && datasheet_url && ( + | + )} + {datasheet_url && ( + + + + )} +
+ )} +
+
+ ) +} + +DeviceCard.propTypes = { + brandId: PropTypes.string.isRequired, + modelId: PropTypes.string.isRequired, + template: PropTypes.deviceTemplate.isRequired, +} + +export default DeviceCard diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/brand-select/brand-select.js b/pkg/webui/console/containers/device-profile-section/device-selection/brand-select/brand-select.js deleted file mode 100644 index 677a790406..0000000000 --- a/pkg/webui/console/containers/device-profile-section/device-selection/brand-select/brand-select.js +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' -import { defineMessages, useIntl } from 'react-intl' - -import Field from '@ttn-lw/components/form/field' -import Select from '@ttn-lw/components/select' - -import PropTypes from '@ttn-lw/lib/prop-types' -import sharedMessages from '@ttn-lw/lib/shared-messages' - -import { SELECT_OTHER_OPTION } from '@console/lib/device-utils' - -const m = defineMessages({ - title: 'End device brand', - noOptionsMessage: 'No matching brand found', -}) - -const formatOptions = (brands = []) => - brands - .map(brand => ({ - value: brand.brand_id, - label: brand.name || brand.brand_id, - profileID: brand.brand_id, - })) - .concat([{ value: SELECT_OTHER_OPTION, label: sharedMessages.otherOption }]) - -const BrandSelect = props => { - const { appId, name, error, fetching, brands, onChange, ...rest } = props - const { formatMessage } = useIntl() - - const options = React.useMemo(() => formatOptions(brands), [brands]) - const handleNoOptions = React.useCallback( - () => formatMessage(m.noOptionsMessage), - [formatMessage], - ) - - return ( - - ) -} - -BrandSelect.propTypes = { - appId: PropTypes.string.isRequired, - brands: PropTypes.arrayOf( - PropTypes.shape({ - brand_id: PropTypes.string.isRequired, - }), - ), - error: PropTypes.error, - fetching: PropTypes.bool, - name: PropTypes.string.isRequired, - onChange: PropTypes.func, -} - -BrandSelect.defaultProps = { - error: undefined, - fetching: false, - brands: [], - onChange: () => null, -} - -export default BrandSelect diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/brand-select/connect.js b/pkg/webui/console/containers/device-profile-section/device-selection/brand-select/connect.js deleted file mode 100644 index 7eda01f495..0000000000 --- a/pkg/webui/console/containers/device-profile-section/device-selection/brand-select/connect.js +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import { - selectDeviceBrands, - selectDeviceBrandsError, - selectDeviceBrandsFetching, -} from '@console/store/selectors/device-repository' -import { selectSelectedApplicationId } from '@console/store/selectors/applications' - -const mapStateToProps = state => ({ - appId: selectSelectedApplicationId(state), - brands: selectDeviceBrands(state), - error: selectDeviceBrandsError(state), - fetching: selectDeviceBrandsFetching(state), -}) - -const mapDispatchToProps = {} - -export default BrandSelect => connect(mapStateToProps, mapDispatchToProps)(BrandSelect) diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/brand-select/index.js b/pkg/webui/console/containers/device-profile-section/device-selection/brand-select/index.js index 87b87a17af..1e93ddaa74 100644 --- a/pkg/webui/console/containers/device-profile-section/device-selection/brand-select/index.js +++ b/pkg/webui/console/containers/device-profile-section/device-selection/brand-select/index.js @@ -12,9 +12,75 @@ // See the License for the specific language governing permissions and // limitations under the License. -import connect from './connect' -import BrandSelect from './brand-select' +import React from 'react' +import { defineMessages, useIntl } from 'react-intl' +import { useSelector } from 'react-redux' -const ConnectedBrandSelect = connect(BrandSelect) +import Field from '@ttn-lw/components/form/field' +import Select from '@ttn-lw/components/select' -export { ConnectedBrandSelect as default, BrandSelect } +import PropTypes from '@ttn-lw/lib/prop-types' +import sharedMessages from '@ttn-lw/lib/shared-messages' + +import { SELECT_OTHER_OPTION } from '@console/lib/device-utils' + +import { + selectDeviceBrands, + selectDeviceBrandsError, + selectDeviceBrandsFetching, +} from '@console/store/selectors/device-repository' + +const m = defineMessages({ + title: 'End device brand', + noOptionsMessage: 'No matching brand found', +}) + +const formatOptions = (brands = []) => + brands + .map(brand => ({ + value: brand.brand_id, + label: brand.name || brand.brand_id, + profileID: brand.brand_id, + })) + .concat([{ value: SELECT_OTHER_OPTION, label: sharedMessages.otherOption }]) + +const BrandSelect = props => { + const { name, onChange, ...rest } = props + const { formatMessage } = useIntl() + const brands = useSelector(selectDeviceBrands) + const error = useSelector(selectDeviceBrandsError) + const fetching = useSelector(selectDeviceBrandsFetching) + + const options = React.useMemo(() => formatOptions(brands), [brands]) + const handleNoOptions = React.useCallback( + () => formatMessage(m.noOptionsMessage), + [formatMessage], + ) + + return ( + + ) +} + +BrandSelect.propTypes = { + name: PropTypes.string.isRequired, + onChange: PropTypes.func, +} + +BrandSelect.defaultProps = { + onChange: () => null, +} + +export default BrandSelect diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/connect.js b/pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/connect.js deleted file mode 100644 index b5cd5c46b0..0000000000 --- a/pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/connect.js +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import { isUnknownHwVersion } from '@console/lib/device-utils' - -import { selectDeviceModelFirmwareVersions } from '@console/store/selectors/device-repository' - -const mapStateToProps = (state, props) => { - const { brandId, modelId, hwVersion } = props - - const fwVersions = selectDeviceModelFirmwareVersions(state, brandId, modelId).filter( - ({ supported_hardware_versions = [] }) => - (Boolean(hwVersion) && supported_hardware_versions.includes(hwVersion)) || - // Include firmware versions when there are no hardware versions configured in device repository - // for selected end device model. - isUnknownHwVersion(hwVersion), - ) - - return { - versions: fwVersions, - } -} - -export default FirmwareVersionSelect => connect(mapStateToProps)(FirmwareVersionSelect) diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/fw-version-select.js b/pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/fw-version-select.js deleted file mode 100644 index 2f4cf20595..0000000000 --- a/pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/fw-version-select.js +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' -import { defineMessages } from 'react-intl' - -import Field from '@ttn-lw/components/form/field' -import Select from '@ttn-lw/components/select' -import { useFormContext } from '@ttn-lw/components/form' - -import PropTypes from '@ttn-lw/lib/prop-types' -import sharedMessages from '@ttn-lw/lib/shared-messages' - -import { SELECT_OTHER_OPTION } from '@console/lib/device-utils' - -const m = defineMessages({ - title: 'Firmware Ver.', -}) - -const formatOptions = (versions = []) => - versions - .map(version => ({ - value: version.version, - label: version.version, - })) - .concat([{ value: SELECT_OTHER_OPTION, label: sharedMessages.otherOption }]) - -const FirmwareVersionSelect = props => { - const { name, versions, onChange, ...rest } = props - const { setFieldValue } = useFormContext() - - const options = React.useMemo(() => formatOptions(versions), [versions]) - - React.useEffect(() => { - if (options.length > 0 && options.length <= 2) { - setFieldValue('version_ids.firmware_version', options[0].value) - } - }, [setFieldValue, options]) - - return ( - - ) -} - -FirmwareVersionSelect.propTypes = { - name: PropTypes.string.isRequired, - onChange: PropTypes.func, - versions: PropTypes.arrayOf( - PropTypes.shape({ - version: PropTypes.string.isRequired, - }), - ), -} - -FirmwareVersionSelect.defaultProps = { - versions: [], - onChange: () => null, -} - -export default FirmwareVersionSelect diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/index.js b/pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/index.js index 5dfd1369ce..4075d42862 100644 --- a/pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/index.js +++ b/pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/index.js @@ -12,9 +12,70 @@ // See the License for the specific language governing permissions and // limitations under the License. -import connect from './connect' -import FirmwareVersionSelect from './fw-version-select' +import React from 'react' +import { defineMessages } from 'react-intl' +import { useSelector } from 'react-redux' -const ConnectedFirmwareVersionSelect = connect(FirmwareVersionSelect) +import Field from '@ttn-lw/components/form/field' +import Select from '@ttn-lw/components/select' +import { useFormContext } from '@ttn-lw/components/form' -export { ConnectedFirmwareVersionSelect as default, FirmwareVersionSelect } +import PropTypes from '@ttn-lw/lib/prop-types' +import sharedMessages from '@ttn-lw/lib/shared-messages' + +import { isUnknownHwVersion, SELECT_OTHER_OPTION } from '@console/lib/device-utils' + +import { selectDeviceModelFirmwareVersions } from '@console/store/selectors/device-repository' + +const m = defineMessages({ + title: 'Firmware Ver.', +}) + +const formatOptions = (versions = []) => + versions + .map(version => ({ + value: version.version, + label: version.version, + })) + .concat([{ value: SELECT_OTHER_OPTION, label: sharedMessages.otherOption }]) + +const FirmwareVersionSelect = props => { + const { name, onChange, brandId, modelId, hwVersion, ...rest } = props + const { setFieldValue, values } = useFormContext() + + const versions = useSelector(state => + selectDeviceModelFirmwareVersions(state, brandId, modelId).filter( + ({ supported_hardware_versions = [] }) => + (Boolean(hwVersion) && supported_hardware_versions.includes(hwVersion)) || + // Include firmware versions when there are no hardware versions configured in device repository + // for selected end device model. + isUnknownHwVersion(hwVersion), + ), + ) + + const options = React.useMemo(() => formatOptions(versions), [versions]) + + React.useEffect(() => { + if (options.length > 0 && options.length <= 2 && !values.version_ids.firmware_version.length) { + setFieldValue('version_ids.firmware_version', options[0].value) + } + }, [setFieldValue, options, values.version_ids.firmware_version.length]) + + return ( + + ) +} + +FirmwareVersionSelect.propTypes = { + brandId: PropTypes.string.isRequired, + hwVersion: PropTypes.string.isRequired, + modelId: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + onChange: PropTypes.func, +} + +FirmwareVersionSelect.defaultProps = { + onChange: () => null, +} + +export default FirmwareVersionSelect diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/connect.js b/pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/connect.js deleted file mode 100644 index 3f1b4282db..0000000000 --- a/pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/connect.js +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import { selectDeviceModelHardwareVersions } from '@console/store/selectors/device-repository' - -const mapStateToProps = (state, props) => { - const { brandId, modelId } = props - - return { - versions: selectDeviceModelHardwareVersions(state, brandId, modelId), - } -} - -export default HardwareVersionSelect => connect(mapStateToProps)(HardwareVersionSelect) diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/hw-version-select.js b/pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/hw-version-select.js deleted file mode 100644 index 940218ae42..0000000000 --- a/pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/hw-version-select.js +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' -import { defineMessages } from 'react-intl' - -import Field from '@ttn-lw/components/form/field' -import Select from '@ttn-lw/components/select' -import { useFormContext } from '@ttn-lw/components/form' - -import PropTypes from '@ttn-lw/lib/prop-types' -import sharedMessages from '@ttn-lw/lib/shared-messages' - -import { SELECT_OTHER_OPTION, SELECT_UNKNOWN_HW_OPTION } from '@console/lib/device-utils' - -const m = defineMessages({ - title: 'Hardware Ver.', -}) - -const formatOptions = (versions = []) => - versions - .map(version => ({ - value: version.version, - label: version.version, - })) - .concat([{ value: SELECT_OTHER_OPTION, label: sharedMessages.otherOption }]) - -const HardwareVersionSelect = props => { - const { name, versions, onChange, ...rest } = props - const { setFieldValue } = useFormContext() - - const options = React.useMemo(() => { - const opts = formatOptions(versions) - // When only the `Other...` option is available (so end device model has no hw versions defined - // in the device repository) add another pseudo option that represents absence of hw versions. - if (opts.length === 1) { - opts.unshift({ value: SELECT_UNKNOWN_HW_OPTION, label: sharedMessages.unknownHwOption }) - } - - return opts - }, [versions]) - - React.useEffect(() => { - if (options.length > 0 && options.length <= 2) { - setFieldValue('version_ids.hardware_version', options[0].value) - } - }, [options, setFieldValue]) - - return ( - - ) -} - -HardwareVersionSelect.propTypes = { - name: PropTypes.string.isRequired, - onChange: PropTypes.func, - versions: PropTypes.arrayOf( - PropTypes.shape({ - version: PropTypes.string.isRequired, - }), - ), -} - -HardwareVersionSelect.defaultProps = { - versions: [], - onChange: () => null, -} - -export default HardwareVersionSelect diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/index.js b/pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/index.js index 9f14b89010..7f1cfa0470 100644 --- a/pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/index.js +++ b/pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/index.js @@ -1,4 +1,4 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,9 +12,69 @@ // See the License for the specific language governing permissions and // limitations under the License. -import connect from './connect' -import HardwareVersionSelect from './hw-version-select' +import React from 'react' +import { defineMessages } from 'react-intl' +import { useSelector } from 'react-redux' -const ConnectedHardwareVersionSelect = connect(HardwareVersionSelect) +import Field from '@ttn-lw/components/form/field' +import Select from '@ttn-lw/components/select' +import { useFormContext } from '@ttn-lw/components/form' -export { ConnectedHardwareVersionSelect as default, HardwareVersionSelect } +import PropTypes from '@ttn-lw/lib/prop-types' +import sharedMessages from '@ttn-lw/lib/shared-messages' + +import { SELECT_OTHER_OPTION, SELECT_UNKNOWN_HW_OPTION } from '@console/lib/device-utils' + +import { selectDeviceModelHardwareVersions } from '@console/store/selectors/device-repository' + +const m = defineMessages({ + title: 'Hardware Ver.', +}) + +const formatOptions = (versions = []) => + versions + .map(version => ({ + value: version.version, + label: version.version, + })) + .concat([{ value: SELECT_OTHER_OPTION, label: sharedMessages.otherOption }]) + +const HardwareVersionSelect = props => { + const { name, brandId, modelId, onChange, ...rest } = props + const { setFieldValue, values } = useFormContext() + const versions = useSelector(state => selectDeviceModelHardwareVersions(state, brandId, modelId)) + + const options = React.useMemo(() => { + const opts = formatOptions(versions) + // When only the `Other...` option is available (so end device model has no hw versions defined + // in the device repository) add another pseudo option that represents absence of hw versions. + if (opts.length === 1) { + opts.unshift({ value: SELECT_UNKNOWN_HW_OPTION, label: sharedMessages.unknownHwOption }) + } + + return opts + }, [versions]) + + React.useEffect(() => { + if (options.length > 0 && options.length <= 2 && !values.version_ids.hardware_version.length) { + setFieldValue('version_ids.hardware_version', options[0].value) + } + }, [options, setFieldValue, values]) + + return ( + + ) +} + +HardwareVersionSelect.propTypes = { + brandId: PropTypes.string.isRequired, + modelId: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + onChange: PropTypes.func, +} + +HardwareVersionSelect.defaultProps = { + onChange: () => null, +} + +export default HardwareVersionSelect diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/model-select/connect.js b/pkg/webui/console/containers/device-profile-section/device-selection/model-select/connect.js deleted file mode 100644 index 5a5a5bedfa..0000000000 --- a/pkg/webui/console/containers/device-profile-section/device-selection/model-select/connect.js +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import { listModels } from '@console/store/actions/device-repository' - -import { - selectDeviceModelsByBrandId, - selectDeviceModelsError, - selectDeviceModelsFetching, -} from '@console/store/selectors/device-repository' -import { selectSelectedApplicationId } from '@console/store/selectors/applications' - -const mapStateToProps = (state, props) => { - const { brandId } = props - - return { - appId: selectSelectedApplicationId(state), - models: selectDeviceModelsByBrandId(state, brandId), - error: selectDeviceModelsError(state), - fetching: selectDeviceModelsFetching(state), - } -} - -const mapDispatchToProps = { listModels } - -export default ModelSelect => connect(mapStateToProps, mapDispatchToProps)(ModelSelect) diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/model-select/index.js b/pkg/webui/console/containers/device-profile-section/device-selection/model-select/index.js index a83ad06022..7c0f6a494b 100644 --- a/pkg/webui/console/containers/device-profile-section/device-selection/model-select/index.js +++ b/pkg/webui/console/containers/device-profile-section/device-selection/model-select/index.js @@ -12,9 +12,106 @@ // See the License for the specific language governing permissions and // limitations under the License. -import connect from './connect' -import ModelSelect from './model-select' +import React from 'react' +import { defineMessages, useIntl } from 'react-intl' +import { useDispatch, useSelector } from 'react-redux' -const ConnectedModelSelect = connect(ModelSelect) +import Field from '@ttn-lw/components/form/field' +import Select from '@ttn-lw/components/select' -export { ConnectedModelSelect as default, ModelSelect } +import PropTypes from '@ttn-lw/lib/prop-types' +import sharedMessages from '@ttn-lw/lib/shared-messages' + +import { SELECT_OTHER_OPTION } from '@console/lib/device-utils' + +import { listModels } from '@console/store/actions/device-repository' + +import { selectSelectedApplicationId } from '@console/store/selectors/applications' +import { + selectDeviceModelsByBrandId, + selectDeviceModelsError, + selectDeviceModelsFetching, +} from '@console/store/selectors/device-repository' + +const m = defineMessages({ + noOptionsMessage: 'No matching model found', +}) + +const formatOptions = (models = []) => + models + .map(model => ({ + value: model.model_id, + label: model.name, + })) + .concat([{ value: SELECT_OTHER_OPTION, label: sharedMessages.otherOption }]) + +const ModelSelect = props => { + const { brandId, name, onChange, ...rest } = props + const { formatMessage } = useIntl() + const dispatch = useDispatch() + const appId = useSelector(selectSelectedApplicationId) + const models = useSelector(state => selectDeviceModelsByBrandId(state, brandId)) + const error = useSelector(selectDeviceModelsError) + const fetching = useSelector(selectDeviceModelsFetching) + + React.useEffect(() => { + dispatch( + listModels(appId, brandId, {}, [ + 'name', + 'description', + 'firmware_versions', + 'hardware_versions', + 'key_provisioning', + 'photos', + 'product_url', + 'datasheet_url', + ]), + ) + }, [appId, brandId, dispatch]) + + const options = React.useMemo(() => formatOptions(models), [models]) + const handleNoOptions = React.useCallback( + () => formatMessage(m.noOptionsMessage), + [formatMessage], + ) + + return ( + + ) +} + +ModelSelect.propTypes = { + appId: PropTypes.string.isRequired, + brandId: PropTypes.string.isRequired, + error: PropTypes.error, + fetching: PropTypes.bool, + models: PropTypes.arrayOf( + PropTypes.shape({ + model_id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }), + ), + name: PropTypes.string.isRequired, + onChange: PropTypes.func, +} + +ModelSelect.defaultProps = { + error: undefined, + fetching: false, + models: [], + onChange: () => null, +} + +export default ModelSelect diff --git a/pkg/webui/console/containers/device-profile-section/device-selection/model-select/model-select.js b/pkg/webui/console/containers/device-profile-section/device-selection/model-select/model-select.js deleted file mode 100644 index d0d64d3607..0000000000 --- a/pkg/webui/console/containers/device-profile-section/device-selection/model-select/model-select.js +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' -import { defineMessages, useIntl } from 'react-intl' - -import Field from '@ttn-lw/components/form/field' -import Select from '@ttn-lw/components/select' - -import PropTypes from '@ttn-lw/lib/prop-types' -import sharedMessages from '@ttn-lw/lib/shared-messages' - -import { SELECT_OTHER_OPTION } from '@console/lib/device-utils' - -const m = defineMessages({ - noOptionsMessage: 'No matching model found', -}) - -const formatOptions = (models = []) => - models - .map(model => ({ - value: model.model_id, - label: model.name, - })) - .concat([{ value: SELECT_OTHER_OPTION, label: sharedMessages.otherOption }]) - -const ModelSelect = props => { - const { appId, brandId, name, error, fetching, models, listModels, onChange, ...rest } = props - const { formatMessage } = useIntl() - - React.useEffect(() => { - listModels(appId, brandId, {}, [ - 'name', - 'description', - 'firmware_versions', - 'hardware_versions', - 'key_provisioning', - 'photos', - 'product_url', - 'datasheet_url', - ]) - }, [appId, brandId, listModels]) - - const options = React.useMemo(() => formatOptions(models), [models]) - const handleNoOptions = React.useCallback( - () => formatMessage(m.noOptionsMessage), - [formatMessage], - ) - - return ( - - ) -} - -ModelSelect.propTypes = { - appId: PropTypes.string.isRequired, - brandId: PropTypes.string.isRequired, - error: PropTypes.error, - fetching: PropTypes.bool, - listModels: PropTypes.func.isRequired, - models: PropTypes.arrayOf( - PropTypes.shape({ - model_id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - }), - ), - name: PropTypes.string.isRequired, - onChange: PropTypes.func, -} - -ModelSelect.defaultProps = { - error: undefined, - fetching: false, - models: [], - onChange: () => null, -} - -export default ModelSelect diff --git a/pkg/webui/console/containers/device-title-section/connect.js b/pkg/webui/console/containers/device-title-section/connect.js deleted file mode 100644 index c1f922fecf..0000000000 --- a/pkg/webui/console/containers/device-title-section/connect.js +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import { - selectDeviceByIds, - selectDeviceDerivedUplinkFrameCount, - selectDeviceDerivedDownlinkFrameCount, - selectDeviceLastSeen, -} from '@console/store/selectors/devices' - -const mapStateToProps = (state, props) => { - const { devId, appId } = props - - return { - devId, - appId, - device: selectDeviceByIds(state, appId, devId), - uplinkFrameCount: selectDeviceDerivedUplinkFrameCount(state, appId, devId), - downlinkFrameCount: selectDeviceDerivedDownlinkFrameCount(state, appId, devId), - lastSeen: selectDeviceLastSeen(state, appId, devId), - } -} - -export default TitleSection => connect(mapStateToProps)(TitleSection) diff --git a/pkg/webui/console/containers/device-title-section/device-title-section.js b/pkg/webui/console/containers/device-title-section/device-title-section.js deleted file mode 100644 index 80e9735676..0000000000 --- a/pkg/webui/console/containers/device-title-section/device-title-section.js +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' -import { defineMessages } from 'react-intl' - -import deviceIcon from '@assets/misc/end-device.svg' - -import Status from '@ttn-lw/components/status' -import Tooltip from '@ttn-lw/components/tooltip' -import DocTooltip from '@ttn-lw/components/tooltip/doc' -import Icon from '@ttn-lw/components/icon' - -import Message from '@ttn-lw/lib/components/message' -import DateTime from '@ttn-lw/lib/components/date-time' - -import EntityTitleSection from '@console/components/entity-title-section' -import LastSeen from '@console/components/last-seen' - -import PropTypes from '@ttn-lw/lib/prop-types' -import sharedMessages from '@ttn-lw/lib/shared-messages' - -import style from './device-title-section.styl' - -const m = defineMessages({ - uplinkDownlinkTooltip: - 'The number of sent uplinks and received downlinks of this end device since the last frame counter reset.', - lastSeenAvailableTooltip: - 'The elapsed time since the network registered the last activity of this end device. This is determined from sent uplinks, confirmed downlinks or (re)join requests.{lineBreak}The last activity was received at {lastActivityInfo}', - noActivityTooltip: - 'The network has not registered any activity from this end device yet. This could mean that your end device has not sent any messages yet or only messages that cannot be handled by the network, e.g. due to a mismatch of EUIs or frequencies.', -}) - -const { Content } = EntityTitleSection - -const DeviceTitleSection = props => { - const { devId, fetching, device, uplinkFrameCount, downlinkFrameCount, lastSeen, children } = - props - const showLastSeen = Boolean(lastSeen) - const showUplinkCount = typeof uplinkFrameCount === 'number' - const showDownlinkCount = typeof downlinkFrameCount === 'number' - const notAvailableElem = - const lastActivityInfo = lastSeen ? : lastSeen - const lineBreak =
- const bottomBarLeft = ( - <> - }> -
- - -
-
- {showLastSeen ? ( - - } - > - - - - - ) : ( - } - > - - - - - )} - - ) - - return ( - - - {children} - - ) -} - -DeviceTitleSection.propTypes = { - children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]), - devId: PropTypes.string.isRequired, - device: PropTypes.device.isRequired, - downlinkFrameCount: PropTypes.number, - fetching: PropTypes.bool, - lastSeen: PropTypes.string, - uplinkFrameCount: PropTypes.number, -} - -DeviceTitleSection.defaultProps = { - uplinkFrameCount: undefined, - lastSeen: undefined, - children: null, - fetching: false, - downlinkFrameCount: undefined, -} - -export default DeviceTitleSection diff --git a/pkg/webui/console/containers/device-title-section/index.js b/pkg/webui/console/containers/device-title-section/index.js index 53e492af34..7588a1c864 100644 --- a/pkg/webui/console/containers/device-title-section/index.js +++ b/pkg/webui/console/containers/device-title-section/index.js @@ -12,9 +12,134 @@ // See the License for the specific language governing permissions and // limitations under the License. -import DeviceTitleSection from './device-title-section' -import connect from './connect' +import React from 'react' +import { defineMessages } from 'react-intl' +import { useSelector } from 'react-redux' -const ConnectedDeviceTitleSection = connect(DeviceTitleSection) +import deviceIcon from '@assets/misc/end-device.svg' -export { ConnectedDeviceTitleSection as default, DeviceTitleSection } +import Status from '@ttn-lw/components/status' +import Tooltip from '@ttn-lw/components/tooltip' +import DocTooltip from '@ttn-lw/components/tooltip/doc' +import Icon from '@ttn-lw/components/icon' + +import Message from '@ttn-lw/lib/components/message' +import DateTime from '@ttn-lw/lib/components/date-time' + +import EntityTitleSection from '@console/components/entity-title-section' +import LastSeen from '@console/components/last-seen' + +import PropTypes from '@ttn-lw/lib/prop-types' +import sharedMessages from '@ttn-lw/lib/shared-messages' + +import { + selectDeviceByIds, + selectDeviceDerivedDownlinkFrameCount, + selectDeviceDerivedUplinkFrameCount, + selectDeviceLastSeen, +} from '@console/store/selectors/devices' + +import style from './device-title-section.styl' + +const m = defineMessages({ + uplinkDownlinkTooltip: + 'The number of sent uplinks and received downlinks of this end device since the last frame counter reset.', + lastSeenAvailableTooltip: + 'The elapsed time since the network registered the last activity of this end device. This is determined from sent uplinks, confirmed downlinks or (re)join requests.{lineBreak}The last activity was received at {lastActivityInfo}', + noActivityTooltip: + 'The network has not registered any activity from this end device yet. This could mean that your end device has not sent any messages yet or only messages that cannot be handled by the network, e.g. due to a mismatch of EUIs or frequencies.', +}) + +const { Content } = EntityTitleSection + +const DeviceTitleSection = props => { + const { appId, devId, fetching, children } = props + const device = useSelector(state => selectDeviceByIds(state, appId, devId)) + const uplinkFrameCount = useSelector(state => + selectDeviceDerivedUplinkFrameCount(state, appId, devId), + ) + const downlinkFrameCount = useSelector(state => + selectDeviceDerivedDownlinkFrameCount(state, appId, devId), + ) + const lastSeen = useSelector(state => selectDeviceLastSeen(state, appId, devId)) + const showLastSeen = Boolean(lastSeen) + const showUplinkCount = typeof uplinkFrameCount === 'number' + const showDownlinkCount = typeof downlinkFrameCount === 'number' + const notAvailableElem = + const lastActivityInfo = lastSeen ? : lastSeen + const lineBreak =
+ const bottomBarLeft = ( + <> + }> +
+ + +
+
+ {showLastSeen ? ( + + } + > + + + + + ) : ( + } + > + + + + + )} + + ) + + return ( + + + {children} + + ) +} + +DeviceTitleSection.propTypes = { + appId: PropTypes.string.isRequired, + children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]), + devId: PropTypes.string.isRequired, + fetching: PropTypes.bool, +} + +DeviceTitleSection.defaultProps = { + children: null, + fetching: false, +} + +export default DeviceTitleSection diff --git a/pkg/webui/console/containers/gateway-connection/connect.js b/pkg/webui/console/containers/gateway-connection/connect.js deleted file mode 100644 index 1cd5a148ec..0000000000 --- a/pkg/webui/console/containers/gateway-connection/connect.js +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import { selectGsConfig } from '@ttn-lw/lib/selectors/env' -import getHostFromUrl from '@ttn-lw/lib/host-from-url' - -import { - startGatewayStatistics, - stopGatewayStatistics, - updateGatewayStatistics, -} from '@console/store/actions/gateways' - -import { - selectGatewayStatistics, - selectGatewayStatisticsError, - selectGatewayStatisticsIsFetching, - selectLatestGatewayEvent, - selectGatewayById, -} from '@console/store/selectors/gateways' -import { selectGatewayLastSeen } from '@console/store/selectors/gateway-status' - -import withConnectionReactor from './gateway-connection-reactor' - -export default GatewayConnection => - connect( - (state, ownProps) => { - const gateway = selectGatewayById(state, ownProps.gtwId) - const gsConfig = selectGsConfig() - const consoleGsAddress = getHostFromUrl(gsConfig.base_url) - const gatewayServerAddress = getHostFromUrl(gateway.gateway_server_address) - - return { - statistics: selectGatewayStatistics(state, ownProps), - error: selectGatewayStatisticsError(state, ownProps), - fetching: selectGatewayStatisticsIsFetching(state, ownProps), - latestEvent: selectLatestGatewayEvent(state, ownProps.gtwId), - lastSeen: selectGatewayLastSeen(state), - isOtherCluster: consoleGsAddress !== gatewayServerAddress, - } - }, - (dispatch, ownProps) => ({ - startStatistics: () => dispatch(startGatewayStatistics(ownProps.gtwId)), - stopStatistics: () => dispatch(stopGatewayStatistics()), - updateGatewayStatistics: () => dispatch(updateGatewayStatistics(ownProps.gtwId)), - }), - )(withConnectionReactor(GatewayConnection)) diff --git a/pkg/webui/console/containers/gateway-connection/gateway-connection-reactor.js b/pkg/webui/console/containers/gateway-connection/gateway-connection-reactor.js deleted file mode 100644 index a680fa9ede..0000000000 --- a/pkg/webui/console/containers/gateway-connection/gateway-connection-reactor.js +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' - -import { - isGsStatusReceiveEvent, - isGsDownlinkSendEvent, - isGsUplinkReceiveEvent, -} from '@ttn-lw/lib/selectors/event' -import PropTypes from '@ttn-lw/lib/prop-types' - -/** - * `withConnectionReactor` is a HOC that handles gateway connection statistics - * updates based on gateway uplink, downlink and connection events. - * - * @param {object} Component - React component to be wrapped by the reactor. - * @returns {object} - A wrapped react component. - */ -const withConnectionReactor = Component => { - class ConnectionReactor extends React.PureComponent { - componentDidUpdate(prevProps) { - const { latestEvent, updateGatewayStatistics } = this.props - - if (Boolean(latestEvent) && latestEvent !== prevProps.latestEvent) { - const { name } = latestEvent - const isHeartBeatEvent = - isGsDownlinkSendEvent(name) || - isGsUplinkReceiveEvent(name) || - isGsStatusReceiveEvent(name) - - if (isHeartBeatEvent) { - updateGatewayStatistics() - } - } - } - - render() { - const { latestEvent, updateGatewayStatistics, ...rest } = this.props - return - } - } - - ConnectionReactor.propTypes = { - latestEvent: PropTypes.event, - updateGatewayStatistics: PropTypes.func.isRequired, - } - - ConnectionReactor.defaultProps = { - latestEvent: undefined, - } - - return ConnectionReactor -} - -export default withConnectionReactor diff --git a/pkg/webui/console/containers/gateway-connection/gateway-connection.js b/pkg/webui/console/containers/gateway-connection/gateway-connection.js deleted file mode 100644 index 54463ba764..0000000000 --- a/pkg/webui/console/containers/gateway-connection/gateway-connection.js +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React, { useEffect, useMemo } from 'react' -import classnames from 'classnames' -import { FormattedNumber, defineMessages } from 'react-intl' - -import Status from '@ttn-lw/components/status' -import Icon from '@ttn-lw/components/icon' -import DocTooltip from '@ttn-lw/components/tooltip/doc' -import Tooltip from '@ttn-lw/components/tooltip' - -import Message from '@ttn-lw/lib/components/message' - -import LastSeen from '@console/components/last-seen' - -import PropTypes from '@ttn-lw/lib/prop-types' -import sharedMessages from '@ttn-lw/lib/shared-messages' -import { isNotFoundError, isTranslated } from '@ttn-lw/lib/errors/utils' - -import style from './gateway-connection.styl' - -const m = defineMessages({ - lastSeenAvailableTooltip: - 'The elapsed time since the network registered the last activity of this gateway. This is determined from received uplinks, or sent status messages of this gateway.', - disconnectedTooltip: - 'The gateway has currently no TCP connection established with the Gateway Server. For (rare) UDP based gateways, this can also mean that the gateway initiated no pull/push data request within the last 30 seconds.', - connectedTooltip: - 'This gateway is connected to the Gateway Server but the network has not registered any activity (sent uplinks or status messages) from it yet.', - otherClusterTooltip: - 'This gateway is connected to an external Gateway Server that is not handling messages for this cluster. You will hence not be able to see any activity from this gateway.', - messageCountTooltip: - 'The amount of received uplinks and sent downlinks of this gateway since the last (re)connect. Note that some gateway types reconnect frequently causing the counter to be reset.', -}) - -const GatewayConnection = props => { - const { - startStatistics, - stopStatistics, - statistics, - error, - fetching, - lastSeen, - isOtherCluster, - className, - } = props - - useEffect(() => { - startStatistics() - return () => { - stopStatistics() - } - }, [startStatistics, stopStatistics]) - - const status = useMemo(() => { - const statsNotFound = Boolean(error) && isNotFoundError(error) - const isDisconnected = Boolean(statistics) && Boolean(statistics.disconnected_at) - const isFetching = !Boolean(statistics) && fetching - const isUnavailable = Boolean(error) && Boolean(error.message) && isTranslated(error.message) - const hasStatistics = Boolean(statistics) - const hasLastSeen = Boolean(lastSeen) - - let statusIndicator = null - let message = null - let tooltipMessage = undefined - let docPath = '/getting-started/console/troubleshooting' - let docTitle = sharedMessages.troubleshooting - - if (statsNotFound) { - statusIndicator = 'bad' - message = sharedMessages.disconnected - tooltipMessage = m.disconnectedTooltip - docPath = '/gateways/troubleshooting/#my-gateway-wont-connect-what-do-i-do' - } else if (isDisconnected) { - tooltipMessage = m.disconnectedTooltip - docPath = '/gateways/troubleshooting/#my-gateway-wont-connect-what-do-i-do' - } else if (isFetching) { - statusIndicator = 'mediocre' - message = sharedMessages.connecting - } else if (isUnavailable) { - statusIndicator = 'unknown' - message = error.message - if (isOtherCluster) { - tooltipMessage = m.otherClusterTooltip - docPath = '/gateways/troubleshooting/#my-gateway-shows-a-other-cluster-status-why' - } - } else if (hasStatistics) { - message = sharedMessages.connected - statusIndicator = 'good' - if (hasLastSeen) { - tooltipMessage = m.lastSeenAvailableTooltip - } else { - docPath = - 'gateways/troubleshooting/#my-gateway-is-shown-as-connected-in-the-console-but-i-dont-see-any-events-including-the-gateway-connection-stats-what-do-i-do' - tooltipMessage = m.connectedTooltip - } - docTitle = sharedMessages.moreInformation - } else { - message = sharedMessages.unknown - statusIndicator = 'unknown' - docPath = '/gateways/troubleshooting' - } - - let node - - if (isDisconnected) { - node = ( - - - - ) - } else if (statusIndicator === 'good' && hasLastSeen) { - node = ( - - - - ) - } else { - node = ( - - - - ) - } - - if (tooltipMessage) { - return ( - } - children={node} - /> - ) - } - - return node - }, [error, fetching, isOtherCluster, lastSeen, statistics]) - - const messages = useMemo(() => { - if (!statistics) { - return null - } - - const uplinks = statistics.uplink_count || '0' - const downlinks = statistics.downlink_count || '0' - - const uplinkCount = parseInt(uplinks) || 0 - const downlinkCount = parseInt(downlinks) || 0 - - return ( - }> -
- - - - - - - - -
-
- ) - }, [statistics]) - - return ( -
- {messages} - {status} -
- ) -} - -GatewayConnection.propTypes = { - className: PropTypes.string, - error: PropTypes.oneOfType([PropTypes.error, PropTypes.shape({ message: PropTypes.message })]), - fetching: PropTypes.bool, - isOtherCluster: PropTypes.bool.isRequired, - lastSeen: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, // Support timestamps. - PropTypes.instanceOf(Date), - ]), - startStatistics: PropTypes.func.isRequired, - statistics: PropTypes.gatewayStats, - stopStatistics: PropTypes.func.isRequired, -} - -GatewayConnection.defaultProps = { - className: undefined, - fetching: false, - error: null, - statistics: null, - lastSeen: undefined, -} - -export default GatewayConnection diff --git a/pkg/webui/console/containers/gateway-connection/index.js b/pkg/webui/console/containers/gateway-connection/index.js index 642d24c74a..b95105dec4 100644 --- a/pkg/webui/console/containers/gateway-connection/index.js +++ b/pkg/webui/console/containers/gateway-connection/index.js @@ -1,4 +1,4 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,9 +12,209 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Connection from './gateway-connection' -import connect from './connect' +import React, { useEffect, useMemo } from 'react' +import classnames from 'classnames' +import { FormattedNumber, defineMessages } from 'react-intl' +import { useDispatch, useSelector } from 'react-redux' -const ConnectedGatewayConnection = connect(Connection) +import Status from '@ttn-lw/components/status' +import Icon from '@ttn-lw/components/icon' +import DocTooltip from '@ttn-lw/components/tooltip/doc' +import Tooltip from '@ttn-lw/components/tooltip' -export { ConnectedGatewayConnection as default, Connection } +import Message from '@ttn-lw/lib/components/message' + +import LastSeen from '@console/components/last-seen' + +import useConnectionReactor from '@console/containers/gateway-connection/useConnectionReactor' + +import PropTypes from '@ttn-lw/lib/prop-types' +import sharedMessages from '@ttn-lw/lib/shared-messages' +import { isNotFoundError, isTranslated } from '@ttn-lw/lib/errors/utils' +import { selectGsConfig } from '@ttn-lw/lib/selectors/env' +import getHostFromUrl from '@ttn-lw/lib/host-from-url' + +import { startGatewayStatistics, stopGatewayStatistics } from '@console/store/actions/gateways' + +import { + selectGatewayById, + selectGatewayStatistics, + selectGatewayStatisticsError, + selectGatewayStatisticsIsFetching, +} from '@console/store/selectors/gateways' +import { selectGatewayLastSeen } from '@console/store/selectors/gateway-status' + +import style from './gateway-connection.styl' + +const m = defineMessages({ + lastSeenAvailableTooltip: + 'The elapsed time since the network registered the last activity of this gateway. This is determined from received uplinks, or sent status messages of this gateway.', + disconnectedTooltip: + 'The gateway has currently no TCP connection established with the Gateway Server. For (rare) UDP based gateways, this can also mean that the gateway initiated no pull/push data request within the last 30 seconds.', + connectedTooltip: + 'This gateway is connected to the Gateway Server but the network has not registered any activity (sent uplinks or status messages) from it yet.', + otherClusterTooltip: + 'This gateway is connected to an external Gateway Server that is not handling messages for this cluster. You will hence not be able to see any activity from this gateway.', + messageCountTooltip: + 'The amount of received uplinks and sent downlinks of this gateway since the last (re)connect. Note that some gateway types reconnect frequently causing the counter to be reset.', +}) + +const GatewayConnection = props => { + const { className, gtwId } = props + + const gateway = useSelector(state => selectGatewayById(state, gtwId)) + const gsConfig = selectGsConfig() + const consoleGsAddress = getHostFromUrl(gsConfig.base_url) + const gatewayServerAddress = getHostFromUrl(gateway.gateway_server_address) + const statistics = useSelector(selectGatewayStatistics) + const error = useSelector(selectGatewayStatisticsError) + const fetching = useSelector(selectGatewayStatisticsIsFetching) + const lastSeen = useSelector(selectGatewayLastSeen) + const isOtherCluster = consoleGsAddress !== gatewayServerAddress + + const dispatch = useDispatch() + + useConnectionReactor(gtwId) + + useEffect(() => { + dispatch(startGatewayStatistics(gtwId)) + return () => { + dispatch(stopGatewayStatistics()) + } + }, [dispatch, gtwId]) + + const status = useMemo(() => { + const statsNotFound = Boolean(error) && isNotFoundError(error) + const isDisconnected = Boolean(statistics) && Boolean(statistics.disconnected_at) + const isFetching = !Boolean(statistics) && fetching + const isUnavailable = Boolean(error) && Boolean(error.message) && isTranslated(error.message) + const hasStatistics = Boolean(statistics) + const hasLastSeen = Boolean(lastSeen) + + let statusIndicator = null + let message = null + let tooltipMessage = undefined + let docPath = '/getting-started/console/troubleshooting' + let docTitle = sharedMessages.troubleshooting + + if (statsNotFound) { + statusIndicator = 'bad' + message = sharedMessages.disconnected + tooltipMessage = m.disconnectedTooltip + docPath = '/gateways/troubleshooting/#my-gateway-wont-connect-what-do-i-do' + } else if (isDisconnected) { + tooltipMessage = m.disconnectedTooltip + docPath = '/gateways/troubleshooting/#my-gateway-wont-connect-what-do-i-do' + } else if (isFetching) { + statusIndicator = 'mediocre' + message = sharedMessages.connecting + } else if (isUnavailable) { + statusIndicator = 'unknown' + message = error.message + if (isOtherCluster) { + tooltipMessage = m.otherClusterTooltip + docPath = '/gateways/troubleshooting/#my-gateway-shows-a-other-cluster-status-why' + } + } else if (hasStatistics) { + message = sharedMessages.connected + statusIndicator = 'good' + if (hasLastSeen) { + tooltipMessage = m.lastSeenAvailableTooltip + } else { + docPath = + 'gateways/troubleshooting/#my-gateway-is-shown-as-connected-in-the-console-but-i-dont-see-any-events-including-the-gateway-connection-stats-what-do-i-do' + tooltipMessage = m.connectedTooltip + } + docTitle = sharedMessages.moreInformation + } else { + message = sharedMessages.unknown + statusIndicator = 'unknown' + docPath = '/gateways/troubleshooting' + } + + let node + + if (isDisconnected) { + node = ( + + + + ) + } else if (statusIndicator === 'good' && hasLastSeen) { + node = ( + + + + ) + } else { + node = ( + + + + ) + } + + if (tooltipMessage) { + return ( + } + children={node} + /> + ) + } + + return node + }, [error, fetching, isOtherCluster, lastSeen, statistics]) + + const messages = useMemo(() => { + if (!statistics) { + return null + } + + const uplinks = statistics.uplink_count || '0' + const downlinks = statistics.downlink_count || '0' + + const uplinkCount = parseInt(uplinks) || 0 + const downlinkCount = parseInt(downlinks) || 0 + + return ( + }> +
+ + + + + + + + +
+
+ ) + }, [statistics]) + + return ( +
+ {messages} + {status} +
+ ) +} + +GatewayConnection.propTypes = { + className: PropTypes.string, + gtwId: PropTypes.string.isRequired, +} + +GatewayConnection.defaultProps = { + className: undefined, +} + +export default GatewayConnection diff --git a/pkg/webui/console/containers/gateway-connection/useConnectionReactor.js b/pkg/webui/console/containers/gateway-connection/useConnectionReactor.js new file mode 100644 index 0000000000..0ae6826d93 --- /dev/null +++ b/pkg/webui/console/containers/gateway-connection/useConnectionReactor.js @@ -0,0 +1,32 @@ +import { useEffect, useRef } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { + isGsDownlinkSendEvent, + isGsStatusReceiveEvent, + isGsUplinkReceiveEvent, +} from '@ttn-lw/lib/selectors/event' + +import { updateGatewayStatistics } from '@console/store/actions/gateways' + +import { selectLatestGatewayEvent } from '@console/store/selectors/gateways' + +const useConnectionReactor = gtwId => { + const latestEvent = useSelector(state => selectLatestGatewayEvent(state, gtwId)) + const dispatch = useDispatch() + const prevEvent = useRef(null) + + useEffect(() => { + if (Boolean(latestEvent) && latestEvent !== prevEvent.current) { + const { name } = latestEvent + const isHeartBeatEvent = + isGsDownlinkSendEvent(name) || isGsUplinkReceiveEvent(name) || isGsStatusReceiveEvent(name) + + if (isHeartBeatEvent) { + dispatch(updateGatewayStatistics(gtwId)) + } + prevEvent.current = latestEvent + } + }, [dispatch, gtwId, latestEvent]) +} +export default useConnectionReactor diff --git a/pkg/webui/console/containers/gateway-events/index.js b/pkg/webui/console/containers/gateway-events/index.js index c9283fb0b6..a876b98e67 100644 --- a/pkg/webui/console/containers/gateway-events/index.js +++ b/pkg/webui/console/containers/gateway-events/index.js @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React from 'react' -import { connect } from 'react-redux' +import React, { useCallback, useMemo } from 'react' +import { useDispatch, useSelector } from 'react-redux' import Events from '@console/components/events' -import withFeatureRequirement from '@console/lib/components/with-feature-requirement' +import Require from '@console/lib/components/require' import PropTypes from '@ttn-lw/lib/prop-types' @@ -38,76 +38,74 @@ import { } from '@console/store/selectors/gateways' const GatewayEvents = props => { - const { - gtwId, - events, - widget, - paused, - onPauseToggle, - onClear, - onFilterChange, - truncated, - filter, - } = props - - if (widget) { + const { gtwId, widget } = props + + const events = useSelector(state => selectGatewayEvents(state, gtwId)) + const paused = useSelector(state => selectGatewayEventsPaused(state, gtwId)) + const truncated = useSelector(state => selectGatewayEventsTruncated(state, gtwId)) + const filter = useSelector(state => selectGatewayEventsFilter(state, gtwId)) + + const dispatch = useDispatch() + + const onClear = useCallback(() => { + dispatch(clearGatewayEventsStream(gtwId)) + }, [dispatch, gtwId]) + + const onPauseToggle = useCallback( + paused => { + if (paused) { + dispatch(resumeGatewayEventsStream(gtwId)) + return + } + dispatch(pauseGatewayEventsStream(gtwId)) + }, + [dispatch, gtwId], + ) + + const onFilterChange = useCallback( + filterId => { + dispatch(setGatewayEventsFilter(gtwId, filterId)) + }, + [dispatch, gtwId], + ) + + const content = useMemo(() => { + if (widget) { + return ( + + ) + } + return ( - + ) - } - - return ( - - ) + }, [events, filter, gtwId, onClear, onFilterChange, onPauseToggle, paused, truncated, widget]) + + return {content} } GatewayEvents.propTypes = { - events: PropTypes.events, - filter: PropTypes.eventFilter, gtwId: PropTypes.string.isRequired, - onClear: PropTypes.func.isRequired, - onFilterChange: PropTypes.func.isRequired, - onPauseToggle: PropTypes.func.isRequired, - paused: PropTypes.bool.isRequired, - truncated: PropTypes.bool.isRequired, widget: PropTypes.bool, } GatewayEvents.defaultProps = { widget: false, - events: [], - filter: undefined, } -export default withFeatureRequirement(mayViewGatewayEvents)( - connect( - (state, props) => { - const { gtwId } = props - - return { - events: selectGatewayEvents(state, gtwId), - paused: selectGatewayEventsPaused(state, gtwId), - truncated: selectGatewayEventsTruncated(state, gtwId), - filter: selectGatewayEventsFilter(state, gtwId), - } - }, - (dispatch, ownProps) => ({ - onClear: () => dispatch(clearGatewayEventsStream(ownProps.gtwId)), - onPauseToggle: paused => - paused - ? dispatch(resumeGatewayEventsStream(ownProps.gtwId)) - : dispatch(pauseGatewayEventsStream(ownProps.gtwId)), - onFilterChange: filterId => dispatch(setGatewayEventsFilter(ownProps.gtwId, filterId)), - }), - )(GatewayEvents), -) +export default GatewayEvents diff --git a/pkg/webui/console/containers/gateway-location-form/connect.js b/pkg/webui/console/containers/gateway-location-form/connect.js deleted file mode 100644 index cf67f307ac..0000000000 --- a/pkg/webui/console/containers/gateway-location-form/connect.js +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' - -import { updateGateway } from '@console/store/actions/gateways' - -import { selectSelectedGateway, selectSelectedGatewayId } from '@console/store/selectors/gateways' - -const mapStateToProps = state => ({ - gateway: selectSelectedGateway(state), - gatewayId: selectSelectedGatewayId(state), -}) - -const mapDispatchToProps = { - updateGateway: attachPromise(updateGateway), -} - -export default Component => connect(mapStateToProps, mapDispatchToProps)(Component) diff --git a/pkg/webui/console/containers/gateway-location-form/gateway-location-form.js b/pkg/webui/console/containers/gateway-location-form/gateway-location-form.js deleted file mode 100644 index 520ee98241..0000000000 --- a/pkg/webui/console/containers/gateway-location-form/gateway-location-form.js +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React, { useCallback, useState } from 'react' -import { defineMessages } from 'react-intl' - -import Checkbox from '@ttn-lw/components/checkbox' -import Form from '@ttn-lw/components/form' -import Radio from '@ttn-lw/components/radio-button' - -import LocationForm, { hasLocationSet } from '@console/components/location-form' - -import Yup from '@ttn-lw/lib/yup' -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 { latitude as latitudeRegexp, longitude as longitudeRegexp } from '@console/lib/regexp' - -const m = defineMessages({ - updateLocationFromStatus: 'Update from status messages', - updateLocationFromStatusDescription: - 'Update the location of this gateway based on incoming status messages', - setGatewayLocation: 'Gateway antenna location settings', - locationSource: 'Location source', - locationPrivacy: 'Location privacy', - placement: 'Placement', - indoor: 'Indoor', - outdoor: 'Outdoor', - locationFromStatusMessage: 'Location set automatically from status messages', - setLocationManually: 'Set location manually', - noLocationSetInfo: 'This gateway has no location information set', -}) - -const validationSchema = Yup.object().shape({ - latitude: Yup.number().when('update_location_from_status', { - is: false, - then: schema => - schema - .required(sharedMessages.validateRequired) - .test('is-valid-latitude', sharedMessages.validateLatitude, value => - latitudeRegexp.test(String(value)), - ), - otherwise: schema => schema.strip(), - }), - longitude: Yup.number().when('update_location_from_status', { - is: false, - then: schema => - schema - .required(sharedMessages.validateRequired) - .test('is-valid-longitude', sharedMessages.validateLongitude, value => - longitudeRegexp.test(String(value)), - ), - otherwise: schema => schema.strip(), - }), - altitude: Yup.number().when('update_location_from_status', { - is: false, - then: schema => - schema.integer(sharedMessages.validateInt32).required(sharedMessages.validateRequired), - otherwise: schema => schema.strip(), - }), - location_public: Yup.bool(), - update_location_from_status: Yup.bool(), - placement: Yup.string().oneOf(['PLACEMENT_UNKNOWN', 'INDOOR', 'OUTDOOR']), -}) - -const getRegistryLocation = antennas => { - let registryLocation - if (antennas) { - for (const key of Object.keys(antennas)) { - if ( - antennas[key].location !== null && - typeof antennas[key].location === 'object' && - antennas[key].location.source === 'SOURCE_REGISTRY' - ) { - registryLocation = { antenna: antennas[key], key } - break - } else { - registryLocation = { antenna: antennas[key], key } - } - } - } - return registryLocation -} - -const GatewayLocationForm = ({ gateway, gatewayId, updateGateway }) => { - const registryLocation = getRegistryLocation(gateway.antennas) - const initialValues = { - placement: - registryLocation && registryLocation.antenna.placement - ? registryLocation.antenna.placement - : 'PLACEMENT_UNKNOWN', - location_public: gateway.location_public || false, - update_location_from_status: gateway.update_location_from_status || false, - ...(hasLocationSet(registryLocation?.antenna?.location) - ? registryLocation.antenna.location - : { - latitude: undefined, - longitude: undefined, - altitude: undefined, - }), - } - - const handleSubmit = useCallback( - async values => { - const { update_location_from_status, location_public, placement, ...location } = values - const patch = { - location_public, - update_location_from_status, - } - - const registryLocation = getRegistryLocation(gateway.antennas) - if (!values.update_location_from_status) { - if (registryLocation) { - // Update old location value. - patch.antennas = [...gateway.antennas] - patch.antennas[registryLocation.key].location = { - ...registryLocation.antenna.location, - ...location, - } - patch.antennas[registryLocation.key].placement = placement - } else { - // Create new location value. - patch.antennas = [ - { - gain: 0, - location: { - ...values, - accuracy: 0, - source: 'SOURCE_REGISTRY', - }, - placement, - }, - ] - } - } else if (registryLocation) { - patch.antennas = gateway.antennas.map(antenna => { - const { location, ...rest } = antenna - return rest - }) - patch.antennas[registryLocation.key].placement = values.placement - } else { - patch.antennas = [{ gain: 0, placement: values.placement }] - } - - return updateGateway(gatewayId, patch) - }, - [gateway, gatewayId, updateGateway], - ) - - const handleDelete = useCallback( - async deleteAll => { - const registryLocation = getRegistryLocation(gateway.antennas) - - if (deleteAll) { - return updateGateway(gatewayId, { antennas: [] }) - } - - const patch = { - antennas: [...gateway.antennas], - } - patch.antennas.splice(registryLocation.key, 1) - - return updateGateway(gatewayId, patch) - }, - [gateway, gatewayId, updateGateway], - ) - - const [updateLocationFromStatus, setUpdateLocationFromStatus] = useState( - initialValues.update_location_from_status, - ) - - const handleUpdateLocationFromStatusChange = useCallback(useAutomaticUpdates => { - setUpdateLocationFromStatus(useAutomaticUpdates) - }, []) - - return ( - - - - - - - - - - - - - ) -} - -GatewayLocationForm.propTypes = { - gateway: PropTypes.gateway.isRequired, - gatewayId: PropTypes.string.isRequired, - updateGateway: PropTypes.func.isRequired, -} - -export default GatewayLocationForm diff --git a/pkg/webui/console/containers/gateway-location-form/index.js b/pkg/webui/console/containers/gateway-location-form/index.js index badd87cdf6..b72ab0fcf2 100644 --- a/pkg/webui/console/containers/gateway-location-form/index.js +++ b/pkg/webui/console/containers/gateway-location-form/index.js @@ -1,4 +1,4 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,230 @@ // See the License for the specific language governing permissions and // limitations under the License. -import GatewayLocationForm from './gateway-location-form' -import connect from './connect' +import React, { useCallback, useState } from 'react' +import { defineMessages } from 'react-intl' +import { useDispatch, useSelector } from 'react-redux' -export default connect(GatewayLocationForm) +import Checkbox from '@ttn-lw/components/checkbox' +import Form from '@ttn-lw/components/form' +import Radio from '@ttn-lw/components/radio-button' + +import LocationForm, { hasLocationSet } from '@console/components/location-form' + +import Yup from '@ttn-lw/lib/yup' +import sharedMessages from '@ttn-lw/lib/shared-messages' +import tooltipIds from '@ttn-lw/lib/constants/tooltip-ids' +import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' + +import { latitude as latitudeRegexp, longitude as longitudeRegexp } from '@console/lib/regexp' + +import { updateGateway } from '@console/store/actions/gateways' + +import { selectSelectedGateway, selectSelectedGatewayId } from '@console/store/selectors/gateways' + +const m = defineMessages({ + updateLocationFromStatus: 'Update from status messages', + updateLocationFromStatusDescription: + 'Update the location of this gateway based on incoming status messages', + setGatewayLocation: 'Gateway antenna location settings', + locationSource: 'Location source', + locationPrivacy: 'Location privacy', + placement: 'Placement', + indoor: 'Indoor', + outdoor: 'Outdoor', + locationFromStatusMessage: 'Location set automatically from status messages', + setLocationManually: 'Set location manually', + noLocationSetInfo: 'This gateway has no location information set', +}) + +const validationSchema = Yup.object().shape({ + latitude: Yup.number().when('update_location_from_status', { + is: false, + then: schema => + schema + .required(sharedMessages.validateRequired) + .test('is-valid-latitude', sharedMessages.validateLatitude, value => + latitudeRegexp.test(String(value)), + ), + otherwise: schema => schema.strip(), + }), + longitude: Yup.number().when('update_location_from_status', { + is: false, + then: schema => + schema + .required(sharedMessages.validateRequired) + .test('is-valid-longitude', sharedMessages.validateLongitude, value => + longitudeRegexp.test(String(value)), + ), + otherwise: schema => schema.strip(), + }), + altitude: Yup.number().when('update_location_from_status', { + is: false, + then: schema => + schema.integer(sharedMessages.validateInt32).required(sharedMessages.validateRequired), + otherwise: schema => schema.strip(), + }), + location_public: Yup.bool(), + update_location_from_status: Yup.bool(), + placement: Yup.string().oneOf(['PLACEMENT_UNKNOWN', 'INDOOR', 'OUTDOOR']), +}) + +const getRegistryLocation = antennas => { + let registryLocation + if (antennas) { + for (const key of Object.keys(antennas)) { + if ( + antennas[key].location !== null && + typeof antennas[key].location === 'object' && + antennas[key].location.source === 'SOURCE_REGISTRY' + ) { + registryLocation = { antenna: antennas[key], key } + break + } else { + registryLocation = { antenna: antennas[key], key } + } + } + } + return registryLocation +} + +const GatewayLocationForm = () => { + const gateway = useSelector(selectSelectedGateway) + const gatewayId = useSelector(selectSelectedGatewayId) + const dispatch = useDispatch() + const registryLocation = getRegistryLocation(gateway.antennas) + const initialValues = { + placement: + registryLocation && registryLocation.antenna.placement + ? registryLocation.antenna.placement + : 'PLACEMENT_UNKNOWN', + location_public: gateway.location_public || false, + update_location_from_status: gateway.update_location_from_status || false, + ...(hasLocationSet(registryLocation?.antenna?.location) + ? registryLocation.antenna.location + : { + latitude: undefined, + longitude: undefined, + altitude: undefined, + }), + } + + const handleSubmit = useCallback( + async values => { + const { update_location_from_status, location_public, placement, ...location } = values + const patch = { + location_public, + update_location_from_status, + } + + const registryLocation = getRegistryLocation(gateway.antennas) + if (!values.update_location_from_status) { + if (registryLocation) { + // Update old location value. + patch.antennas = [...gateway.antennas] + patch.antennas[registryLocation.key].location = { + ...registryLocation.antenna.location, + ...location, + } + patch.antennas[registryLocation.key].placement = placement + } else { + // Create new location value. + patch.antennas = [ + { + gain: 0, + location: { + ...values, + accuracy: 0, + source: 'SOURCE_REGISTRY', + }, + placement, + }, + ] + } + } else if (registryLocation) { + patch.antennas = gateway.antennas.map(antenna => { + const { location, ...rest } = antenna + return rest + }) + patch.antennas[registryLocation.key].placement = values.placement + } else { + patch.antennas = [{ gain: 0, placement: values.placement }] + } + + return dispatch(attachPromise(updateGateway(gatewayId, patch))) + }, + [dispatch, gateway.antennas, gatewayId], + ) + + const handleDelete = useCallback( + async deleteAll => { + const registryLocation = getRegistryLocation(gateway.antennas) + + if (deleteAll) { + return dispatch(attachPromise(updateGateway(gatewayId, { antennas: [] }))) + } + + const patch = { + antennas: [...gateway.antennas], + } + patch.antennas.splice(registryLocation.key, 1) + + return dispatch(attachPromise(updateGateway(gatewayId, patch))) + }, + [dispatch, gateway.antennas, gatewayId], + ) + + const [updateLocationFromStatus, setUpdateLocationFromStatus] = useState( + initialValues.update_location_from_status, + ) + + const handleUpdateLocationFromStatusChange = useCallback(useAutomaticUpdates => { + setUpdateLocationFromStatus(useAutomaticUpdates) + }, []) + + return ( + + + + + + + + + + + + + ) +} + +export default GatewayLocationForm diff --git a/pkg/webui/console/containers/organization-events/connect.js b/pkg/webui/console/containers/organization-events/connect.js deleted file mode 100644 index ec37e084ba..0000000000 --- a/pkg/webui/console/containers/organization-events/connect.js +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import { - clearOrganizationEventsStream, - pauseOrganizationEventsStream, - resumeOrganizationEventsStream, -} from '@console/store/actions/organizations' - -import { - selectOrganizationEvents, - selectOrganizationEventsPaused, - selectOrganizationEventsTruncated, -} from '@console/store/selectors/organizations' - -const mapStateToProps = (state, props) => { - const { orgId } = props - - return { - events: selectOrganizationEvents(state, orgId), - paused: selectOrganizationEventsPaused(state, orgId), - truncated: selectOrganizationEventsTruncated(state, orgId), - } -} - -const mapDispatchToProps = (dispatch, ownProps) => ({ - onClear: () => dispatch(clearOrganizationEventsStream(ownProps.orgId)), - onPauseToggle: paused => - paused - ? dispatch(resumeOrganizationEventsStream(ownProps.orgId)) - : dispatch(pauseOrganizationEventsStream(ownProps.orgId)), -}) - -export default Events => connect(mapStateToProps, mapDispatchToProps)(Events) diff --git a/pkg/webui/console/containers/organization-events/index.js b/pkg/webui/console/containers/organization-events/index.js index 98a82898da..a9f830c22e 100644 --- a/pkg/webui/console/containers/organization-events/index.js +++ b/pkg/webui/console/containers/organization-events/index.js @@ -12,9 +12,80 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Events from './organization-events' -import connect from './connect' +import React, { useCallback } from 'react' +import { useDispatch, useSelector } from 'react-redux' -const ConnectedOrganizationEvents = connect(Events) +import Events from '@console/components/events' -export { ConnectedOrganizationEvents as default, Events } +import PropTypes from '@ttn-lw/lib/prop-types' + +import { + clearOrganizationEventsStream, + pauseOrganizationEventsStream, + resumeOrganizationEventsStream, +} from '@console/store/actions/organizations' + +import { + selectOrganizationEvents, + selectOrganizationEventsPaused, + selectOrganizationEventsTruncated, +} from '@console/store/selectors/organizations' + +const OrganizationEvents = props => { + const { orgId, widget } = props + + const events = useSelector(state => selectOrganizationEvents(state, orgId)) + const paused = useSelector(state => selectOrganizationEventsPaused(state, orgId)) + const truncated = useSelector(state => selectOrganizationEventsTruncated(state, orgId)) + + const dispatch = useDispatch() + + const onPauseToggle = useCallback( + paused => { + if (paused) { + dispatch(resumeOrganizationEventsStream(orgId)) + return + } + dispatch(pauseOrganizationEventsStream(orgId)) + }, + [dispatch, orgId], + ) + + const onClear = useCallback(() => { + dispatch(clearOrganizationEventsStream(orgId)) + }, [dispatch, orgId]) + + if (widget) { + return ( + + ) + } + + return ( + + ) +} + +OrganizationEvents.propTypes = { + orgId: PropTypes.string.isRequired, + widget: PropTypes.bool, +} + +OrganizationEvents.defaultProps = { + widget: false, +} + +export default OrganizationEvents diff --git a/pkg/webui/console/containers/organization-events/organization-events.js b/pkg/webui/console/containers/organization-events/organization-events.js deleted file mode 100644 index 25b61e5add..0000000000 --- a/pkg/webui/console/containers/organization-events/organization-events.js +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' - -import Events from '@console/components/events' - -import PropTypes from '@ttn-lw/lib/prop-types' - -const OrganizationEvents = props => { - const { orgId, events, widget, paused, onPauseToggle, onClear, truncated } = props - - if (widget) { - return ( - - ) - } - - return ( - - ) -} - -OrganizationEvents.propTypes = { - events: PropTypes.events, - onClear: PropTypes.func.isRequired, - onPauseToggle: PropTypes.func.isRequired, - orgId: PropTypes.string.isRequired, - paused: PropTypes.bool.isRequired, - truncated: PropTypes.bool.isRequired, - widget: PropTypes.bool, -} - -OrganizationEvents.defaultProps = { - widget: false, - events: [], -} - -export default OrganizationEvents diff --git a/pkg/webui/console/containers/owners-select/connect.js b/pkg/webui/console/containers/owners-select/connect.js deleted file mode 100644 index dcd1f08cd7..0000000000 --- a/pkg/webui/console/containers/owners-select/connect.js +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' - -import { getOrganizationsList } from '@console/store/actions/organizations' - -import { - selectOrganizationsFetching, - selectOrganizationsError, - selectOrganizations, -} from '@console/store/selectors/organizations' -import { selectUser } from '@console/store/selectors/logout' - -export default OwnersSelect => - connect( - state => ({ - user: selectUser(state), - organizations: selectOrganizations(state), - error: selectOrganizationsError(state), - fetching: selectOrganizationsFetching(state), - }), - { getOrganizationsList }, - )(OwnersSelect) diff --git a/pkg/webui/console/containers/owners-select/index.js b/pkg/webui/console/containers/owners-select/index.js index c20c89c993..b9454b96ec 100644 --- a/pkg/webui/console/containers/owners-select/index.js +++ b/pkg/webui/console/containers/owners-select/index.js @@ -12,9 +12,87 @@ // See the License for the specific language governing permissions and // limitations under the License. -import connect from './connect' -import OwnersSelect from './owners-select' +import React from 'react' +import { defineMessages } from 'react-intl' +import { useSelector } from 'react-redux' -const ConnectedOwnersSelect = connect(OwnersSelect) +import Select from '@ttn-lw/components/select' +import Field from '@ttn-lw/components/form/field' -export { ConnectedOwnersSelect as default, OwnersSelect } +import { getOrganizationId, getUserId } from '@ttn-lw/lib/selectors/id' +import PropTypes from '@ttn-lw/lib/prop-types' + +import { selectUser } from '@console/store/selectors/logout' +import { + selectOrganizations, + selectOrganizationsError, + selectOrganizationsFetching, +} from '@console/store/selectors/organizations' + +const m = defineMessages({ + title: 'Owner', + warning: 'There was an error and the list of organizations could not be displayed', +}) + +const OwnersSelect = props => { + const { autoFocus, menuPlacement, name, onChange, required } = props + + const user = useSelector(selectUser) + const organizations = useSelector(selectOrganizations) + const error = useSelector(selectOrganizationsError) + const fetching = useSelector(selectOrganizationsFetching) + + const options = React.useMemo(() => { + const usrOption = { label: getUserId(user), value: getUserId(user) } + const orgsOptions = organizations.map(org => ({ + label: getOrganizationId(org), + value: getOrganizationId(org), + })) + + return [usrOption, ...orgsOptions] + }, [user, organizations]) + const handleChange = React.useCallback( + value => { + onChange(options.find(option => option.value === value)) + }, + [onChange, options], + ) + + // Do not show the input when there are no alternative options. + if (options.length === 1) { + return null + } + + return ( + + ) +} + +OwnersSelect.propTypes = { + autoFocus: PropTypes.bool, + menuPlacement: PropTypes.oneOf(['top', 'bottom', 'auto']), + name: PropTypes.string.isRequired, + onChange: PropTypes.func, + required: PropTypes.bool, +} + +OwnersSelect.defaultProps = { + autoFocus: false, + onChange: () => null, + menuPlacement: 'auto', + required: false, +} + +export default OwnersSelect diff --git a/pkg/webui/console/containers/owners-select/owners-select.js b/pkg/webui/console/containers/owners-select/owners-select.js deleted file mode 100644 index f8ad538978..0000000000 --- a/pkg/webui/console/containers/owners-select/owners-select.js +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' -import { defineMessages } from 'react-intl' - -import Select from '@ttn-lw/components/select' -import Field from '@ttn-lw/components/form/field' - -import { getOrganizationId, getUserId } from '@ttn-lw/lib/selectors/id' -import PropTypes from '@ttn-lw/lib/prop-types' - -const m = defineMessages({ - title: 'Owner', - warning: 'There was an error and the list of organizations could not be displayed', -}) - -const OwnersSelect = props => { - const { - autoFocus, - error, - fetching, - menuPlacement, - name, - onChange, - organizations, - required, - user, - } = props - - const options = React.useMemo(() => { - const usrOption = { label: getUserId(user), value: getUserId(user) } - const orgsOptions = organizations.map(org => ({ - label: getOrganizationId(org), - value: getOrganizationId(org), - })) - - return [usrOption, ...orgsOptions] - }, [user, organizations]) - const handleChange = React.useCallback( - value => { - onChange(options.find(option => option.value === value)) - }, - [onChange, options], - ) - - // Do not show the input when there are no alternative options. - if (options.length === 1) { - return null - } - - return ( - - ) -} - -OwnersSelect.propTypes = { - autoFocus: PropTypes.bool, - error: PropTypes.error, - fetching: PropTypes.bool, - menuPlacement: PropTypes.oneOf(['top', 'bottom', 'auto']), - name: PropTypes.string.isRequired, - onChange: PropTypes.func, - organizations: PropTypes.arrayOf(PropTypes.organization).isRequired, - required: PropTypes.bool, - user: PropTypes.user.isRequired, -} - -OwnersSelect.defaultProps = { - autoFocus: false, - error: undefined, - fetching: false, - onChange: () => null, - menuPlacement: 'auto', - required: false, -} - -export default OwnersSelect diff --git a/pkg/webui/console/lib/components/with-feature-requirement.js b/pkg/webui/console/lib/components/with-feature-requirement.js deleted file mode 100644 index a4aad75af5..0000000000 --- a/pkg/webui/console/lib/components/with-feature-requirement.js +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright © 2019 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react' - -import Require from './require' - -/** - * `withFeatureRequirement` is a HOC that checks whether the current has the - * necessary authorization to view the wrapped component. It can be set up to - * either redirect to another route, to render something different or to not - * render anything if the requirement is not met. - * - * @param {object} featureCheck - The feature check object containing the right - * selector as well as the check itself. - * @param {object} otherwise - A configuration object determining what should be - * rendered if the requirement was not met. If not set, nothing will be - * rendered. - * @returns {Function} - An instance of the `withFeatureRequirement` HOC. - */ -const withFeatureRequirement = (featureCheck, otherwise) => Component => - class WithFeatureRequirement extends React.Component { - constructor(props) { - super(props) - - if ( - typeof otherwise === 'object' && - 'redirect' in otherwise && - typeof otherwise.redirect === 'function' - ) { - this.otherwise = { ...otherwise, redirect: otherwise.redirect(props) } - } else { - this.otherwise = otherwise - } - } - - render() { - return ( - - - - ) - } - } - -export default withFeatureRequirement diff --git a/pkg/webui/console/views/applications/index.js b/pkg/webui/console/views/applications/index.js index e6df665061..a22737072a 100644 --- a/pkg/webui/console/views/applications/index.js +++ b/pkg/webui/console/views/applications/index.js @@ -21,7 +21,7 @@ import { useBreadcrumbs } from '@ttn-lw/components/breadcrumbs/context' import GenericNotFound from '@ttn-lw/lib/components/full-view-error/not-found' import ValidateRouteParam from '@ttn-lw/lib/components/validate-route-param' -import withFeatureRequirement from '@console/lib/components/with-feature-requirement' +import Require from '@console/lib/components/require' import Application from '@console/views/application' import ApplicationsList from '@console/views/applications-list' @@ -36,15 +36,17 @@ const Applications = () => { useBreadcrumbs('apps', ) return ( - - - - } - /> - - + + + + + } + /> + + + ) } -export default withFeatureRequirement(mayViewApplications, { redirect: '/' })(Applications) +export default Applications diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 4ea7546a91..8f4e19e56c 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -205,16 +205,16 @@ "console.components.device-import-form.index.inputMethodManual": "Enter LoRaWAN versions and frequency plan manually", "console.components.device-import-form.index.fallbackValues": "Fallback values", "console.components.device-import-form.index.noFallback": "Do not set any fallback values", - "console.components.downlink-form.downlink-form.insertMode": "Insert Mode", - "console.components.downlink-form.downlink-form.payloadType": "Payload type", - "console.components.downlink-form.downlink-form.bytes": "Bytes", - "console.components.downlink-form.downlink-form.replace": "Replace downlink queue", - "console.components.downlink-form.downlink-form.push": "Push to downlink queue (append)", - "console.components.downlink-form.downlink-form.scheduleDownlink": "Schedule downlink", - "console.components.downlink-form.downlink-form.downlinkSuccess": "Downlink scheduled", - "console.components.downlink-form.downlink-form.bytesPayloadDescription": "The desired payload bytes of the downlink message", - "console.components.downlink-form.downlink-form.jsonPayloadDescription": "The decoded payload of the downlink message", - "console.components.downlink-form.downlink-form.invalidSessionWarning": "Downlinks can only be scheduled for end devices with a valid session. Please make sure your end device is properly connected to the network.", + "console.components.downlink-form.index.insertMode": "Insert Mode", + "console.components.downlink-form.index.payloadType": "Payload type", + "console.components.downlink-form.index.bytes": "Bytes", + "console.components.downlink-form.index.replace": "Replace downlink queue", + "console.components.downlink-form.index.push": "Push to downlink queue (append)", + "console.components.downlink-form.index.scheduleDownlink": "Schedule downlink", + "console.components.downlink-form.index.downlinkSuccess": "Downlink scheduled", + "console.components.downlink-form.index.bytesPayloadDescription": "The desired payload bytes of the downlink message", + "console.components.downlink-form.index.jsonPayloadDescription": "The decoded payload of the downlink message", + "console.components.downlink-form.index.invalidSessionWarning": "Downlinks can only be scheduled for end devices with a valid session. Please make sure your end device is properly connected to the network.", "console.components.events.messages.MACPayload": "MAC payload", "console.components.events.messages.devAddr": "DevAddr", "console.components.events.messages.fPort": "FPort", @@ -342,6 +342,7 @@ "console.components.pubsub-form.messages.mqttClientIdPlaceholder": "my-client-id", "console.components.pubsub-form.messages.mqttServerUrlPlaceholder": "mqtts://example.com", "console.components.pubsub-form.messages.subscribeQos": "Subscribe QoS", + "console.components.pubsub-form.messages.providerDescription": "Changing the Pub/Sub provider has been disabled by an administrator", "console.components.pubsub-form.messages.publishQos": "Publish QoS", "console.components.pubsub-form.messages.tlsCa": "Root CA certificate", "console.components.pubsub-form.messages.tlsClientCert": "Client certificate", @@ -356,9 +357,9 @@ "console.components.routing-policy-form.index.saveDefaultPolicy": "Save default policy", "console.components.routing-policy-form.index.useSpecificPolicy": "Use network specific routing policy", "console.components.routing-policy-form.index.doNotUseAPolicy": "Do not use a routing policy for this network", - "console.components.uplink-form.uplink-form.simulateUplink": "Simulate uplink", - "console.components.uplink-form.uplink-form.payloadDescription": "The desired payload bytes of the uplink message", - "console.components.uplink-form.uplink-form.uplinkSuccess": "Uplink sent", + "console.components.uplink-form.index.simulateUplink": "Simulate uplink", + "console.components.uplink-form.index.payloadDescription": "The desired payload bytes of the uplink message", + "console.components.uplink-form.index.uplinkSuccess": "Uplink sent", "console.components.webhook-form.index.idPlaceholder": "my-new-webhook", "console.components.webhook-form.index.messageInfo": "For each enabled event type an optional path can be defined which will be appended to the base URL", "console.components.webhook-form.index.deleteWebhook": "Delete Webhook", @@ -488,48 +489,48 @@ "console.containers.device-onboarding-form.warning-tooltip.sessionDescription": "An ABP device is personalized with a session and MAC settings. These MAC settings are considered the current parameters and must match exactly the settings entered here. The Network Server uses desired parameters to change the MAC state with LoRaWAN MAC commands to the desired state. You can use the General Settings page to update the desired setting after you registered the end device.", "console.containers.device-payload-formatters.messages.defaultFormatter": "Click here to modify the default payload formatter for this application", "console.containers.device-payload-formatters.messages.mayNotViewLink": "You are not allowed to view link information of this application. This includes seeing the default payload formatter of this application.", - "console.containers.device-profile-section.device-card.device-card.productWebsite": "Product website", - "console.containers.device-profile-section.device-card.device-card.dataSheet": "Data sheet", - "console.containers.device-profile-section.device-card.device-card.classA": "Class A", - "console.containers.device-profile-section.device-card.device-card.classB": "Class B", - "console.containers.device-profile-section.device-card.device-card.classC": "Class C", + "console.containers.device-profile-section.device-card.index.productWebsite": "Product website", + "console.containers.device-profile-section.device-card.index.dataSheet": "Data sheet", + "console.containers.device-profile-section.device-card.index.classA": "Class A", + "console.containers.device-profile-section.device-card.index.classB": "Class B", + "console.containers.device-profile-section.device-card.index.classC": "Class C", "console.containers.device-profile-section.device-selection.band-select.index.title": "Profile (Region)", - "console.containers.device-profile-section.device-selection.brand-select.brand-select.title": "End device brand", - "console.containers.device-profile-section.device-selection.brand-select.brand-select.noOptionsMessage": "No matching brand found", - "console.containers.device-profile-section.device-selection.fw-version-select.fw-version-select.title": "Firmware Ver.", - "console.containers.device-profile-section.device-selection.hw-version-select.hw-version-select.title": "Hardware Ver.", - "console.containers.device-profile-section.device-selection.model-select.model-select.noOptionsMessage": "No matching model found", + "console.containers.device-profile-section.device-selection.brand-select.index.title": "End device brand", + "console.containers.device-profile-section.device-selection.brand-select.index.noOptionsMessage": "No matching brand found", + "console.containers.device-profile-section.device-selection.fw-version-select.index.title": "Firmware Ver.", + "console.containers.device-profile-section.device-selection.hw-version-select.index.title": "Hardware Ver.", + "console.containers.device-profile-section.device-selection.model-select.index.noOptionsMessage": "No matching model found", "console.containers.device-profile-section.hints.other-hint.hintTitle": "Your end device will be added soon!", "console.containers.device-profile-section.hints.other-hint.hintMessage": "We're sorry, but your device is not yet part of The LoRaWAN Device Repository. You can use enter end device specifics manually option above, using the information your end device manufacturer provided e.g. in the product's data sheet. Please also refer to our documentation on Adding Devices.", "console.containers.device-profile-section.hints.progress-hint.hintMessage": "Cannot find your exact end device? Get help here and try enter end device specifics manually option above.", "console.containers.device-profile-section.hints.progress-hint.hintNoSupportMessage": "Cannot find your exact end device? Try enter end device specifics manually option above.", "console.containers.device-template-format-select.index.title": "File format", "console.containers.device-template-format-select.index.warning": "End device template formats unavailable", - "console.containers.device-title-section.device-title-section.uplinkDownlinkTooltip": "The number of sent uplinks and received downlinks of this end device since the last frame counter reset.", - "console.containers.device-title-section.device-title-section.lastSeenAvailableTooltip": "The elapsed time since the network registered the last activity of this end device. This is determined from sent uplinks, confirmed downlinks or (re)join requests.{lineBreak}The last activity was received at {lastActivityInfo}", - "console.containers.device-title-section.device-title-section.noActivityTooltip": "The network has not registered any activity from this end device yet. This could mean that your end device has not sent any messages yet or only messages that cannot be handled by the network, e.g. due to a mismatch of EUIs or frequencies.", + "console.containers.device-title-section.index.uplinkDownlinkTooltip": "The number of sent uplinks and received downlinks of this end device since the last frame counter reset.", + "console.containers.device-title-section.index.lastSeenAvailableTooltip": "The elapsed time since the network registered the last activity of this end device. This is determined from sent uplinks, confirmed downlinks or (re)join requests.{lineBreak}The last activity was received at {lastActivityInfo}", + "console.containers.device-title-section.index.noActivityTooltip": "The network has not registered any activity from this end device yet. This could mean that your end device has not sent any messages yet or only messages that cannot be handled by the network, e.g. due to a mismatch of EUIs or frequencies.", "console.containers.devices-table.index.otherClusterTooltip": "This end device is registered on a different cluster (`{host}`). To access this device, use the Console of the cluster that this end device was registered on.", "console.containers.freq-plans-select.utils.warning": "Frequency plans unavailable", "console.containers.freq-plans-select.utils.none": "Do not set a frequency plan", "console.containers.freq-plans-select.utils.selectFrequencyPlan": "Select a frequency plan...", "console.containers.freq-plans-select.utils.addFrequencyPlan": "Add frequency plan", "console.containers.freq-plans-select.utils.frequencyPlanDescription": "Note: most gateways use a single frequency plan. Some 16 and 64 channel gateways however allow setting multiple.", - "console.containers.gateway-connection.gateway-connection.lastSeenAvailableTooltip": "The elapsed time since the network registered the last activity of this gateway. This is determined from received uplinks, or sent status messages of this gateway.", - "console.containers.gateway-connection.gateway-connection.disconnectedTooltip": "The gateway has currently no TCP connection established with the Gateway Server. For (rare) UDP based gateways, this can also mean that the gateway initiated no pull/push data request within the last 30 seconds.", - "console.containers.gateway-connection.gateway-connection.connectedTooltip": "This gateway is connected to the Gateway Server but the network has not registered any activity (sent uplinks or status messages) from it yet.", - "console.containers.gateway-connection.gateway-connection.otherClusterTooltip": "This gateway is connected to an external Gateway Server that is not handling messages for this cluster. You will hence not be able to see any activity from this gateway.", - "console.containers.gateway-connection.gateway-connection.messageCountTooltip": "The amount of received uplinks and sent downlinks of this gateway since the last (re)connect. Note that some gateway types reconnect frequently causing the counter to be reset.", - "console.containers.gateway-location-form.gateway-location-form.updateLocationFromStatus": "Update from status messages", - "console.containers.gateway-location-form.gateway-location-form.updateLocationFromStatusDescription": "Update the location of this gateway based on incoming status messages", - "console.containers.gateway-location-form.gateway-location-form.setGatewayLocation": "Gateway antenna location settings", - "console.containers.gateway-location-form.gateway-location-form.locationSource": "Location source", - "console.containers.gateway-location-form.gateway-location-form.locationPrivacy": "Location privacy", - "console.containers.gateway-location-form.gateway-location-form.placement": "Placement", - "console.containers.gateway-location-form.gateway-location-form.indoor": "Indoor", - "console.containers.gateway-location-form.gateway-location-form.outdoor": "Outdoor", - "console.containers.gateway-location-form.gateway-location-form.locationFromStatusMessage": "Location set automatically from status messages", - "console.containers.gateway-location-form.gateway-location-form.setLocationManually": "Set location manually", - "console.containers.gateway-location-form.gateway-location-form.noLocationSetInfo": "This gateway has no location information set", + "console.containers.gateway-connection.index.lastSeenAvailableTooltip": "The elapsed time since the network registered the last activity of this gateway. This is determined from received uplinks, or sent status messages of this gateway.", + "console.containers.gateway-connection.index.disconnectedTooltip": "The gateway has currently no TCP connection established with the Gateway Server. For (rare) UDP based gateways, this can also mean that the gateway initiated no pull/push data request within the last 30 seconds.", + "console.containers.gateway-connection.index.connectedTooltip": "This gateway is connected to the Gateway Server but the network has not registered any activity (sent uplinks or status messages) from it yet.", + "console.containers.gateway-connection.index.otherClusterTooltip": "This gateway is connected to an external Gateway Server that is not handling messages for this cluster. You will hence not be able to see any activity from this gateway.", + "console.containers.gateway-connection.index.messageCountTooltip": "The amount of received uplinks and sent downlinks of this gateway since the last (re)connect. Note that some gateway types reconnect frequently causing the counter to be reset.", + "console.containers.gateway-location-form.index.updateLocationFromStatus": "Update from status messages", + "console.containers.gateway-location-form.index.updateLocationFromStatusDescription": "Update the location of this gateway based on incoming status messages", + "console.containers.gateway-location-form.index.setGatewayLocation": "Gateway antenna location settings", + "console.containers.gateway-location-form.index.locationSource": "Location source", + "console.containers.gateway-location-form.index.locationPrivacy": "Location privacy", + "console.containers.gateway-location-form.index.placement": "Placement", + "console.containers.gateway-location-form.index.indoor": "Indoor", + "console.containers.gateway-location-form.index.outdoor": "Outdoor", + "console.containers.gateway-location-form.index.locationFromStatusMessage": "Location set automatically from status messages", + "console.containers.gateway-location-form.index.setLocationManually": "Set location manually", + "console.containers.gateway-location-form.index.noLocationSetInfo": "This gateway has no location information set", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.index.requireAuthenticatedConnectionDescription": "Select which information can be seen by other network participants, including {packetBrokerURL}", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.index.shareGatewayInfoDescription": "Choose this option eg. if your gateway is powered by {loraBasicStationURL}", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.validation-schema.validateEntry": "There must be at least one selected frequency plan ID.", @@ -579,8 +580,8 @@ "console.containers.organizations-table.index.restoreFail": "There was an error and the organization could not be restored", "console.containers.organizations-table.index.purgeSuccess": "Organization purged", "console.containers.organizations-table.index.purgeFail": "There was an error and the organization could not be purged", - "console.containers.owners-select.owners-select.title": "Owner", - "console.containers.owners-select.owners-select.warning": "There was an error and the list of organizations could not be displayed", + "console.containers.owners-select.index.title": "Owner", + "console.containers.owners-select.index.warning": "There was an error and the list of organizations could not be displayed", "console.containers.packet-broker-networks-table.index.nonDefaultPolicies": "Networks with non-default policies", "console.containers.packet-broker-networks-table.index.search": "Search by tenant ID or name", "console.containers.packet-broker-networks-table.index.forwarderPolicy": "Their routing policy towards us", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index f13141418e..2f9f10b27d 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -205,16 +205,16 @@ "console.components.device-import-form.index.inputMethodManual": "LoRaWANのバージョンと周波数プランを手動で入力します", "console.components.device-import-form.index.fallbackValues": "フォールバック値", "console.components.device-import-form.index.noFallback": "フォールバック値は設定しない", - "console.components.downlink-form.downlink-form.insertMode": "挿入モード", - "console.components.downlink-form.downlink-form.payloadType": "ペイロード・タイプ", - "console.components.downlink-form.downlink-form.bytes": "バイト", - "console.components.downlink-form.downlink-form.replace": "ダウンリンクキューの交換", - "console.components.downlink-form.downlink-form.push": "ダウンリンクキューへのプッシュ(付け加え)", - "console.components.downlink-form.downlink-form.scheduleDownlink": "スケジュール ダウンリンク", - "console.components.downlink-form.downlink-form.downlinkSuccess": "ダウンリンク予定", - "console.components.downlink-form.downlink-form.bytesPayloadDescription": "ダウンリンクメッセージの希望ペイロードバイト数", - "console.components.downlink-form.downlink-form.jsonPayloadDescription": "ダウンリンクメッセージのデコードされたペイロード", - "console.components.downlink-form.downlink-form.invalidSessionWarning": "ダウンリンクは、有効なセッションを持つエンドデバイスに対してのみスケジュールすることができます。エンドデバイスがネットワークに正しく接続されていることを確認してください", + "console.components.downlink-form.index.insertMode": "挿入モード", + "console.components.downlink-form.index.payloadType": "ペイロード・タイプ", + "console.components.downlink-form.index.bytes": "バイト", + "console.components.downlink-form.index.replace": "ダウンリンクキューの交換", + "console.components.downlink-form.index.push": "ダウンリンクキューへのプッシュ(付け加え)", + "console.components.downlink-form.index.scheduleDownlink": "スケジュール ダウンリンク", + "console.components.downlink-form.index.downlinkSuccess": "ダウンリンク予定", + "console.components.downlink-form.index.bytesPayloadDescription": "ダウンリンクメッセージの希望ペイロードバイト数", + "console.components.downlink-form.index.jsonPayloadDescription": "ダウンリンクメッセージのデコードされたペイロード", + "console.components.downlink-form.index.invalidSessionWarning": "ダウンリンクは、有効なセッションを持つエンドデバイスに対してのみスケジュールすることができます。エンドデバイスがネットワークに正しく接続されていることを確認してください", "console.components.events.messages.MACPayload": "MAC ペイロード", "console.components.events.messages.devAddr": "DevAddr", "console.components.events.messages.fPort": "FPort", @@ -342,6 +342,7 @@ "console.components.pubsub-form.messages.mqttClientIdPlaceholder": "my-client-id", "console.components.pubsub-form.messages.mqttServerUrlPlaceholder": "mqtts://example.com", "console.components.pubsub-form.messages.subscribeQos": "QoSを購読", + "console.components.pubsub-form.messages.providerDescription": "Pub/Subプロバイダーの変更が管理者により無効化", "console.components.pubsub-form.messages.publishQos": "QoSを発行", "console.components.pubsub-form.messages.tlsCa": "ルート認証局証明書", "console.components.pubsub-form.messages.tlsClientCert": "クライアント証明書", @@ -356,9 +357,9 @@ "console.components.routing-policy-form.index.saveDefaultPolicy": "", "console.components.routing-policy-form.index.useSpecificPolicy": "ネットワーク固有のルーティングポリシーを使用", "console.components.routing-policy-form.index.doNotUseAPolicy": "このネットワークには、ルーティングポリシーを使用しないでください", - "console.components.uplink-form.uplink-form.simulateUplink": "アップリンクのシミュレーション", - "console.components.uplink-form.uplink-form.payloadDescription": "アップリンクメッセージの希望するペイロードバイト数", - "console.components.uplink-form.uplink-form.uplinkSuccess": "アップリンク送信済", + "console.components.uplink-form.index.simulateUplink": "アップリンクのシミュレーション", + "console.components.uplink-form.index.payloadDescription": "アップリンクメッセージの希望するペイロードバイト数", + "console.components.uplink-form.index.uplinkSuccess": "アップリンク送信済", "console.components.webhook-form.index.idPlaceholder": "my-new-webhook", "console.components.webhook-form.index.messageInfo": "有効なメッセージタイプごとに、オプションのパスをベースURLに合わせて定義することができます", "console.components.webhook-form.index.deleteWebhook": "Webhookの削除", @@ -488,48 +489,48 @@ "console.containers.device-onboarding-form.warning-tooltip.sessionDescription": "ABP 装置は、セッションと MAC 設定でパーソナライズされます。これらのMAC設定は現在のパラメータとみなされ、ここで入力された設定と正確に一致しなければなりません。ネットワークサーバーは、LoRaWAN MACコマンドでMAC状態を希望する状態に変更するために希望するパラメータを使用します。エンドデバイスを登録した後に、一般設定ページを使用して希望する設定を更新することができます", "console.containers.device-payload-formatters.messages.defaultFormatter": "こちら をクリックすると、このアプリケーションのデフォルトのペイロードフォーマッターを変更できます", "console.containers.device-payload-formatters.messages.mayNotViewLink": "このアプリケーションのリンク情報を表示することは許可されていません。これには、このアプリケーションのデフォルトのペイロードフォーマッタを見ることも含まれます", - "console.containers.device-profile-section.device-card.device-card.productWebsite": "", - "console.containers.device-profile-section.device-card.device-card.dataSheet": "", - "console.containers.device-profile-section.device-card.device-card.classA": "", - "console.containers.device-profile-section.device-card.device-card.classB": "", - "console.containers.device-profile-section.device-card.device-card.classC": "", + "console.containers.device-profile-section.device-card.index.productWebsite": "", + "console.containers.device-profile-section.device-card.index.dataSheet": "", + "console.containers.device-profile-section.device-card.index.classA": "", + "console.containers.device-profile-section.device-card.index.classB": "", + "console.containers.device-profile-section.device-card.index.classC": "", "console.containers.device-profile-section.device-selection.band-select.index.title": "プロフィール(リージョン)", - "console.containers.device-profile-section.device-selection.brand-select.brand-select.title": "", - "console.containers.device-profile-section.device-selection.brand-select.brand-select.noOptionsMessage": "", - "console.containers.device-profile-section.device-selection.fw-version-select.fw-version-select.title": "", - "console.containers.device-profile-section.device-selection.hw-version-select.hw-version-select.title": "", - "console.containers.device-profile-section.device-selection.model-select.model-select.noOptionsMessage": "", + "console.containers.device-profile-section.device-selection.brand-select.index.title": "", + "console.containers.device-profile-section.device-selection.brand-select.index.noOptionsMessage": "", + "console.containers.device-profile-section.device-selection.fw-version-select.index.title": "", + "console.containers.device-profile-section.device-selection.hw-version-select.index.title": "", + "console.containers.device-profile-section.device-selection.model-select.index.noOptionsMessage": "", "console.containers.device-profile-section.hints.other-hint.hintTitle": "お客様のエンドデバイスはすぐに追加されます!", "console.containers.device-profile-section.hints.other-hint.hintMessage": "申し訳ありませんが、あなたのデバイスはまだLoRaWANデバイスリポジトリの一部ではありません。エンドデバイスの製造元が提供する情報(製品のデータシートなど)を使用して、上記のenter end device specifics manuallyオプションを使用することができます。また、デバイスの追加に関するドキュメントも参照してください", "console.containers.device-profile-section.hints.progress-hint.hintMessage": "", "console.containers.device-profile-section.hints.progress-hint.hintNoSupportMessage": "", "console.containers.device-template-format-select.index.title": "ファイルフォーマット", "console.containers.device-template-format-select.index.warning": "エンドデバイスのテンプレートフォーマットが利用できません", - "console.containers.device-title-section.device-title-section.uplinkDownlinkTooltip": "前回のフレームカウンタリセット以降、このエンドデバイスの送信アップリンクと受信ダウンリンクの数です", - "console.containers.device-title-section.device-title-section.lastSeenAvailableTooltip": "ネットワークがこのエンドデバイスの最後のアクティビティを登録してから経過した時間です。これは、送信されたアップリンク、確認されたダウンリンク、または(再)参加要求から判断されます。{lineBreak}最後のアクティビティは{lastActivityInfo}で受信します", - "console.containers.device-title-section.device-title-section.noActivityTooltip": "ネットワークは、このエンドデバイスからのアクティビティをまだ登録していません。これは、エンドデバイスがまだメッセージを送信していないか、EUIや周波数の不一致など、ネットワークで処理できないメッセージしか送信していないことを意味する可能性があります", + "console.containers.device-title-section.index.uplinkDownlinkTooltip": "前回のフレームカウンタリセット以降、このエンドデバイスの送信アップリンクと受信ダウンリンクの数です", + "console.containers.device-title-section.index.lastSeenAvailableTooltip": "ネットワークがこのエンドデバイスの最後のアクティビティを登録してから経過した時間です。これは、送信されたアップリンク、確認されたダウンリンク、または(再)参加要求から判断されます。{lineBreak}最後のアクティビティは{lastActivityInfo}で受信します", + "console.containers.device-title-section.index.noActivityTooltip": "ネットワークは、このエンドデバイスからのアクティビティをまだ登録していません。これは、エンドデバイスがまだメッセージを送信していないか、EUIや周波数の不一致など、ネットワークで処理できないメッセージしか送信していないことを意味する可能性があります", "console.containers.devices-table.index.otherClusterTooltip": "このエンドデバイスは、別のクラスタ(`{host}`)に登録されています。このデバイスにアクセスするには、このエンドデバイスが登録されているクラスタのコンソールを使用します", "console.containers.freq-plans-select.utils.warning": "", "console.containers.freq-plans-select.utils.none": "", "console.containers.freq-plans-select.utils.selectFrequencyPlan": "", "console.containers.freq-plans-select.utils.addFrequencyPlan": "", "console.containers.freq-plans-select.utils.frequencyPlanDescription": "", - "console.containers.gateway-connection.gateway-connection.lastSeenAvailableTooltip": "ネットワークがこのゲートウェイの最後のアクティビティを登録してから経過した時間です。これは、このゲートウェイの受信したアップリンク、または送信したステータスメッセージから決定されます", - "console.containers.gateway-connection.gateway-connection.disconnectedTooltip": "ゲートウェイは現在、ゲートウェイサーバーとの TCP 接続を確立していません。まれに)UDPベースのゲートウェイの場合、これはゲートウェイが過去30秒以内にpull/pushデータ要求を開始しなかったことを意味することもあります", - "console.containers.gateway-connection.gateway-connection.connectedTooltip": "このゲートウェイはゲートウェイサーバーに接続されていますが、ネットワークはまだゲートウェイからのアクティビティ(アップリンクやステータスメッセージの送信)を登録していません", - "console.containers.gateway-connection.gateway-connection.otherClusterTooltip": "このゲートウェイは、このクラスタのメッセージを処理しない外部のゲートウェイサーバーに接続されています。そのため、このゲートウェイからのアクティビティを見ることはできません", - "console.containers.gateway-connection.gateway-connection.messageCountTooltip": "最後の(再)接続以降、このゲートウェイの受信アップリンクと送信ダウンリンクの量です。ゲートウェイの種類によっては、頻繁に再接続するため、カウンタがリセットされることに注意してください", - "console.containers.gateway-location-form.gateway-location-form.updateLocationFromStatus": "", - "console.containers.gateway-location-form.gateway-location-form.updateLocationFromStatusDescription": "", - "console.containers.gateway-location-form.gateway-location-form.setGatewayLocation": "", - "console.containers.gateway-location-form.gateway-location-form.locationSource": "", - "console.containers.gateway-location-form.gateway-location-form.locationPrivacy": "", - "console.containers.gateway-location-form.gateway-location-form.placement": "", - "console.containers.gateway-location-form.gateway-location-form.indoor": "", - "console.containers.gateway-location-form.gateway-location-form.outdoor": "", - "console.containers.gateway-location-form.gateway-location-form.locationFromStatusMessage": "", - "console.containers.gateway-location-form.gateway-location-form.setLocationManually": "", - "console.containers.gateway-location-form.gateway-location-form.noLocationSetInfo": "", + "console.containers.gateway-connection.index.lastSeenAvailableTooltip": "ネットワークがこのゲートウェイの最後のアクティビティを登録してから経過した時間です。これは、このゲートウェイの受信したアップリンク、または送信したステータスメッセージから決定されます", + "console.containers.gateway-connection.index.disconnectedTooltip": "ゲートウェイは現在、ゲートウェイサーバーとの TCP 接続を確立していません。まれに)UDPベースのゲートウェイの場合、これはゲートウェイが過去30秒以内にpull/pushデータ要求を開始しなかったことを意味することもあります", + "console.containers.gateway-connection.index.connectedTooltip": "このゲートウェイはゲートウェイサーバーに接続されていますが、ネットワークはまだゲートウェイからのアクティビティ(アップリンクやステータスメッセージの送信)を登録していません", + "console.containers.gateway-connection.index.otherClusterTooltip": "このゲートウェイは、このクラスタのメッセージを処理しない外部のゲートウェイサーバーに接続されています。そのため、このゲートウェイからのアクティビティを見ることはできません", + "console.containers.gateway-connection.index.messageCountTooltip": "最後の(再)接続以降、このゲートウェイの受信アップリンクと送信ダウンリンクの量です。ゲートウェイの種類によっては、頻繁に再接続するため、カウンタがリセットされることに注意してください", + "console.containers.gateway-location-form.index.updateLocationFromStatus": "", + "console.containers.gateway-location-form.index.updateLocationFromStatusDescription": "", + "console.containers.gateway-location-form.index.setGatewayLocation": "", + "console.containers.gateway-location-form.index.locationSource": "", + "console.containers.gateway-location-form.index.locationPrivacy": "", + "console.containers.gateway-location-form.index.placement": "", + "console.containers.gateway-location-form.index.indoor": "", + "console.containers.gateway-location-form.index.outdoor": "", + "console.containers.gateway-location-form.index.locationFromStatusMessage": "", + "console.containers.gateway-location-form.index.setLocationManually": "", + "console.containers.gateway-location-form.index.noLocationSetInfo": "", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.index.requireAuthenticatedConnectionDescription": "{packetBrokerURL}など、他のネットワーク参加者が見ることができる情報を選択します", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.index.shareGatewayInfoDescription": "ゲートウェイが{loraBasicStationURL}で駆動している場合など、このオプションを選択します", "console.containers.gateway-onboarding-form.gateway-provisioning-form.gateway-registration-form-section.validation-schema.validateEntry": "", @@ -579,8 +580,8 @@ "console.containers.organizations-table.index.restoreFail": "エラーが発生し、組織を復元することができませんでした", "console.containers.organizations-table.index.purgeSuccess": "パージされた組織", "console.containers.organizations-table.index.purgeFail": "エラーが発生したため、組織をパージすることができませんでした", - "console.containers.owners-select.owners-select.title": "", - "console.containers.owners-select.owners-select.warning": "", + "console.containers.owners-select.index.title": "", + "console.containers.owners-select.index.warning": "", "console.containers.packet-broker-networks-table.index.nonDefaultPolicies": "デフォルトでないポリシーを持つネットワーク", "console.containers.packet-broker-networks-table.index.search": "テナントID、テナント名で検索", "console.containers.packet-broker-networks-table.index.forwarderPolicy": "私たちに対する彼らのルーティングポリシー",