From f0d3f0ea2221f2b532f77b4cd28e04a0eff89859 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Mon, 27 Nov 2023 19:26:46 +0100 Subject: [PATCH 01/21] console: Removed and refactored withFeatureRequirement HOC --- DEVELOPMENT.md | 3 +- .../integrations/pub-subs/create.spec.js | 83 ++++++++++++++++++ .../components/pubsub-form/messages.js | 1 + .../containers/application-events/index.js | 78 ++++++++--------- .../containers/gateway-events/index.js | 85 ++++++++++--------- .../components/with-feature-requirement.js | 57 ------------- pkg/webui/console/views/applications/index.js | 24 +++--- pkg/webui/locales/en.json | 1 + pkg/webui/locales/ja.json | 1 + 9 files changed, 186 insertions(+), 147 deletions(-) delete mode 100644 pkg/webui/console/lib/components/with-feature-requirement.js 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..27f3fe3c28 100644 --- a/cypress/e2e/console/integrations/pub-subs/create.spec.js +++ b/cypress/e2e/console/integrations/pub-subs/create.spec.js @@ -323,4 +323,87 @@ 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('MQTT').should('be.checked') + 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('NATS').should('be.checked') + 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/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/containers/application-events/index.js b/pkg/webui/console/containers/application-events/index.js index 8cd3854e74..cf313c5d0f 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 React, { useMemo } from 'react' import { connect } 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' @@ -50,24 +50,28 @@ const ApplicationEvents = props => { filter, } = props - if (widget) { + const content = useMemo(() => { + if (widget) { + return ( + + ) + } + return ( - + ) - } + }, [appId, events, filter, onClear, onFilterChange, onPauseToggle, paused, truncated, widget]) - return ( - - ) + return {content} } ApplicationEvents.propTypes = { @@ -88,25 +92,23 @@ ApplicationEvents.defaultProps = { filter: undefined, } -export default withFeatureRequirement(mayViewApplicationEvents)( - connect( - (state, props) => { - const { appId } = props +export default 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), -) + 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) diff --git a/pkg/webui/console/containers/gateway-events/index.js b/pkg/webui/console/containers/gateway-events/index.js index c9283fb0b6..c47953a816 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 React, { useMemo } from 'react' import { connect } 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' @@ -50,25 +50,34 @@ const GatewayEvents = props => { filter, } = props - if (widget) { + const content = useMemo(() => { + if (widget) { + return ( + + ) + } + return ( - + ) - } + }, [events, filter, gtwId, onClear, onFilterChange, onPauseToggle, paused, truncated, widget]) - return ( - - ) + return {content} } GatewayEvents.propTypes = { @@ -89,25 +98,23 @@ GatewayEvents.defaultProps = { filter: undefined, } -export default withFeatureRequirement(mayViewGatewayEvents)( - connect( - (state, props) => { - const { gtwId } = props +export default 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), -) + 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) 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..93f6f45f3f 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -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", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index f13141418e..028c9859ba 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -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": "", "console.components.pubsub-form.messages.publishQos": "QoSを発行", "console.components.pubsub-form.messages.tlsCa": "ルート認証局証明書", "console.components.pubsub-form.messages.tlsClientCert": "クライアント証明書", From 35919efb63d0b9ad94dd2e549914adb3b1cbf438 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Mon, 27 Nov 2023 19:58:35 +0100 Subject: [PATCH 02/21] console: Removed and refactored withConnectionReactor HOC --- .../containers/gateway-connection/connect.js | 4 +- .../gateway-connection-reactor.js | 67 ------------------- .../gateway-connection/gateway-connection.js | 4 ++ .../useConnectionReactor.js | 34 ++++++++++ 4 files changed, 39 insertions(+), 70 deletions(-) delete mode 100644 pkg/webui/console/containers/gateway-connection/gateway-connection-reactor.js create mode 100644 pkg/webui/console/containers/gateway-connection/useConnectionReactor.js diff --git a/pkg/webui/console/containers/gateway-connection/connect.js b/pkg/webui/console/containers/gateway-connection/connect.js index 1cd5a148ec..ecc2bdf7ad 100644 --- a/pkg/webui/console/containers/gateway-connection/connect.js +++ b/pkg/webui/console/containers/gateway-connection/connect.js @@ -32,8 +32,6 @@ import { } 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) => { @@ -56,4 +54,4 @@ export default GatewayConnection => stopStatistics: () => dispatch(stopGatewayStatistics()), updateGatewayStatistics: () => dispatch(updateGatewayStatistics(ownProps.gtwId)), }), - )(withConnectionReactor(GatewayConnection)) + )(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 index 54463ba764..7b281373fb 100644 --- a/pkg/webui/console/containers/gateway-connection/gateway-connection.js +++ b/pkg/webui/console/containers/gateway-connection/gateway-connection.js @@ -25,6 +25,8 @@ 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' @@ -56,6 +58,8 @@ const GatewayConnection = props => { className, } = props + useConnectionReactor() + useEffect(() => { startStatistics() return () => { 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..72137c117d --- /dev/null +++ b/pkg/webui/console/containers/gateway-connection/useConnectionReactor.js @@ -0,0 +1,34 @@ +import { useEffect, useRef } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' + +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 = () => { + const { gtwId } = useParams() + 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 From fb8b90e31e24bd03d00e92fd6fd1131cb4273665 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 00:31:52 +0100 Subject: [PATCH 03/21] console: Refactor connect in owners select --- .../containers/owners-select/connect.js | 35 ------ .../console/containers/owners-select/index.js | 86 ++++++++++++++- .../containers/owners-select/owners-select.js | 101 ------------------ pkg/webui/locales/en.json | 2 + pkg/webui/locales/ja.json | 2 + 5 files changed, 86 insertions(+), 140 deletions(-) delete mode 100644 pkg/webui/console/containers/owners-select/connect.js delete mode 100644 pkg/webui/console/containers/owners-select/owners-select.js 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..a09c2b8c0f 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 Index = 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 ( + + ) +} + +Index.propTypes = { + autoFocus: PropTypes.bool, + menuPlacement: PropTypes.oneOf(['top', 'bottom', 'auto']), + name: PropTypes.string.isRequired, + onChange: PropTypes.func, + required: PropTypes.bool, +} + +Index.defaultProps = { + autoFocus: false, + onChange: () => null, + menuPlacement: 'auto', + required: false, +} + +export default Index 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/locales/en.json b/pkg/webui/locales/en.json index 93f6f45f3f..b27dc3573e 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -580,6 +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.index.title": "Owner", + "console.containers.owners-select.index.warning": "There was an error and the list of organizations could not be displayed", "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.packet-broker-networks-table.index.nonDefaultPolicies": "Networks with non-default policies", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 028c9859ba..e2cbaddd39 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -580,6 +580,8 @@ "console.containers.organizations-table.index.restoreFail": "エラーが発生し、組織を復元することができませんでした", "console.containers.organizations-table.index.purgeSuccess": "パージされた組織", "console.containers.organizations-table.index.purgeFail": "エラーが発生したため、組織をパージすることができませんでした", + "console.containers.owners-select.index.title": "", + "console.containers.owners-select.index.warning": "", "console.containers.owners-select.owners-select.title": "", "console.containers.owners-select.owners-select.warning": "", "console.containers.packet-broker-networks-table.index.nonDefaultPolicies": "デフォルトでないポリシーを持つネットワーク", From 645b13e8315cee63f9bec3a46e8cdc728cbaa444 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 00:49:26 +0100 Subject: [PATCH 04/21] console: Refactor connect in organization events and device title section --- .../device-title-section/connect.js | 37 ----- .../device-title-section.js | 136 ------------------ .../containers/device-title-section/index.js | 133 ++++++++++++++++- .../containers/organization-events/connect.js | 47 ------ .../containers/organization-events/index.js | 80 ++++++++++- .../organization-events.js | 63 -------- .../console/containers/owners-select/index.js | 8 +- pkg/webui/locales/en.json | 3 + pkg/webui/locales/ja.json | 3 + 9 files changed, 215 insertions(+), 295 deletions(-) delete mode 100644 pkg/webui/console/containers/device-title-section/connect.js delete mode 100644 pkg/webui/console/containers/device-title-section/device-title-section.js delete mode 100644 pkg/webui/console/containers/organization-events/connect.js delete mode 100644 pkg/webui/console/containers/organization-events/organization-events.js 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..fe0f33df36 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' +import { useParams } from 'react-router-dom' -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 } = useParams() + const { 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 = { + children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]), + fetching: PropTypes.bool, +} + +DeviceTitleSection.defaultProps = { + children: null, + fetching: false, +} + +export default DeviceTitleSection 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..835aee2764 100644 --- a/pkg/webui/console/containers/organization-events/index.js +++ b/pkg/webui/console/containers/organization-events/index.js @@ -12,9 +12,81 @@ // 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 { useParams } from 'react-router-dom' +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 } = useParams() + const { 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 = { + 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/index.js b/pkg/webui/console/containers/owners-select/index.js index a09c2b8c0f..b9454b96ec 100644 --- a/pkg/webui/console/containers/owners-select/index.js +++ b/pkg/webui/console/containers/owners-select/index.js @@ -34,7 +34,7 @@ const m = defineMessages({ warning: 'There was an error and the list of organizations could not be displayed', }) -const Index = props => { +const OwnersSelect = props => { const { autoFocus, menuPlacement, name, onChange, required } = props const user = useSelector(selectUser) @@ -80,7 +80,7 @@ const Index = props => { ) } -Index.propTypes = { +OwnersSelect.propTypes = { autoFocus: PropTypes.bool, menuPlacement: PropTypes.oneOf(['top', 'bottom', 'auto']), name: PropTypes.string.isRequired, @@ -88,11 +88,11 @@ Index.propTypes = { required: PropTypes.bool, } -Index.defaultProps = { +OwnersSelect.defaultProps = { autoFocus: false, onChange: () => null, menuPlacement: 'auto', required: false, } -export default Index +export default OwnersSelect diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index b27dc3573e..3e266b754a 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -509,6 +509,9 @@ "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", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index e2cbaddd39..51d051a71b 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -509,6 +509,9 @@ "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": "", + "console.containers.device-title-section.index.noActivityTooltip": "", "console.containers.devices-table.index.otherClusterTooltip": "このエンドデバイスは、別のクラスタ(`{host}`)に登録されています。このデバイスにアクセスするには、このエンドデバイスが登録されているクラスタのコンソールを使用します", "console.containers.freq-plans-select.utils.warning": "", "console.containers.freq-plans-select.utils.none": "", From 49fb55c1d672668e636a5db3be0a1bce4fa99733 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 00:55:22 +0100 Subject: [PATCH 05/21] console: Refactor connect in application events --- .../containers/application-events/index.js | 78 +++++++++---------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/pkg/webui/console/containers/application-events/index.js b/pkg/webui/console/containers/application-events/index.js index cf313c5d0f..22daa52773 100644 --- a/pkg/webui/console/containers/application-events/index.js +++ b/pkg/webui/console/containers/application-events/index.js @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useMemo } from 'react' -import { connect } from 'react-redux' +import React, { useCallback, useMemo } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' import Events from '@console/components/events' @@ -38,17 +39,37 @@ import { } from '@console/store/selectors/applications' const ApplicationEvents = props => { - const { - appId, - events, - widget, - paused, - onClear, - onPauseToggle, - truncated, - onFilterChange, - filter, - } = props + const { appId } = useParams() + const { 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) { @@ -75,40 +96,11 @@ const ApplicationEvents = props => { } 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 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 From f2d6bee26e42f43f5e658af6013df47a2ac59580 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 01:11:12 +0100 Subject: [PATCH 06/21] console: Refactor connect in uplink form --- .../console/components/uplink-form/connect.js | 40 ----- .../console/components/uplink-form/index.js | 133 ++++++++++++++++- .../components/uplink-form/uplink-form.js | 139 ------------------ pkg/webui/locales/en.json | 14 ++ pkg/webui/locales/ja.json | 14 ++ 5 files changed, 157 insertions(+), 183 deletions(-) delete mode 100644 pkg/webui/console/components/uplink-form/connect.js delete mode 100644 pkg/webui/console/components/uplink-form/uplink-form.js 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/locales/en.json b/pkg/webui/locales/en.json index 3e266b754a..a4e74eb28b 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -357,6 +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.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.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", @@ -534,6 +537,17 @@ "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-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.", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 51d051a71b..da72f1e089 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -357,6 +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.index.simulateUplink": "", + "console.components.uplink-form.index.payloadDescription": "", + "console.components.uplink-form.index.uplinkSuccess": "", "console.components.uplink-form.uplink-form.simulateUplink": "アップリンクのシミュレーション", "console.components.uplink-form.uplink-form.payloadDescription": "アップリンクメッセージの希望するペイロードバイト数", "console.components.uplink-form.uplink-form.uplinkSuccess": "アップリンク送信済", @@ -534,6 +537,17 @@ "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-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": "", From 22a31f7320d3a0423d889a99c0dc23745b63892e Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 01:18:15 +0100 Subject: [PATCH 07/21] console: Refactor connect in fw version select --- .../fw-version-select/connect.js | 37 ---------- .../fw-version-select/fw-version-select.js | 71 ------------------- .../fw-version-select/index.js | 68 ++++++++++++++++-- pkg/webui/locales/en.json | 1 + pkg/webui/locales/ja.json | 1 + 5 files changed, 66 insertions(+), 112 deletions(-) delete mode 100644 pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/connect.js delete mode 100644 pkg/webui/console/containers/device-profile-section/device-selection/fw-version-select/fw-version-select.js 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..b66f170998 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,69 @@ // 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' +import { useParams } from 'react-router-dom' -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 { brandId, modelId, hwVersion } = useParams() + const { name, onChange, ...rest } = props + const { setFieldValue } = 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) { + setFieldValue('version_ids.firmware_version', options[0].value) + } + }, [setFieldValue, options]) + + return ( + + ) +} + +FirmwareVersionSelect.propTypes = { + name: PropTypes.string.isRequired, + onChange: PropTypes.func, +} + +FirmwareVersionSelect.defaultProps = { + onChange: () => null, +} + +export default FirmwareVersionSelect diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index a4e74eb28b..c7a3347756 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -501,6 +501,7 @@ "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.fw-version-select.index.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.hints.other-hint.hintTitle": "Your end device will be added soon!", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index da72f1e089..e20aa4ace8 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -501,6 +501,7 @@ "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.fw-version-select.index.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.hints.other-hint.hintTitle": "お客様のエンドデバイスはすぐに追加されます!", From baae71ba5e0c5521247d52e4460aa64a6bbb7b58 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 10:31:37 +0100 Subject: [PATCH 08/21] console: Refactor connect in hw version select --- .../fw-version-select/index.js | 13 +-- .../hw-version-select/connect.js | 27 ------- .../hw-version-select/hw-version-select.js | 80 ------------------- .../hw-version-select/index.js | 70 ++++++++++++++-- pkg/webui/locales/en.json | 22 +---- pkg/webui/locales/ja.json | 34 ++------ 6 files changed, 80 insertions(+), 166 deletions(-) delete mode 100644 pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/connect.js delete mode 100644 pkg/webui/console/containers/device-profile-section/device-selection/hw-version-select/hw-version-select.js 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 b66f170998..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 @@ -15,7 +15,6 @@ import React from 'react' import { defineMessages } from 'react-intl' import { useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' import Field from '@ttn-lw/components/form/field' import Select from '@ttn-lw/components/select' @@ -41,9 +40,8 @@ const formatOptions = (versions = []) => .concat([{ value: SELECT_OTHER_OPTION, label: sharedMessages.otherOption }]) const FirmwareVersionSelect = props => { - const { brandId, modelId, hwVersion } = useParams() - const { name, onChange, ...rest } = props - const { setFieldValue } = useFormContext() + const { name, onChange, brandId, modelId, hwVersion, ...rest } = props + const { setFieldValue, values } = useFormContext() const versions = useSelector(state => selectDeviceModelFirmwareVersions(state, brandId, modelId).filter( @@ -58,10 +56,10 @@ const FirmwareVersionSelect = props => { const options = React.useMemo(() => formatOptions(versions), [versions]) React.useEffect(() => { - if (options.length > 0 && options.length <= 2) { + if (options.length > 0 && options.length <= 2 && !values.version_ids.firmware_version.length) { setFieldValue('version_ids.firmware_version', options[0].value) } - }, [setFieldValue, options]) + }, [setFieldValue, options, values.version_ids.firmware_version.length]) return ( @@ -69,6 +67,9 @@ const FirmwareVersionSelect = props => { } FirmwareVersionSelect.propTypes = { + brandId: PropTypes.string.isRequired, + hwVersion: PropTypes.string.isRequired, + modelId: PropTypes.string.isRequired, name: PropTypes.string.isRequired, onChange: PropTypes.func, } 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/locales/en.json b/pkg/webui/locales/en.json index c7a3347756..6cd524fa2c 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -360,9 +360,6 @@ "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.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.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", @@ -500,9 +497,8 @@ "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.fw-version-select.index.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.hw-version-select.index.title": "Hardware Ver.", "console.containers.device-profile-section.device-selection.model-select.model-select.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.", @@ -510,9 +506,6 @@ "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.", @@ -538,17 +531,6 @@ "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-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.", @@ -600,8 +582,6 @@ "console.containers.organizations-table.index.purgeFail": "There was an error and the organization could not be purged", "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.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.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 e20aa4ace8..360ca36121 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -357,12 +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.index.simulateUplink": "", - "console.components.uplink-form.index.payloadDescription": "", - "console.components.uplink-form.index.uplinkSuccess": "", - "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の削除", @@ -500,9 +497,8 @@ "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.fw-version-select.index.title": "", - "console.containers.device-profile-section.device-selection.hw-version-select.hw-version-select.title": "", + "console.containers.device-profile-section.device-selection.hw-version-select.index.title": "", "console.containers.device-profile-section.device-selection.model-select.model-select.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オプションを使用することができます。また、デバイスの追加に関するドキュメントも参照してください", @@ -510,11 +506,8 @@ "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": "", + "console.containers.device-title-section.index.uplinkDownlinkTooltip": "前回のフレームカウンタリセット以降、このエンドデバイスの送信アップリンクと受信ダウンリンクの数です", + "console.containers.device-title-section.index.lastSeenAvailableTooltip": "ネットワークがこのゲートウェイの最後のアクティビティを登録してから経過した時間です。これは、このゲートウェイの受信したアップリンク、または送信したステータスメッセージから決定されます", "console.containers.device-title-section.index.noActivityTooltip": "", "console.containers.devices-table.index.otherClusterTooltip": "このエンドデバイスは、別のクラスタ(`{host}`)に登録されています。このデバイスにアクセスするには、このエンドデバイスが登録されているクラスタのコンソールを使用します", "console.containers.freq-plans-select.utils.warning": "", @@ -522,7 +515,7 @@ "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.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": "このゲートウェイは、このクラスタのメッセージを処理しない外部のゲートウェイサーバーに接続されています。そのため、このゲートウェイからのアクティビティを見ることはできません", @@ -538,17 +531,6 @@ "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-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": "", @@ -600,8 +582,6 @@ "console.containers.organizations-table.index.purgeFail": "エラーが発生したため、組織をパージすることができませんでした", "console.containers.owners-select.index.title": "", "console.containers.owners-select.index.warning": "", - "console.containers.owners-select.owners-select.title": "", - "console.containers.owners-select.owners-select.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": "私たちに対する彼らのルーティングポリシー", From 10557290967515c9ef8b63e70f59740c1fcf2a1b Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 10:37:06 +0100 Subject: [PATCH 09/21] console: Change useParams with props --- pkg/webui/console/containers/application-events/index.js | 5 ++--- pkg/webui/console/containers/device-title-section/index.js | 6 +++--- pkg/webui/console/containers/organization-events/index.js | 5 ++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pkg/webui/console/containers/application-events/index.js b/pkg/webui/console/containers/application-events/index.js index 22daa52773..f39f2d0349 100644 --- a/pkg/webui/console/containers/application-events/index.js +++ b/pkg/webui/console/containers/application-events/index.js @@ -14,7 +14,6 @@ import React, { useCallback, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' import Events from '@console/components/events' @@ -39,8 +38,7 @@ import { } from '@console/store/selectors/applications' const ApplicationEvents = props => { - const { appId } = useParams() - const { widget } = props + const { appId, widget } = props const events = useSelector(state => selectApplicationEvents(state, appId)) const paused = useSelector(state => selectApplicationEventsPaused(state, appId)) @@ -96,6 +94,7 @@ const ApplicationEvents = props => { } ApplicationEvents.propTypes = { + appId: PropTypes.string.isRequired, widget: PropTypes.bool, } diff --git a/pkg/webui/console/containers/device-title-section/index.js b/pkg/webui/console/containers/device-title-section/index.js index fe0f33df36..7588a1c864 100644 --- a/pkg/webui/console/containers/device-title-section/index.js +++ b/pkg/webui/console/containers/device-title-section/index.js @@ -15,7 +15,6 @@ import React from 'react' import { defineMessages } from 'react-intl' import { useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' import deviceIcon from '@assets/misc/end-device.svg' @@ -54,8 +53,7 @@ const m = defineMessages({ const { Content } = EntityTitleSection const DeviceTitleSection = props => { - const { appId, devId } = useParams() - const { fetching, children } = props + const { appId, devId, fetching, children } = props const device = useSelector(state => selectDeviceByIds(state, appId, devId)) const uplinkFrameCount = useSelector(state => selectDeviceDerivedUplinkFrameCount(state, appId, devId), @@ -133,7 +131,9 @@ const DeviceTitleSection = props => { } DeviceTitleSection.propTypes = { + appId: PropTypes.string.isRequired, children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]), + devId: PropTypes.string.isRequired, fetching: PropTypes.bool, } diff --git a/pkg/webui/console/containers/organization-events/index.js b/pkg/webui/console/containers/organization-events/index.js index 835aee2764..a9f830c22e 100644 --- a/pkg/webui/console/containers/organization-events/index.js +++ b/pkg/webui/console/containers/organization-events/index.js @@ -13,7 +13,6 @@ // limitations under the License. import React, { useCallback } from 'react' -import { useParams } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' import Events from '@console/components/events' @@ -33,8 +32,7 @@ import { } from '@console/store/selectors/organizations' const OrganizationEvents = props => { - const { orgId } = useParams() - const { widget } = props + const { orgId, widget } = props const events = useSelector(state => selectOrganizationEvents(state, orgId)) const paused = useSelector(state => selectOrganizationEventsPaused(state, orgId)) @@ -82,6 +80,7 @@ const OrganizationEvents = props => { } OrganizationEvents.propTypes = { + orgId: PropTypes.string.isRequired, widget: PropTypes.bool, } From 6659f18e74e4f560e682a4791f3ea084593f2d03 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 10:42:09 +0100 Subject: [PATCH 10/21] console: Refactor connect in device card --- .../device-card/connect.js | 27 ---- .../device-card/device-card.js | 130 ------------------ .../device-card/index.js | 111 ++++++++++++++- 3 files changed, 106 insertions(+), 162 deletions(-) delete mode 100644 pkg/webui/console/containers/device-profile-section/device-card/connect.js delete mode 100644 pkg/webui/console/containers/device-profile-section/device-card/device-card.js 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 From 171219c6fbd4dbabdc4130deb6fef641a62068de Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 10:50:19 +0100 Subject: [PATCH 11/21] console: Refactor connect in downlink form --- .../components/downlink-form/connect.js | 40 ---- .../components/downlink-form/downlink-form.js | 210 ------------------ .../console/components/downlink-form/index.js | 198 ++++++++++++++++- pkg/webui/locales/en.json | 15 ++ pkg/webui/locales/ja.json | 15 ++ 5 files changed, 224 insertions(+), 254 deletions(-) delete mode 100644 pkg/webui/console/components/downlink-form/connect.js delete mode 100644 pkg/webui/console/components/downlink-form/downlink-form.js 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/locales/en.json b/pkg/webui/locales/en.json index 6cd524fa2c..749b2985f2 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -215,6 +215,16 @@ "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", @@ -494,6 +504,11 @@ "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", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 360ca36121..e67d8034b1 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -215,6 +215,16 @@ "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", @@ -494,6 +504,11 @@ "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": "", From 9ddc38ac80f0e5be93185320bc5dc1aa37c56bc7 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 10:56:39 +0100 Subject: [PATCH 12/21] console: Refactor connect in model select --- .../device-selection/model-select/connect.js | 39 ------- .../device-selection/model-select/index.js | 105 +++++++++++++++++- .../model-select/model-select.js | 101 ----------------- pkg/webui/locales/en.json | 1 + pkg/webui/locales/ja.json | 1 + 5 files changed, 103 insertions(+), 144 deletions(-) delete mode 100644 pkg/webui/console/containers/device-profile-section/device-selection/model-select/connect.js delete mode 100644 pkg/webui/console/containers/device-profile-section/device-selection/model-select/model-select.js 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/locales/en.json b/pkg/webui/locales/en.json index 749b2985f2..197d312c8b 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -514,6 +514,7 @@ "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.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.device-selection.model-select.model-select.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.", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index e67d8034b1..f420af6ad7 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -514,6 +514,7 @@ "console.containers.device-profile-section.device-selection.brand-select.brand-select.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.device-selection.model-select.model-select.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オプションを使用することができます。また、デバイスの追加に関するドキュメントも参照してください", From 6c1a0519e7fa7bc207c673c3d314d0f102591ece Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 11:00:20 +0100 Subject: [PATCH 13/21] console: Refactor connect in brand select --- .../brand-select/brand-select.js | 87 ------------------- .../device-selection/brand-select/connect.js | 33 ------- .../device-selection/brand-select/index.js | 74 +++++++++++++++- pkg/webui/locales/en.json | 2 + pkg/webui/locales/ja.json | 2 + 5 files changed, 74 insertions(+), 124 deletions(-) delete mode 100644 pkg/webui/console/containers/device-profile-section/device-selection/brand-select/brand-select.js delete mode 100644 pkg/webui/console/containers/device-profile-section/device-selection/brand-select/connect.js 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/locales/en.json b/pkg/webui/locales/en.json index 197d312c8b..b765e57bcd 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -512,6 +512,8 @@ "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.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", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index f420af6ad7..99025e9654 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -512,6 +512,8 @@ "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.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": "", From ba481b6a1f7b51368d8547d0a07087a52604d05c Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 11:04:43 +0100 Subject: [PATCH 14/21] console: Refactor connect in gateway connection form --- .../gateway-location-form/connect.js | 32 --- .../gateway-location-form.js | 239 ------------------ .../containers/gateway-location-form/index.js | 231 ++++++++++++++++- pkg/webui/locales/en.json | 11 + pkg/webui/locales/ja.json | 11 + 5 files changed, 249 insertions(+), 275 deletions(-) delete mode 100644 pkg/webui/console/containers/gateway-location-form/connect.js delete mode 100644 pkg/webui/console/containers/gateway-location-form/gateway-location-form.js 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/locales/en.json b/pkg/webui/locales/en.json index b765e57bcd..1ad80232f5 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -549,6 +549,17 @@ "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-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.", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 99025e9654..0b0971a388 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -549,6 +549,17 @@ "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-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": "", From c5632cebdb1a8ab15913f8a702c74251be579176 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 11:10:21 +0100 Subject: [PATCH 15/21] console: Refactor connect in full view error --- pkg/webui/account.js | 2 +- pkg/webui/console.js | 2 +- .../lib/components/full-view-error/connect.js | 22 -- .../lib/components/full-view-error/error.js | 340 ------------------ .../lib/components/full-view-error/index.js | 332 ++++++++++++++++- 5 files changed, 330 insertions(+), 368 deletions(-) delete mode 100644 pkg/webui/lib/components/full-view-error/connect.js delete mode 100644 pkg/webui/lib/components/full-view-error/error.js diff --git a/pkg/webui/account.js b/pkg/webui/account.js index 83bbfd4ec9..8196eca9b4 100644 --- a/pkg/webui/account.js +++ b/pkg/webui/account.js @@ -25,7 +25,7 @@ import Header from '@ttn-lw/components/header' import WithLocale from '@ttn-lw/lib/components/with-locale' import { ErrorView } from '@ttn-lw/lib/components/error-view' -import { FullViewError } from '@ttn-lw/lib/components/full-view-error' +import FullViewError from '@ttn-lw/lib/components/full-view-error' import Init from '@ttn-lw/lib/components/init' import Logo from '@account/containers/logo' diff --git a/pkg/webui/console.js b/pkg/webui/console.js index fdba93024f..ffb1423631 100644 --- a/pkg/webui/console.js +++ b/pkg/webui/console.js @@ -24,7 +24,7 @@ import { BreadcrumbsProvider } from '@ttn-lw/components/breadcrumbs/context' import Header from '@ttn-lw/components/header' import { ErrorView } from '@ttn-lw/lib/components/error-view' -import { FullViewError } from '@ttn-lw/lib/components/full-view-error' +import FullViewError from '@ttn-lw/lib/components/full-view-error' import Init from '@ttn-lw/lib/components/init' import WithLocale from '@ttn-lw/lib/components/with-locale' diff --git a/pkg/webui/lib/components/full-view-error/connect.js b/pkg/webui/lib/components/full-view-error/connect.js deleted file mode 100644 index 618e10dc4d..0000000000 --- a/pkg/webui/lib/components/full-view-error/connect.js +++ /dev/null @@ -1,22 +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 { selectOnlineStatus } from '@ttn-lw/lib/store/selectors/status' - -export default ErrorView => - connect(state => ({ - onlineStatus: selectOnlineStatus(state), - }))(ErrorView) diff --git a/pkg/webui/lib/components/full-view-error/error.js b/pkg/webui/lib/components/full-view-error/error.js deleted file mode 100644 index 96e0dca16b..0000000000 --- a/pkg/webui/lib/components/full-view-error/error.js +++ /dev/null @@ -1,340 +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, useEffect, useRef, useState } from 'react' -import { Container, Row, Col } from 'react-grid-system' -import clipboard from 'clipboard' -import { Helmet } from 'react-helmet' -import classnames from 'classnames' - -import Footer from '@ttn-lw/components/footer' -import buttonStyle from '@ttn-lw/components/button/button.styl' -import Icon from '@ttn-lw/components/icon' - -import Message from '@ttn-lw/lib/components/message' -import IntlHelmet from '@ttn-lw/lib/components/intl-helmet' - -import errorMessages from '@ttn-lw/lib/errors/error-messages' -import sharedMessages from '@ttn-lw/lib/shared-messages' -import * as cache from '@ttn-lw/lib/cache' -import { - isUnknown as isUnknownError, - isNotFoundError, - isFrontend as isFrontendError, - isBackend as isBackendError, - getCorrelationId, - getBackendErrorId, - isOAuthClientRefusedError, - isOAuthInvalidStateError, -} from '@ttn-lw/lib/errors/utils' -import statusCodeMessages from '@ttn-lw/lib/errors/status-code-messages' -import PropTypes from '@ttn-lw/lib/prop-types' -import { - selectApplicationRootPath, - selectSupportLinkConfig, - selectApplicationSiteName, - selectApplicationSiteTitle, - selectDocumentationUrlConfig, -} from '@ttn-lw/lib/selectors/env' - -import style from './error.styl' - -const appRoot = selectApplicationRootPath() -const siteName = selectApplicationSiteName() -const siteTitle = selectApplicationSiteTitle() -const supportLink = selectSupportLinkConfig() -const documentationLink = selectDocumentationUrlConfig() -const hasSupportLink = Boolean(supportLink) -const lastRefreshAttemptCacheKey = 'last-refresh-attempt' - -// Mind any rendering that is dependant on context, since the errors -// can be rendered before such context is injected. Use the `safe` -// prop to conditionally render any context-dependant nodes. -const FullViewError = ({ error, header, onlineStatus, safe, action, unexpected }) => ( -
- {Boolean(header) && header} -
- -
-
-
-) - -const FullViewErrorInner = ({ error, safe, action, unexpected }) => { - const isUnknown = isUnknownError(error) - const isNotFound = isNotFoundError(error) - const isFrontend = isFrontendError(error) - const isBackend = isBackendError(error) - const hasAction = Boolean(action) - const isErrorObject = error instanceof Error - const isOAuthCallback = /oauth.*\/callback$/.test(window.location.pathname) - - const errorId = getBackendErrorId(error) || 'n/a' - const correlationId = getCorrelationId(error) || 'n/a' - - const [copied, setCopied] = useState(false) - - const lastRefreshAttempt = cache.get(lastRefreshAttemptCacheKey) - let doBlankRender = false - - let errorMessage - let errorTitle - if (isNotFound) { - errorTitle = statusCodeMessages['404'] - errorMessage = errorMessages.genericNotFound - } else if (isOAuthCallback) { - errorTitle = sharedMessages.loginFailed - errorMessage = errorMessages.loginFailedDescription - if (isOAuthClientRefusedError(error) || error.error === 'access_denied') { - errorMessage = errorMessages.loginFailedAbortDescription - } else if (isOAuthInvalidStateError(error)) { - // Usually in case of state errors, the state has expired or otherwise - // invalidated since initiating the OAuth request. This can usually be - // resolved by re-initiating the OAuth flow. We need to keep track of - // refresh attempts though to avoid infinite refresh loops. - if (!lastRefreshAttempt || Date.now() - lastRefreshAttempt > 6 * 1000) { - cache.set(lastRefreshAttemptCacheKey, Date.now()) - doBlankRender = true - window.location = appRoot - } - } - } else if (isFrontend) { - errorMessage = error.errorMessage - if (Boolean(error.errorTitle)) { - errorTitle = error.errorTitle - } - } else if (!isUnknown) { - errorTitle = errorMessages.error - errorMessage = errorMessages.errorOccurred - } else { - errorTitle = errorMessages.error - errorMessage = errorMessages.genericError - } - - const copiedTimer = useRef(undefined) - const handleCopyClick = useCallback(() => { - if (!copied) { - setCopied(true) - copiedTimer.current = setTimeout(() => setCopied(false), 3000) - } - }, [setCopied, copied]) - - const copyButton = useRef(null) - useEffect(() => { - if (copyButton.current) { - new clipboard(copyButton.current) - } - return () => { - // Clear timer on unmount. - if (copiedTimer.current) { - clearTimeout(copiedTimer.current) - } - } - }, []) - - const errorDetails = JSON.stringify(error, undefined, 2) - const hasErrorDetails = - (!isNotFound && Boolean(error) && errorDetails.length > 2) || (isFrontend && error.errorCode) - const buttonClasses = classnames(buttonStyle.button, style.actionButton) - - // Do not render anything when performing a redirect. - if (doBlankRender) { - return null - } - - return ( -
- - - - {safe ? ( - - Error - - ) : ( - - )} -

- - -

-
- - {!isNotFound && unexpected && ( - <> - {' '} - -
- - - )} -
-
- {isNotFound && ( - - - - - )} - {isOAuthCallback && ( - - - - - )} - {hasAction && ( - - )} - {hasSupportLink && !isNotFound && ( - <> - - - - - {hasErrorDetails && ( - - )} - - )} -
- {hasErrorDetails && ( - <> - {isErrorObject && ( - <> -
-
- - Error Type: {error.name} - -
- - )} - {isFrontend && ( - <> -
-
- - Frontend Error ID: {error.errorCode} - -
- - )} - {isBackend && ( - <> -
-
- - Error ID: {errorId} - - - Correlation ID: {correlationId} - -
- - )} -
-
- - - -
{errorDetails}
- -
- - )} - -
-
-
- ) -} - -FullViewErrorInner.propTypes = { - action: PropTypes.shape({ - action: PropTypes.func.isRequired, - icon: PropTypes.string.isRequired, - message: PropTypes.message.isRequired, - }), - error: PropTypes.error.isRequired, - safe: PropTypes.bool, - unexpected: PropTypes.bool, -} - -FullViewErrorInner.defaultProps = { - action: undefined, - safe: false, - unexpected: true, -} - -FullViewError.propTypes = { - action: PropTypes.shape({ - action: PropTypes.func.isRequired, - icon: PropTypes.string.isRequired, - message: PropTypes.message.isRequired, - }), - error: PropTypes.error.isRequired, - header: PropTypes.node, - onlineStatus: PropTypes.onlineStatus, - safe: PropTypes.bool, - unexpected: PropTypes.bool, -} - -FullViewError.defaultProps = { - action: undefined, - header: undefined, - onlineStatus: undefined, - safe: false, - unexpected: true, -} - -export { FullViewError, FullViewErrorInner } diff --git a/pkg/webui/lib/components/full-view-error/index.js b/pkg/webui/lib/components/full-view-error/index.js index e28a0ab4df..4eb96545b9 100644 --- a/pkg/webui/lib/components/full-view-error/index.js +++ b/pkg/webui/lib/components/full-view-error/index.js @@ -12,9 +12,333 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { FullViewError, FullViewErrorInner } from './error' -import connect from './connect' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { Container, Row, Col } from 'react-grid-system' +import clipboard from 'clipboard' +import { Helmet } from 'react-helmet' +import classnames from 'classnames' +import { useSelector } from 'react-redux' -const ConnectedFullErrorView = connect(FullViewError) +import Footer from '@ttn-lw/components/footer' +import buttonStyle from '@ttn-lw/components/button/button.styl' +import Icon from '@ttn-lw/components/icon' -export { ConnectedFullErrorView as default, FullViewError, FullViewErrorInner } +import Message from '@ttn-lw/lib/components/message' +import IntlHelmet from '@ttn-lw/lib/components/intl-helmet' + +import errorMessages from '@ttn-lw/lib/errors/error-messages' +import sharedMessages from '@ttn-lw/lib/shared-messages' +import * as cache from '@ttn-lw/lib/cache' +import { + isUnknown as isUnknownError, + isNotFoundError, + isFrontend as isFrontendError, + isBackend as isBackendError, + getCorrelationId, + getBackendErrorId, + isOAuthClientRefusedError, + isOAuthInvalidStateError, +} from '@ttn-lw/lib/errors/utils' +import statusCodeMessages from '@ttn-lw/lib/errors/status-code-messages' +import PropTypes from '@ttn-lw/lib/prop-types' +import { + selectApplicationRootPath, + selectSupportLinkConfig, + selectApplicationSiteName, + selectApplicationSiteTitle, + selectDocumentationUrlConfig, +} from '@ttn-lw/lib/selectors/env' +import { selectOnlineStatus } from '@ttn-lw/lib/store/selectors/status' + +import style from './error.styl' + +const appRoot = selectApplicationRootPath() +const siteName = selectApplicationSiteName() +const siteTitle = selectApplicationSiteTitle() +const supportLink = selectSupportLinkConfig() +const documentationLink = selectDocumentationUrlConfig() +const hasSupportLink = Boolean(supportLink) +const lastRefreshAttemptCacheKey = 'last-refresh-attempt' + +// Mind any rendering that is dependant on context, since the errors +// can be rendered before such context is injected. Use the `safe` +// prop to conditionally render any context-dependant nodes. +const FullViewError = ({ error, header, safe, action, unexpected }) => { + const onlineStatus = useSelector(selectOnlineStatus) + + return ( +
+ {Boolean(header) && header} +
+ +
+
+
+ ) +} + +const FullViewErrorInner = ({ error, safe, action, unexpected }) => { + const isUnknown = isUnknownError(error) + const isNotFound = isNotFoundError(error) + const isFrontend = isFrontendError(error) + const isBackend = isBackendError(error) + const hasAction = Boolean(action) + const isErrorObject = error instanceof Error + const isOAuthCallback = /oauth.*\/callback$/.test(window.location.pathname) + + const errorId = getBackendErrorId(error) || 'n/a' + const correlationId = getCorrelationId(error) || 'n/a' + + const [copied, setCopied] = useState(false) + + const lastRefreshAttempt = cache.get(lastRefreshAttemptCacheKey) + let doBlankRender = false + + let errorMessage + let errorTitle + if (isNotFound) { + errorTitle = statusCodeMessages['404'] + errorMessage = errorMessages.genericNotFound + } else if (isOAuthCallback) { + errorTitle = sharedMessages.loginFailed + errorMessage = errorMessages.loginFailedDescription + if (isOAuthClientRefusedError(error) || error.error === 'access_denied') { + errorMessage = errorMessages.loginFailedAbortDescription + } else if (isOAuthInvalidStateError(error)) { + // Usually in case of state errors, the state has expired or otherwise + // invalidated since initiating the OAuth request. This can usually be + // resolved by re-initiating the OAuth flow. We need to keep track of + // refresh attempts though to avoid infinite refresh loops. + if (!lastRefreshAttempt || Date.now() - lastRefreshAttempt > 6 * 1000) { + cache.set(lastRefreshAttemptCacheKey, Date.now()) + doBlankRender = true + window.location = appRoot + } + } + } else if (isFrontend) { + errorMessage = error.errorMessage + if (Boolean(error.errorTitle)) { + errorTitle = error.errorTitle + } + } else if (!isUnknown) { + errorTitle = errorMessages.error + errorMessage = errorMessages.errorOccurred + } else { + errorTitle = errorMessages.error + errorMessage = errorMessages.genericError + } + + const copiedTimer = useRef(undefined) + const handleCopyClick = useCallback(() => { + if (!copied) { + setCopied(true) + copiedTimer.current = setTimeout(() => setCopied(false), 3000) + } + }, [setCopied, copied]) + + const copyButton = useRef(null) + useEffect(() => { + if (copyButton.current) { + new clipboard(copyButton.current) + } + return () => { + // Clear timer on unmount. + if (copiedTimer.current) { + clearTimeout(copiedTimer.current) + } + } + }, []) + + const errorDetails = JSON.stringify(error, undefined, 2) + const hasErrorDetails = + (!isNotFound && Boolean(error) && errorDetails.length > 2) || (isFrontend && error.errorCode) + const buttonClasses = classnames(buttonStyle.button, style.actionButton) + + // Do not render anything when performing a redirect. + if (doBlankRender) { + return null + } + + return ( +
+ + + + {safe ? ( + + Error + + ) : ( + + )} +

+ + +

+
+ + {!isNotFound && unexpected && ( + <> + {' '} + +
+ + + )} +
+
+ {isNotFound && ( + + + + + )} + {isOAuthCallback && ( + + + + + )} + {hasAction && ( + + )} + {hasSupportLink && !isNotFound && ( + <> + + + + + {hasErrorDetails && ( + + )} + + )} +
+ {hasErrorDetails && ( + <> + {isErrorObject && ( + <> +
+
+ + Error Type: {error.name} + +
+ + )} + {isFrontend && ( + <> +
+
+ + Frontend Error ID: {error.errorCode} + +
+ + )} + {isBackend && ( + <> +
+
+ + Error ID: {errorId} + + + Correlation ID: {correlationId} + +
+ + )} +
+
+ + + +
{errorDetails}
+ +
+ + )} + +
+
+
+ ) +} + +FullViewErrorInner.propTypes = { + action: PropTypes.shape({ + action: PropTypes.func.isRequired, + icon: PropTypes.string.isRequired, + message: PropTypes.message.isRequired, + }), + error: PropTypes.error.isRequired, + safe: PropTypes.bool, + unexpected: PropTypes.bool, +} + +FullViewErrorInner.defaultProps = { + action: undefined, + safe: false, + unexpected: true, +} + +FullViewError.propTypes = { + action: PropTypes.shape({ + action: PropTypes.func.isRequired, + icon: PropTypes.string.isRequired, + message: PropTypes.message.isRequired, + }), + error: PropTypes.error.isRequired, + header: PropTypes.node, + safe: PropTypes.bool, + unexpected: PropTypes.bool, +} + +FullViewError.defaultProps = { + action: undefined, + header: undefined, + safe: false, + unexpected: true, +} + +export { FullViewError as default, FullViewErrorInner } From 2df18cc3869c7aad0fedb54d4a488d7952b6f7dd Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 11:39:20 +0100 Subject: [PATCH 16/21] console: Refactor connect in gateway connection reactor --- .../containers/gateway-connection/connect.js | 57 ----- .../gateway-connection/gateway-connection.js | 218 ------------------ .../containers/gateway-connection/index.js | 210 ++++++++++++++++- .../useConnectionReactor.js | 4 +- pkg/webui/locales/en.json | 5 + pkg/webui/locales/ja.json | 5 + 6 files changed, 216 insertions(+), 283 deletions(-) delete mode 100644 pkg/webui/console/containers/gateway-connection/connect.js delete mode 100644 pkg/webui/console/containers/gateway-connection/gateway-connection.js 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 ecc2bdf7ad..0000000000 --- a/pkg/webui/console/containers/gateway-connection/connect.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 { 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' - -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)), - }), - )(GatewayConnection) 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 7b281373fb..0000000000 --- a/pkg/webui/console/containers/gateway-connection/gateway-connection.js +++ /dev/null @@ -1,218 +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 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 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 - - useConnectionReactor() - - 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 index 72137c117d..0ae6826d93 100644 --- a/pkg/webui/console/containers/gateway-connection/useConnectionReactor.js +++ b/pkg/webui/console/containers/gateway-connection/useConnectionReactor.js @@ -1,6 +1,5 @@ import { useEffect, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' import { isGsDownlinkSendEvent, @@ -12,8 +11,7 @@ import { updateGatewayStatistics } from '@console/store/actions/gateways' import { selectLatestGatewayEvent } from '@console/store/selectors/gateways' -const useConnectionReactor = () => { - const { gtwId } = useParams() +const useConnectionReactor = gtwId => { const latestEvent = useSelector(state => selectLatestGatewayEvent(state, gtwId)) const dispatch = useDispatch() const prevEvent = useRef(null) diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 1ad80232f5..2bd84b2b6c 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -538,6 +538,11 @@ "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-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.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", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 0b0971a388..a33472acac 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -538,6 +538,11 @@ "console.containers.gateway-connection.gateway-connection.connectedTooltip": "このゲートウェイはゲートウェイサーバーに接続されていますが、ネットワークはまだゲートウェイからのアクティビティ(アップリンクやステータスメッセージの送信)を登録していません", "console.containers.gateway-connection.gateway-connection.otherClusterTooltip": "このゲートウェイは、このクラスタのメッセージを処理しない外部のゲートウェイサーバーに接続されています。そのため、このゲートウェイからのアクティビティを見ることはできません", "console.containers.gateway-connection.gateway-connection.messageCountTooltip": "最後の(再)接続以降、このゲートウェイの受信アップリンクと送信ダウンリンクの量です。ゲートウェイの種類によっては、頻繁に再接続するため、カウンタがリセットされることに注意してください", + "console.containers.gateway-connection.index.lastSeenAvailableTooltip": "", + "console.containers.gateway-connection.index.disconnectedTooltip": "", + "console.containers.gateway-connection.index.connectedTooltip": "", + "console.containers.gateway-connection.index.otherClusterTooltip": "", + "console.containers.gateway-connection.index.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": "", From 0c1acb19dbe0e3477b0969bc9d9ac8c89ecef7b2 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 11:45:33 +0100 Subject: [PATCH 17/21] console: Refactor connect in gateway events --- .../containers/gateway-events/index.js | 75 ++++++++----------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/pkg/webui/console/containers/gateway-events/index.js b/pkg/webui/console/containers/gateway-events/index.js index c47953a816..a876b98e67 100644 --- a/pkg/webui/console/containers/gateway-events/index.js +++ b/pkg/webui/console/containers/gateway-events/index.js @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useMemo } 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' @@ -38,17 +38,36 @@ import { } from '@console/store/selectors/gateways' const GatewayEvents = props => { - const { - gtwId, - events, - widget, - paused, - onPauseToggle, - onClear, - onFilterChange, - truncated, - filter, - } = props + 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) { @@ -81,40 +100,12 @@ const GatewayEvents = props => { } 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 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 From 8badbf2cf49005791847ea89d87b0c7e49e89e44 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 12:01:02 +0100 Subject: [PATCH 18/21] console: Refactor connect in device events --- .../console/containers/device-events/index.js | 92 ++++++++----------- 1 file changed, 37 insertions(+), 55 deletions(-) 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 From 07556e788eaee9a978ab5ccdd7a23624de2dd24e Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 12:12:31 +0100 Subject: [PATCH 19/21] console: Refactor connect in collaborators table --- .../containers/collaborators-table/index.js | 47 ++++++------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/pkg/webui/account/containers/collaborators-table/index.js b/pkg/webui/account/containers/collaborators-table/index.js index 2d28c12b5d..093ec54553 100644 --- a/pkg/webui/account/containers/collaborators-table/index.js +++ b/pkg/webui/account/containers/collaborators-table/index.js @@ -13,9 +13,8 @@ // limitations under the License. import React from 'react' -import { connect, useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { defineMessages, useIntl } from 'react-intl' -import { bindActionCreators } from 'redux' import { createSelector } from 'reselect' import Icon from '@ttn-lw/components/icon' @@ -29,10 +28,12 @@ import FetchTable from '@ttn-lw/containers/fetch-table' import Message from '@ttn-lw/lib/components/message' import { getCollaboratorId } from '@ttn-lw/lib/selectors/id' -import PropTypes from '@ttn-lw/lib/prop-types' import sharedMessages from '@ttn-lw/lib/shared-messages' import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' -import { getCollaboratorsList, deleteCollaborator } from '@ttn-lw/lib/store/actions/collaborators' +import { + getCollaboratorsList, + deleteCollaborator as deleteCollaboratorAction, +} from '@ttn-lw/lib/store/actions/collaborators' import { selectCollaborators, selectCollaboratorsTotalCount, @@ -52,9 +53,17 @@ const m = defineMessages({ }) const CollaboratorsTable = props => { - const { clientId, currentUserId, handleDeleteCollaborator, ...rest } = props + const { ...rest } = props const dispatch = useDispatch() const intl = useIntl() + const clientId = useSelector(selectSelectedClientId) + const currentUserId = useSelector(selectUserId) + + const handleDeleteCollaborator = React.useCallback( + (collaboratorID, isUsr) => + dispatch(attachPromise(deleteCollaboratorAction('client', clientId, collaboratorID, isUsr))), + [clientId, dispatch], + ) const deleteCollaborator = React.useCallback( async ids => { @@ -194,30 +203,4 @@ const CollaboratorsTable = props => { ) } -CollaboratorsTable.propTypes = { - clientId: PropTypes.string.isRequired, - currentUserId: PropTypes.string.isRequired, - handleDeleteCollaborator: PropTypes.func.isRequired, -} - -export default connect( - state => ({ - clientId: selectSelectedClientId(state), - currentUserId: selectUserId(state), - }), - dispatch => ({ - ...bindActionCreators( - { - handleDeleteCollaborator: attachPromise(deleteCollaborator), - }, - dispatch, - ), - }), - (stateProps, dispatchProps, ownProps) => ({ - ...stateProps, - ...dispatchProps, - ...ownProps, - handleDeleteCollaborator: (collaboratorID, isUsr) => - dispatchProps.handleDeleteCollaborator('client', stateProps.clientId, collaboratorID, isUsr), - }), -)(CollaboratorsTable) +export default CollaboratorsTable From 21fad626da7e44bd163e019b29a67b7b7dc93506 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Tue, 28 Nov 2023 12:18:14 +0100 Subject: [PATCH 20/21] console: Fix locale issues --- pkg/webui/account.js | 2 +- .../containers/collaborators-table/index.js | 47 ++- pkg/webui/console.js | 2 +- .../console/components/pubsub-form/index.js | 1 + .../lib/components/full-view-error/connect.js | 22 ++ .../lib/components/full-view-error/error.js | 340 ++++++++++++++++++ .../lib/components/full-view-error/index.js | 332 +---------------- pkg/webui/locales/en.json | 34 -- pkg/webui/locales/ja.json | 70 +--- 9 files changed, 419 insertions(+), 431 deletions(-) create mode 100644 pkg/webui/lib/components/full-view-error/connect.js create mode 100644 pkg/webui/lib/components/full-view-error/error.js diff --git a/pkg/webui/account.js b/pkg/webui/account.js index 8196eca9b4..83bbfd4ec9 100644 --- a/pkg/webui/account.js +++ b/pkg/webui/account.js @@ -25,7 +25,7 @@ import Header from '@ttn-lw/components/header' import WithLocale from '@ttn-lw/lib/components/with-locale' import { ErrorView } from '@ttn-lw/lib/components/error-view' -import FullViewError from '@ttn-lw/lib/components/full-view-error' +import { FullViewError } from '@ttn-lw/lib/components/full-view-error' import Init from '@ttn-lw/lib/components/init' import Logo from '@account/containers/logo' diff --git a/pkg/webui/account/containers/collaborators-table/index.js b/pkg/webui/account/containers/collaborators-table/index.js index 093ec54553..2d28c12b5d 100644 --- a/pkg/webui/account/containers/collaborators-table/index.js +++ b/pkg/webui/account/containers/collaborators-table/index.js @@ -13,8 +13,9 @@ // limitations under the License. import React from 'react' -import { useDispatch, useSelector } from 'react-redux' +import { connect, useDispatch } from 'react-redux' import { defineMessages, useIntl } from 'react-intl' +import { bindActionCreators } from 'redux' import { createSelector } from 'reselect' import Icon from '@ttn-lw/components/icon' @@ -28,12 +29,10 @@ import FetchTable from '@ttn-lw/containers/fetch-table' import Message from '@ttn-lw/lib/components/message' import { getCollaboratorId } from '@ttn-lw/lib/selectors/id' +import PropTypes from '@ttn-lw/lib/prop-types' import sharedMessages from '@ttn-lw/lib/shared-messages' import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' -import { - getCollaboratorsList, - deleteCollaborator as deleteCollaboratorAction, -} from '@ttn-lw/lib/store/actions/collaborators' +import { getCollaboratorsList, deleteCollaborator } from '@ttn-lw/lib/store/actions/collaborators' import { selectCollaborators, selectCollaboratorsTotalCount, @@ -53,17 +52,9 @@ const m = defineMessages({ }) const CollaboratorsTable = props => { - const { ...rest } = props + const { clientId, currentUserId, handleDeleteCollaborator, ...rest } = props const dispatch = useDispatch() const intl = useIntl() - const clientId = useSelector(selectSelectedClientId) - const currentUserId = useSelector(selectUserId) - - const handleDeleteCollaborator = React.useCallback( - (collaboratorID, isUsr) => - dispatch(attachPromise(deleteCollaboratorAction('client', clientId, collaboratorID, isUsr))), - [clientId, dispatch], - ) const deleteCollaborator = React.useCallback( async ids => { @@ -203,4 +194,30 @@ const CollaboratorsTable = props => { ) } -export default CollaboratorsTable +CollaboratorsTable.propTypes = { + clientId: PropTypes.string.isRequired, + currentUserId: PropTypes.string.isRequired, + handleDeleteCollaborator: PropTypes.func.isRequired, +} + +export default connect( + state => ({ + clientId: selectSelectedClientId(state), + currentUserId: selectUserId(state), + }), + dispatch => ({ + ...bindActionCreators( + { + handleDeleteCollaborator: attachPromise(deleteCollaborator), + }, + dispatch, + ), + }), + (stateProps, dispatchProps, ownProps) => ({ + ...stateProps, + ...dispatchProps, + ...ownProps, + handleDeleteCollaborator: (collaboratorID, isUsr) => + dispatchProps.handleDeleteCollaborator('client', stateProps.clientId, collaboratorID, isUsr), + }), +)(CollaboratorsTable) diff --git a/pkg/webui/console.js b/pkg/webui/console.js index ffb1423631..fdba93024f 100644 --- a/pkg/webui/console.js +++ b/pkg/webui/console.js @@ -24,7 +24,7 @@ import { BreadcrumbsProvider } from '@ttn-lw/components/breadcrumbs/context' import Header from '@ttn-lw/components/header' import { ErrorView } from '@ttn-lw/lib/components/error-view' -import FullViewError from '@ttn-lw/lib/components/full-view-error' +import { FullViewError } from '@ttn-lw/lib/components/full-view-error' import Init from '@ttn-lw/lib/components/init' import WithLocale from '@ttn-lw/lib/components/with-locale' 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/lib/components/full-view-error/connect.js b/pkg/webui/lib/components/full-view-error/connect.js new file mode 100644 index 0000000000..618e10dc4d --- /dev/null +++ b/pkg/webui/lib/components/full-view-error/connect.js @@ -0,0 +1,22 @@ +// 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 { selectOnlineStatus } from '@ttn-lw/lib/store/selectors/status' + +export default ErrorView => + connect(state => ({ + onlineStatus: selectOnlineStatus(state), + }))(ErrorView) diff --git a/pkg/webui/lib/components/full-view-error/error.js b/pkg/webui/lib/components/full-view-error/error.js new file mode 100644 index 0000000000..96e0dca16b --- /dev/null +++ b/pkg/webui/lib/components/full-view-error/error.js @@ -0,0 +1,340 @@ +// 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, useEffect, useRef, useState } from 'react' +import { Container, Row, Col } from 'react-grid-system' +import clipboard from 'clipboard' +import { Helmet } from 'react-helmet' +import classnames from 'classnames' + +import Footer from '@ttn-lw/components/footer' +import buttonStyle from '@ttn-lw/components/button/button.styl' +import Icon from '@ttn-lw/components/icon' + +import Message from '@ttn-lw/lib/components/message' +import IntlHelmet from '@ttn-lw/lib/components/intl-helmet' + +import errorMessages from '@ttn-lw/lib/errors/error-messages' +import sharedMessages from '@ttn-lw/lib/shared-messages' +import * as cache from '@ttn-lw/lib/cache' +import { + isUnknown as isUnknownError, + isNotFoundError, + isFrontend as isFrontendError, + isBackend as isBackendError, + getCorrelationId, + getBackendErrorId, + isOAuthClientRefusedError, + isOAuthInvalidStateError, +} from '@ttn-lw/lib/errors/utils' +import statusCodeMessages from '@ttn-lw/lib/errors/status-code-messages' +import PropTypes from '@ttn-lw/lib/prop-types' +import { + selectApplicationRootPath, + selectSupportLinkConfig, + selectApplicationSiteName, + selectApplicationSiteTitle, + selectDocumentationUrlConfig, +} from '@ttn-lw/lib/selectors/env' + +import style from './error.styl' + +const appRoot = selectApplicationRootPath() +const siteName = selectApplicationSiteName() +const siteTitle = selectApplicationSiteTitle() +const supportLink = selectSupportLinkConfig() +const documentationLink = selectDocumentationUrlConfig() +const hasSupportLink = Boolean(supportLink) +const lastRefreshAttemptCacheKey = 'last-refresh-attempt' + +// Mind any rendering that is dependant on context, since the errors +// can be rendered before such context is injected. Use the `safe` +// prop to conditionally render any context-dependant nodes. +const FullViewError = ({ error, header, onlineStatus, safe, action, unexpected }) => ( +
+ {Boolean(header) && header} +
+ +
+
+
+) + +const FullViewErrorInner = ({ error, safe, action, unexpected }) => { + const isUnknown = isUnknownError(error) + const isNotFound = isNotFoundError(error) + const isFrontend = isFrontendError(error) + const isBackend = isBackendError(error) + const hasAction = Boolean(action) + const isErrorObject = error instanceof Error + const isOAuthCallback = /oauth.*\/callback$/.test(window.location.pathname) + + const errorId = getBackendErrorId(error) || 'n/a' + const correlationId = getCorrelationId(error) || 'n/a' + + const [copied, setCopied] = useState(false) + + const lastRefreshAttempt = cache.get(lastRefreshAttemptCacheKey) + let doBlankRender = false + + let errorMessage + let errorTitle + if (isNotFound) { + errorTitle = statusCodeMessages['404'] + errorMessage = errorMessages.genericNotFound + } else if (isOAuthCallback) { + errorTitle = sharedMessages.loginFailed + errorMessage = errorMessages.loginFailedDescription + if (isOAuthClientRefusedError(error) || error.error === 'access_denied') { + errorMessage = errorMessages.loginFailedAbortDescription + } else if (isOAuthInvalidStateError(error)) { + // Usually in case of state errors, the state has expired or otherwise + // invalidated since initiating the OAuth request. This can usually be + // resolved by re-initiating the OAuth flow. We need to keep track of + // refresh attempts though to avoid infinite refresh loops. + if (!lastRefreshAttempt || Date.now() - lastRefreshAttempt > 6 * 1000) { + cache.set(lastRefreshAttemptCacheKey, Date.now()) + doBlankRender = true + window.location = appRoot + } + } + } else if (isFrontend) { + errorMessage = error.errorMessage + if (Boolean(error.errorTitle)) { + errorTitle = error.errorTitle + } + } else if (!isUnknown) { + errorTitle = errorMessages.error + errorMessage = errorMessages.errorOccurred + } else { + errorTitle = errorMessages.error + errorMessage = errorMessages.genericError + } + + const copiedTimer = useRef(undefined) + const handleCopyClick = useCallback(() => { + if (!copied) { + setCopied(true) + copiedTimer.current = setTimeout(() => setCopied(false), 3000) + } + }, [setCopied, copied]) + + const copyButton = useRef(null) + useEffect(() => { + if (copyButton.current) { + new clipboard(copyButton.current) + } + return () => { + // Clear timer on unmount. + if (copiedTimer.current) { + clearTimeout(copiedTimer.current) + } + } + }, []) + + const errorDetails = JSON.stringify(error, undefined, 2) + const hasErrorDetails = + (!isNotFound && Boolean(error) && errorDetails.length > 2) || (isFrontend && error.errorCode) + const buttonClasses = classnames(buttonStyle.button, style.actionButton) + + // Do not render anything when performing a redirect. + if (doBlankRender) { + return null + } + + return ( +
+ + + + {safe ? ( + + Error + + ) : ( + + )} +

+ + +

+
+ + {!isNotFound && unexpected && ( + <> + {' '} + +
+ + + )} +
+
+ {isNotFound && ( + + + + + )} + {isOAuthCallback && ( + + + + + )} + {hasAction && ( + + )} + {hasSupportLink && !isNotFound && ( + <> + + + + + {hasErrorDetails && ( + + )} + + )} +
+ {hasErrorDetails && ( + <> + {isErrorObject && ( + <> +
+
+ + Error Type: {error.name} + +
+ + )} + {isFrontend && ( + <> +
+
+ + Frontend Error ID: {error.errorCode} + +
+ + )} + {isBackend && ( + <> +
+
+ + Error ID: {errorId} + + + Correlation ID: {correlationId} + +
+ + )} +
+
+ + + +
{errorDetails}
+ +
+ + )} + +
+
+
+ ) +} + +FullViewErrorInner.propTypes = { + action: PropTypes.shape({ + action: PropTypes.func.isRequired, + icon: PropTypes.string.isRequired, + message: PropTypes.message.isRequired, + }), + error: PropTypes.error.isRequired, + safe: PropTypes.bool, + unexpected: PropTypes.bool, +} + +FullViewErrorInner.defaultProps = { + action: undefined, + safe: false, + unexpected: true, +} + +FullViewError.propTypes = { + action: PropTypes.shape({ + action: PropTypes.func.isRequired, + icon: PropTypes.string.isRequired, + message: PropTypes.message.isRequired, + }), + error: PropTypes.error.isRequired, + header: PropTypes.node, + onlineStatus: PropTypes.onlineStatus, + safe: PropTypes.bool, + unexpected: PropTypes.bool, +} + +FullViewError.defaultProps = { + action: undefined, + header: undefined, + onlineStatus: undefined, + safe: false, + unexpected: true, +} + +export { FullViewError, FullViewErrorInner } diff --git a/pkg/webui/lib/components/full-view-error/index.js b/pkg/webui/lib/components/full-view-error/index.js index 4eb96545b9..e28a0ab4df 100644 --- a/pkg/webui/lib/components/full-view-error/index.js +++ b/pkg/webui/lib/components/full-view-error/index.js @@ -12,333 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useCallback, useEffect, useRef, useState } from 'react' -import { Container, Row, Col } from 'react-grid-system' -import clipboard from 'clipboard' -import { Helmet } from 'react-helmet' -import classnames from 'classnames' -import { useSelector } from 'react-redux' +import { FullViewError, FullViewErrorInner } from './error' +import connect from './connect' -import Footer from '@ttn-lw/components/footer' -import buttonStyle from '@ttn-lw/components/button/button.styl' -import Icon from '@ttn-lw/components/icon' +const ConnectedFullErrorView = connect(FullViewError) -import Message from '@ttn-lw/lib/components/message' -import IntlHelmet from '@ttn-lw/lib/components/intl-helmet' - -import errorMessages from '@ttn-lw/lib/errors/error-messages' -import sharedMessages from '@ttn-lw/lib/shared-messages' -import * as cache from '@ttn-lw/lib/cache' -import { - isUnknown as isUnknownError, - isNotFoundError, - isFrontend as isFrontendError, - isBackend as isBackendError, - getCorrelationId, - getBackendErrorId, - isOAuthClientRefusedError, - isOAuthInvalidStateError, -} from '@ttn-lw/lib/errors/utils' -import statusCodeMessages from '@ttn-lw/lib/errors/status-code-messages' -import PropTypes from '@ttn-lw/lib/prop-types' -import { - selectApplicationRootPath, - selectSupportLinkConfig, - selectApplicationSiteName, - selectApplicationSiteTitle, - selectDocumentationUrlConfig, -} from '@ttn-lw/lib/selectors/env' -import { selectOnlineStatus } from '@ttn-lw/lib/store/selectors/status' - -import style from './error.styl' - -const appRoot = selectApplicationRootPath() -const siteName = selectApplicationSiteName() -const siteTitle = selectApplicationSiteTitle() -const supportLink = selectSupportLinkConfig() -const documentationLink = selectDocumentationUrlConfig() -const hasSupportLink = Boolean(supportLink) -const lastRefreshAttemptCacheKey = 'last-refresh-attempt' - -// Mind any rendering that is dependant on context, since the errors -// can be rendered before such context is injected. Use the `safe` -// prop to conditionally render any context-dependant nodes. -const FullViewError = ({ error, header, safe, action, unexpected }) => { - const onlineStatus = useSelector(selectOnlineStatus) - - return ( -
- {Boolean(header) && header} -
- -
-
-
- ) -} - -const FullViewErrorInner = ({ error, safe, action, unexpected }) => { - const isUnknown = isUnknownError(error) - const isNotFound = isNotFoundError(error) - const isFrontend = isFrontendError(error) - const isBackend = isBackendError(error) - const hasAction = Boolean(action) - const isErrorObject = error instanceof Error - const isOAuthCallback = /oauth.*\/callback$/.test(window.location.pathname) - - const errorId = getBackendErrorId(error) || 'n/a' - const correlationId = getCorrelationId(error) || 'n/a' - - const [copied, setCopied] = useState(false) - - const lastRefreshAttempt = cache.get(lastRefreshAttemptCacheKey) - let doBlankRender = false - - let errorMessage - let errorTitle - if (isNotFound) { - errorTitle = statusCodeMessages['404'] - errorMessage = errorMessages.genericNotFound - } else if (isOAuthCallback) { - errorTitle = sharedMessages.loginFailed - errorMessage = errorMessages.loginFailedDescription - if (isOAuthClientRefusedError(error) || error.error === 'access_denied') { - errorMessage = errorMessages.loginFailedAbortDescription - } else if (isOAuthInvalidStateError(error)) { - // Usually in case of state errors, the state has expired or otherwise - // invalidated since initiating the OAuth request. This can usually be - // resolved by re-initiating the OAuth flow. We need to keep track of - // refresh attempts though to avoid infinite refresh loops. - if (!lastRefreshAttempt || Date.now() - lastRefreshAttempt > 6 * 1000) { - cache.set(lastRefreshAttemptCacheKey, Date.now()) - doBlankRender = true - window.location = appRoot - } - } - } else if (isFrontend) { - errorMessage = error.errorMessage - if (Boolean(error.errorTitle)) { - errorTitle = error.errorTitle - } - } else if (!isUnknown) { - errorTitle = errorMessages.error - errorMessage = errorMessages.errorOccurred - } else { - errorTitle = errorMessages.error - errorMessage = errorMessages.genericError - } - - const copiedTimer = useRef(undefined) - const handleCopyClick = useCallback(() => { - if (!copied) { - setCopied(true) - copiedTimer.current = setTimeout(() => setCopied(false), 3000) - } - }, [setCopied, copied]) - - const copyButton = useRef(null) - useEffect(() => { - if (copyButton.current) { - new clipboard(copyButton.current) - } - return () => { - // Clear timer on unmount. - if (copiedTimer.current) { - clearTimeout(copiedTimer.current) - } - } - }, []) - - const errorDetails = JSON.stringify(error, undefined, 2) - const hasErrorDetails = - (!isNotFound && Boolean(error) && errorDetails.length > 2) || (isFrontend && error.errorCode) - const buttonClasses = classnames(buttonStyle.button, style.actionButton) - - // Do not render anything when performing a redirect. - if (doBlankRender) { - return null - } - - return ( -
- - - - {safe ? ( - - Error - - ) : ( - - )} -

- - -

-
- - {!isNotFound && unexpected && ( - <> - {' '} - -
- - - )} -
-
- {isNotFound && ( - - - - - )} - {isOAuthCallback && ( - - - - - )} - {hasAction && ( - - )} - {hasSupportLink && !isNotFound && ( - <> - - - - - {hasErrorDetails && ( - - )} - - )} -
- {hasErrorDetails && ( - <> - {isErrorObject && ( - <> -
-
- - Error Type: {error.name} - -
- - )} - {isFrontend && ( - <> -
-
- - Frontend Error ID: {error.errorCode} - -
- - )} - {isBackend && ( - <> -
-
- - Error ID: {errorId} - - - Correlation ID: {correlationId} - -
- - )} -
-
- - - -
{errorDetails}
- -
- - )} - -
-
-
- ) -} - -FullViewErrorInner.propTypes = { - action: PropTypes.shape({ - action: PropTypes.func.isRequired, - icon: PropTypes.string.isRequired, - message: PropTypes.message.isRequired, - }), - error: PropTypes.error.isRequired, - safe: PropTypes.bool, - unexpected: PropTypes.bool, -} - -FullViewErrorInner.defaultProps = { - action: undefined, - safe: false, - unexpected: true, -} - -FullViewError.propTypes = { - action: PropTypes.shape({ - action: PropTypes.func.isRequired, - icon: PropTypes.string.isRequired, - message: PropTypes.message.isRequired, - }), - error: PropTypes.error.isRequired, - header: PropTypes.node, - safe: PropTypes.bool, - unexpected: PropTypes.bool, -} - -FullViewError.defaultProps = { - action: undefined, - header: undefined, - safe: false, - unexpected: true, -} - -export { FullViewError as default, FullViewErrorInner } +export { ConnectedFullErrorView as default, FullViewError, FullViewErrorInner } diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 2bd84b2b6c..8f4e19e56c 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -205,16 +205,6 @@ "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", @@ -499,25 +489,17 @@ "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.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.device-selection.model-select.model-select.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.", @@ -533,27 +515,11 @@ "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-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.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-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", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index a33472acac..2f9f10b27d 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -205,26 +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.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", @@ -352,7 +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": "", + "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": "クライアント証明書", @@ -499,25 +489,17 @@ "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.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.device-selection.model-select.model-select.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": "", @@ -525,35 +507,19 @@ "console.containers.device-template-format-select.index.title": "ファイルフォーマット", "console.containers.device-template-format-select.index.warning": "エンドデバイスのテンプレートフォーマットが利用できません", "console.containers.device-title-section.index.uplinkDownlinkTooltip": "前回のフレームカウンタリセット以降、このエンドデバイスの送信アップリンクと受信ダウンリンクの数です", - "console.containers.device-title-section.index.lastSeenAvailableTooltip": "ネットワークがこのゲートウェイの最後のアクティビティを登録してから経過した時間です。これは、このゲートウェイの受信したアップリンク、または送信したステータスメッセージから決定されます", - "console.containers.device-title-section.index.noActivityTooltip": "", + "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-connection.index.lastSeenAvailableTooltip": "", - "console.containers.gateway-connection.index.disconnectedTooltip": "", - "console.containers.gateway-connection.index.connectedTooltip": "", - "console.containers.gateway-connection.index.otherClusterTooltip": "", - "console.containers.gateway-connection.index.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": "", From 22127c99dc2fb63938bf0bba86404a4cfe23cb23 Mon Sep 17 00:00:00 2001 From: Pavel Jankoski Date: Wed, 29 Nov 2023 20:33:33 +0100 Subject: [PATCH 21/21] console: Removed commented code --- cypress/e2e/console/integrations/pub-subs/create.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/cypress/e2e/console/integrations/pub-subs/create.spec.js b/cypress/e2e/console/integrations/pub-subs/create.spec.js index 27f3fe3c28..78f4616d4e 100644 --- a/cypress/e2e/console/integrations/pub-subs/create.spec.js +++ b/cypress/e2e/console/integrations/pub-subs/create.spec.js @@ -347,7 +347,6 @@ describe('Application Pub/Sub create', () => { cy.intercept('GET', `/api/v3/as/configuration`, response) }) it('succeeds setting MQTT as default provider', () => { - // Cy.findByLabelText('MQTT').should('be.checked') cy.findByLabelText('NATS').should('be.disabled') cy.findByText(description).should('be.visible') }) @@ -374,7 +373,6 @@ describe('Application Pub/Sub create', () => { }) it('succeeds setting NATS as default provider', () => { - // Cy.findByLabelText('NATS').should('be.checked') cy.findByLabelText('MQTT').should('be.disabled') cy.findByText(description).should('be.visible') })