From e91742ef11acc717ffa876636ebf7d5e5a5a7617 Mon Sep 17 00:00:00 2001 From: Kevin Schiffer Date: Tue, 27 Jun 2023 22:54:49 +0200 Subject: [PATCH] console,account: Apply various fixes to make cypress pass --- pkg/webui/components/prompt/index.js | 67 ++++-------------- .../index.js | 9 +-- .../payload-formatters-form/index.js | 15 ++-- .../pubsub-form/validation-schema.js | 6 +- .../console/containers/api-key-form/create.js | 2 +- .../console/containers/api-key-form/edit.js | 2 +- pkg/webui/console/lib/attributes.js | 50 ++++++------- pkg/webui/console/views/app/index.js | 2 +- .../index.js | 11 +-- .../index.js | 3 +- .../identity-server-form/index.js | 8 ++- .../identity-server-form/validation-schema.js | 12 ++-- .../basic-settings-form/index.js | 13 ++-- .../basic-settings-form/validation-schema.js | 12 ++-- .../views/gateway-general-settings/index.js | 3 +- .../console/views/user-api-keys/index.js | 2 +- pkg/webui/console/views/user/index.js | 2 +- .../containers/collaborator-form/index.js | 4 +- pkg/webui/lib/hooks/use-prompt.js | 70 ------------------- pkg/webui/locales/en.json | 66 +++++++++++++++-- 20 files changed, 149 insertions(+), 210 deletions(-) delete mode 100644 pkg/webui/lib/hooks/use-prompt.js diff --git a/pkg/webui/components/prompt/index.js b/pkg/webui/components/prompt/index.js index 728f61ead2e..c7debb7e907 100644 --- a/pkg/webui/components/prompt/index.js +++ b/pkg/webui/components/prompt/index.js @@ -13,68 +13,34 @@ // limitations under the License. import React from 'react' -import { useNavigate } from 'react-router-dom' +import { unstable_usePrompt } from 'react-router-dom' import PortalledModal from '@ttn-lw/components/modal/portalled' -import { usePrompt } from '@ttn-lw/lib/hooks/use-prompt' import PropTypes from '@ttn-lw/lib/prop-types' -/* - * `` is used to prompt the user before navigating from the current page. This is - * helpful to avoid losing the state of the current page because of accidental misclick, for example, - * for half-filled forms. - */ const Prompt = props => { - const { modal, children, when, shouldBlockNavigation, onApprove, onCancel } = props + const { modal, children, message, when } = props + const [showModal, setShowModal] = React.useState(false) - const navigate = useNavigate() - - const [state, setState] = React.useState({ - showModal: false, - nextLocation: undefined, - confirmedLocationChange: false, - }) - const { showModal, nextLocation, confirmedLocationChange } = state - - const handleModalShow = React.useCallback(nextLocation => { - setState(prev => ({ ...prev, showModal: true, nextLocation })) - }, []) - - const handleModalHide = React.useCallback(() => { - setState(prev => ({ ...prev, showModal: false })) - }, []) + // The usage of `unstable_usePrompt` might change as the library updates. + const continueNavigation = unstable_usePrompt(when, message) const handleModalComplete = React.useCallback( approved => { - setState(prev => ({ ...prev, confirmedLocationChange: approved })) - handleModalHide() - }, - [handleModalHide], - ) - - const handlePromptTrigger = React.useCallback( - location => { - if (!confirmedLocationChange && shouldBlockNavigation(location)) { - handleModalShow(location) - - return false + setShowModal(false) + if (approved) { + continueNavigation() } - - return true }, - [handleModalShow, shouldBlockNavigation, confirmedLocationChange], + [continueNavigation], ) - usePrompt(handlePromptTrigger, when) - React.useEffect(() => { - if (confirmedLocationChange) { - onApprove(nextLocation, navigate) - } else { - onCancel(nextLocation, navigate) + if (when) { + setShowModal(true) } - }, [confirmedLocationChange, navigate, nextLocation, onApprove, onCancel]) + }, [when]) return ( @@ -85,20 +51,13 @@ const Prompt = props => { Prompt.propTypes = { children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]), + message: PropTypes.string.isRequired, modal: PropTypes.shape({ ...PortalledModal.Modal.propTypes }).isRequired, - onApprove: PropTypes.func, - onCancel: PropTypes.func, - shouldBlockNavigation: PropTypes.func, when: PropTypes.bool.isRequired, } Prompt.defaultProps = { children: undefined, - shouldBlockNavigation: () => true, - onApprove: (location, navigate) => { - navigate(location) - }, - onCancel: () => null, } export default Prompt diff --git a/pkg/webui/console/components/application-general-settings-form/index.js b/pkg/webui/console/components/application-general-settings-form/index.js index a59607fff9e..d67feafa83a 100644 --- a/pkg/webui/console/components/application-general-settings-form/index.js +++ b/pkg/webui/console/components/application-general-settings-form/index.js @@ -48,14 +48,9 @@ const validationSchema = Yup.object().shape({ .min(3, Yup.passValues(sharedMessages.validateTooShort)) .max(50, Yup.passValues(sharedMessages.validateTooLong)), description: Yup.string().max(150, Yup.passValues(sharedMessages.validateTooLong)), - attributes: Yup.array() + attributes: Yup.object() .nullable() - .max(10, Yup.passValues(sharedMessages.attributesValidateTooMany)) - .test( - 'has no empty string values', - sharedMessages.attributesValidateRequired, - attributeValidCheck, - ) + .test('has no null values', sharedMessages.attributesValidateRequired, attributeValidCheck) .test( 'has key length longer than 2', sharedMessages.attributeKeyValidateTooShort, diff --git a/pkg/webui/console/components/payload-formatters-form/index.js b/pkg/webui/console/components/payload-formatters-form/index.js index 2b46e37291a..410fb493326 100644 --- a/pkg/webui/console/components/payload-formatters-form/index.js +++ b/pkg/webui/console/components/payload-formatters-form/index.js @@ -19,7 +19,6 @@ import { Col, Row } from 'react-grid-system' import TYPES from '@console/constants/formatter-types' -import Prompt from '@ttn-lw/components/prompt' import Select from '@ttn-lw/components/select' import Form from '@ttn-lw/components/form' import SubmitButton from '@ttn-lw/components/submit-button' @@ -104,9 +103,10 @@ const validationSchema = Yup.object().shape({ .matches(addressRegexp, Yup.passValues(sharedMessages.validateAddressFormat)) .when(FIELD_NAMES.SELECT, { is: TYPES.GRPC, - then: Yup.string() - .required(sharedMessages.validateRequired) - .max(40960, Yup.passValues(sharedMessages.validateTooLong)), + then: schema => + schema + .required(sharedMessages.validateRequired) + .max(40960, Yup.passValues(sharedMessages.validateTooLong)), }), }) @@ -422,6 +422,12 @@ class PayloadFormattersForm extends React.Component { /> )} {this.formatter} + {/* + // TODO: Refactor to use data API and re-enable prompt. + // NOTE: Unfortunately react router v6 requires us to do further + // refactoring to use the data API to be able to use `usePrompt` + // again, which is required to make the Prompt component work. + // For now we will disable the prompt. + */} )} diff --git a/pkg/webui/console/components/pubsub-form/validation-schema.js b/pkg/webui/console/components/pubsub-form/validation-schema.js index 467d201755d..ea78e3d253f 100644 --- a/pkg/webui/console/components/pubsub-form/validation-schema.js +++ b/pkg/webui/console/components/pubsub-form/validation-schema.js @@ -27,7 +27,11 @@ import { qosLevels } from './qos-options' import providers from './providers' export default Yup.object().shape({ - pub_sub_id: Yup.string(), + pub_sub_id: Yup.string() + .matches(idRegexp, Yup.passValues(sharedMessages.validateIdFormat)) + .required(sharedMessages.validateRequired) + .min(2, Yup.passValues(sharedMessages.validateTooShort)) + .max(36, Yup.passValues(sharedMessages.validateTooLong)), format: Yup.string().required(sharedMessages.validateRequired), base_topic: Yup.string(), nats: Yup.object().when('_provider', { diff --git a/pkg/webui/console/containers/api-key-form/create.js b/pkg/webui/console/containers/api-key-form/create.js index 37f4f974a0a..5fcf38eb63f 100644 --- a/pkg/webui/console/containers/api-key-form/create.js +++ b/pkg/webui/console/containers/api-key-form/create.js @@ -40,7 +40,7 @@ const CreateForm = ({ entity, entityId }) => { const handleModalApprove = useCallback(async () => { setModal(null) // Navigate back to list - navigate('../') + navigate('..') }, [navigate]) const handleCreate = useCallback( diff --git a/pkg/webui/console/containers/api-key-form/edit.js b/pkg/webui/console/containers/api-key-form/edit.js index b0526c88817..6993be0cf38 100644 --- a/pkg/webui/console/containers/api-key-form/edit.js +++ b/pkg/webui/console/containers/api-key-form/edit.js @@ -60,7 +60,7 @@ const ApiKeyEditForm = ({ entity, entityId }) => { message: m.deleteSuccess, type: toast.types.SUCCESS, }) - navigate('../') + navigate('..') }, [navigate]) const handleEditSuccess = useCallback(async () => { diff --git a/pkg/webui/console/lib/attributes.js b/pkg/webui/console/lib/attributes.js index d61998aeee1..02ad62b2400 100644 --- a/pkg/webui/console/lib/attributes.js +++ b/pkg/webui/console/lib/attributes.js @@ -14,7 +14,7 @@ import { id as idRegexp } from '@ttn-lw/lib/regexp' -export const mapFormValueToAttributes = formValue => +export const encodeAttributes = formValue => (Array.isArray(formValue) && formValue.reduce( (result, { key, value }) => ({ @@ -23,9 +23,9 @@ export const mapFormValueToAttributes = formValue => }), {}, )) || - null + undefined -export const mapAttributesToFormValue = attributesType => +export const decodeAttributes = attributesType => (attributesType && Object.keys(attributesType).reduce( (result, key) => @@ -37,30 +37,26 @@ export const mapAttributesToFormValue = attributesType => )) || [] -export const attributeValidCheck = attributes => - attributes === undefined || - attributes === null || - (attributes instanceof Array && - (attributes.length === 0 || - attributes.every(attribute => Boolean(attribute.key) && Boolean(attribute.value)))) +export const attributesCountCheck = object => + object === undefined || + object === null || + (object instanceof Object && Object.keys(object).length <= 10) +export const attributeValidCheck = object => + object === undefined || + object === null || + (object instanceof Object && Object.values(object).every(attribute => Boolean(attribute))) -export const attributeTooShortCheck = attributes => - attributes === undefined || - attributes === null || - (attributes instanceof Array && - (attributes.length === 0 || - attributes.every(attribute => RegExp(idRegexp).test(attribute.key)))) +export const attributeTooShortCheck = object => + object === undefined || + object === null || + (object instanceof Object && Object.keys(object).every(key => RegExp(idRegexp).test(key))) -export const attributeKeyTooLongCheck = attributes => - attributes === undefined || - attributes === null || - (attributes instanceof Array && - (attributes.length === 0 || - attributes.every(attribute => attribute.key && attribute.key.length <= 36))) +export const attributeKeyTooLongCheck = object => + object === undefined || + object === null || + (object instanceof Object && Object.keys(object).every(key => key.length <= 36)) -export const attributeValueTooLongCheck = attributes => - attributes === undefined || - attributes === null || - (attributes instanceof Array && - (attributes.length === 0 || - attributes.every(attribute => attribute.value && attribute.value.length <= 200))) +export const attributeValueTooLongCheck = object => + object === undefined || + object === null || + (object instanceof Object && Object.values(object).every(value => value.length <= 200)) diff --git a/pkg/webui/console/views/app/index.js b/pkg/webui/console/views/app/index.js index 51059e15f49..5d3eba517ff 100644 --- a/pkg/webui/console/views/app/index.js +++ b/pkg/webui/console/views/app/index.js @@ -168,7 +168,7 @@ class ConsoleApp extends React.PureComponent { - + diff --git a/pkg/webui/console/views/application-integrations-webhook-edit/index.js b/pkg/webui/console/views/application-integrations-webhook-edit/index.js index c64664c6a69..63417b3a99f 100644 --- a/pkg/webui/console/views/application-integrations-webhook-edit/index.js +++ b/pkg/webui/console/views/application-integrations-webhook-edit/index.js @@ -109,15 +109,10 @@ const webhookEntitySelector = [ const ApplicationWebhookEdit = () => { const { appId, webhookId } = useParams() - const healthStatusEnabled = useSelector(selectWebhooksHealthStatusEnabled) - const hasUnhealthyWebhookConfig = useSelector(selectWebhookHasUnhealthyConfig) - return ( - - - - - + + + ) } diff --git a/pkg/webui/console/views/application-integrations-webhooks/index.js b/pkg/webui/console/views/application-integrations-webhooks/index.js index d6f51ee9601..cb0526e9d8d 100644 --- a/pkg/webui/console/views/application-integrations-webhooks/index.js +++ b/pkg/webui/console/views/application-integrations-webhooks/index.js @@ -54,9 +54,8 @@ const ApplicationWebhooksInner = () => { { const initialValues = { ...device, _external_js: hasExternalJs(device), - attributes: mapAttributesToFormValue(device.attributes), + attributes: device.attributes, } return validationSchema.cast(initialValues, { context: validationContext }) @@ -115,7 +115,7 @@ const IdentityServerForm = React.memo(props => { const onFormSubmit = React.useCallback( async (values, { resetForm, setSubmitting }) => { const castedValues = validationSchema.cast(values, { context: validationContext }) - const attributes = mapFormValueToAttributes(values.attributes) + const { attributes } = values if (isEqual(initialValues.attributes || {}, attributes)) { delete castedValues.attributes @@ -286,6 +286,8 @@ const IdentityServerForm = React.memo(props => { addMessage={sharedMessages.addAttributes} component={KeyValueMap} description={sharedMessages.attributeDescription} + encode={encodeAttributes} + decode={decodeAttributes} /> diff --git a/pkg/webui/console/views/device-general-settings/identity-server-form/validation-schema.js b/pkg/webui/console/views/device-general-settings/identity-server-form/validation-schema.js index c49bfe3682c..25618704840 100644 --- a/pkg/webui/console/views/device-general-settings/identity-server-form/validation-schema.js +++ b/pkg/webui/console/views/device-general-settings/identity-server-form/validation-schema.js @@ -21,6 +21,7 @@ import { attributeTooShortCheck, attributeKeyTooLongCheck, attributeValueTooLongCheck, + attributesCountCheck, } from '@console/lib/attributes' import { address as addressRegexp } from '@console/lib/regexp' import { parseLorawanMacVersion, generate16BytesKey } from '@console/lib/device-utils' @@ -110,13 +111,14 @@ const validationSchema = Yup.object() }) }, ), - attributes: Yup.array() - .max(10, Yup.passValues(sharedMessages.attributesValidateTooMany)) + attributes: Yup.object() + .nullable() .test( - 'has no empty string values', - sharedMessages.attributesValidateRequired, - attributeValidCheck, + 'has no more than 10 keys', + sharedMessages.attributesValidateTooMany, + attributesCountCheck, ) + .test('has no null values', sharedMessages.attributesValidateRequired, attributeValidCheck) .test( 'has key length longer than 2', sharedMessages.attributeKeyValidateTooShort, diff --git a/pkg/webui/console/views/gateway-general-settings/basic-settings-form/index.js b/pkg/webui/console/views/gateway-general-settings/basic-settings-form/index.js index d82f178a4fe..3e598f508d8 100644 --- a/pkg/webui/console/views/gateway-general-settings/basic-settings-form/index.js +++ b/pkg/webui/console/views/gateway-general-settings/basic-settings-form/index.js @@ -28,7 +28,7 @@ import PropTypes from '@ttn-lw/lib/prop-types' import sharedMessages from '@ttn-lw/lib/shared-messages' import tooltipIds from '@ttn-lw/lib/constants/tooltip-ids' -import { mapAttributesToFormValue } from '@console/lib/attributes' +import { encodeAttributes, decodeAttributes } from '@console/lib/attributes' import m from '../messages' @@ -77,14 +77,7 @@ const BasicSettingsForm = React.memo(props => { [onDelete], ) - const initialValues = React.useMemo(() => { - const initialValues = { - ...gateway, - attributes: mapAttributesToFormValue(gateway.attributes), - } - - return validationSchema.cast(initialValues) - }, [gateway]) + const initialValues = React.useMemo(() => validationSchema.cast(gateway), [gateway]) const onFormSubmit = React.useCallback( async (values, { resetForm, setSubmitting }) => { @@ -197,6 +190,8 @@ const BasicSettingsForm = React.memo(props => { component={KeyValueMap} description={sharedMessages.attributeDescription} tooltipId={tooltipIds.GATEWAY_ATTRIBUTES} + encode={encodeAttributes} + decode={decodeAttributes} /> { const handleSubmit = useCallback( async values => { const formValues = { ...values } - const attributes = mapFormValueToAttributes(formValues.attributes) + const { attributes } = formValues if (isEqual(gateway.attributes || {}, attributes)) { delete formValues.attributes } diff --git a/pkg/webui/console/views/user-api-keys/index.js b/pkg/webui/console/views/user-api-keys/index.js index dda44fc91c4..0c4fd85267b 100644 --- a/pkg/webui/console/views/user-api-keys/index.js +++ b/pkg/webui/console/views/user-api-keys/index.js @@ -41,7 +41,7 @@ const UserApiKeys = () => { - + ( - + diff --git a/pkg/webui/containers/collaborator-form/index.js b/pkg/webui/containers/collaborator-form/index.js index 838b074dcd8..b1f7057dd7e 100644 --- a/pkg/webui/containers/collaborator-form/index.js +++ b/pkg/webui/containers/collaborator-form/index.js @@ -86,10 +86,10 @@ const CollaboratorForm = props => { resetForm({ values }) if (!update) { - navigate('../') + navigate('..') } else { toast({ - message: sharedMessages.collaboradorUpdateSuccess, + message: sharedMessages.collaboratorUpdateSuccess, type: toast.types.SUCCESS, }) } diff --git a/pkg/webui/lib/hooks/use-prompt.js b/pkg/webui/lib/hooks/use-prompt.js deleted file mode 100644 index e75f085e124..00000000000 --- a/pkg/webui/lib/hooks/use-prompt.js +++ /dev/null @@ -1,70 +0,0 @@ -// 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. -// 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. - -/** - * These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'. - * Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315 - * Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381. - */ -import { useContext, useEffect, useCallback } from 'react' -import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom' -/** - * Blocks all navigation attempts. This is useful for preventing the page from - * changing until some condition is met, like saving form data. - * - * @param {Function} blocker - Blocking function. - * @param {boolean} when - Whether to block or not. - * @see https://reactrouter.com/api/useBlocker - */ -export const useBlocker = (blocker, when = true) => { - const { navigator } = useContext(NavigationContext) - - useEffect(() => { - if (!when) return - - const unblock = navigator.block(tx => { - const autoUnblockingTx = { - ...tx, - retry: () => { - // Automatically unblock the transition so it can play all the way - // through before retrying it. TODO: Figure out how to re-enable - // this block if the transition is cancelled for some reason. - unblock() - tx.retry() - }, - } - - blocker(autoUnblockingTx) - }) - - return unblock - }, [navigator, blocker, when]) -} -/** - * Prompts the user with an Alert before they leave the current screen. - * - * @param {Function} message - Message to display. - * @param {boolean} when - Whether to prompt or not. - */ -export const usePrompt = (message, when = true) => { - const blocker = useCallback( - tx => { - // eslint-disable-next-line no-alert - if (window.confirm(message)) tx.retry() - }, - [message], - ) - - useBlocker(blocker, when) -} diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 69b7b3f1e5b..159ebac4bc6 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -91,6 +91,15 @@ "account.containers.tokens-table.index.deleteAllButton": "Invalidate all access tokens", "account.containers.tokens-table.index.expires": "Expires", "account.containers.tokens-table.index.accessTokens": "Access tokens", + "account.containers.tokens-table.tokens-table.tableTitle": "Access tokens", + "account.containers.tokens-table.tokens-table.deleteSuccess": "Access token invalidated", + "account.containers.tokens-table.tokens-table.deleteFail": "There was an error and the access token could not be invalidated", + "account.containers.tokens-table.tokens-table.deleteButton": "Invalidate this access token", + "account.containers.tokens-table.tokens-table.deleteAllSuccess": "All access tokens invalidated", + "account.containers.tokens-table.tokens-table.deleteAllFail": "There was an error and the access tokens could not be invalidated", + "account.containers.tokens-table.tokens-table.deleteAllButton": "Invalidate all access tokens", + "account.containers.tokens-table.tokens-table.expires": "Expires", + "account.containers.tokens-table.tokens-table.accessTokens": "Access tokens", "account.views.authorize.index.modalTitle": "Request for permission", "account.views.authorize.index.modalSubtitle": "{clientName} is requesting to be granted the following rights:", "account.views.authorize.index.loginInfo": "You are logged in as {userId}.", @@ -178,8 +187,8 @@ "components.offline-status.index.offline": "{applicationName} is offline. Please check your internet connection.", "components.offline-status.index.online": "{applicationName} is back online", "components.progress-bar.index.estimatedCompletion": "Estimated completion {eta}", - "components.progress-bar.index.progress": "{current} of {target}", - "components.progress-bar.index.percentage": "{percentage}% finished", + "components.progress-bar.index.progress": "{current, number} of {target, number}", + "components.progress-bar.index.percentage": "{percentage, number, percent} finished", "components.qr-modal-button.index.scanEndDevice": "Scan end device QR code", "components.qr-modal-button.index.scanEndDeviceContinue": "Please scan the QR code to continue. {qrScanDoc}", "components.qr-modal-button.index.invalidData": "Invalid QR code data. Please note that only TR005 LoRaWAN® Device Identification QR Code can be scanned. Some devices have unrelated QR codes printed on them that cannot be used.", @@ -208,6 +217,10 @@ "components.wizard.form.next-button.next": "Next", "components.wizard.form.next-button.complete": "Complete", "components.wizard.form.prev-button.prev": "Previous", + "console.components.api-key-form.edit.deleteKey": "Delete key", + "console.components.api-key-form.edit.modalWarning": "Are you sure you want to delete the {keyName} API key? Deleting an API key cannot be undone.", + "console.components.api-key-form.edit.updateSuccess": "API key updated", + "console.components.api-key-form.edit.deleteSuccess": "API key deleted", "console.components.api-key-modal.index.title": "Please copy newly created API key", "console.components.api-key-modal.index.subtitle": "You won't be able to view the key afterward", "console.components.api-key-modal.index.buttonMessage": "I have copied the key", @@ -489,7 +502,13 @@ "console.containers.applications-table.index.purgeSuccess": "Application purged", "console.containers.applications-table.index.purgeFail": "There was an error and the application could not be purged", "console.containers.applications-table.index.otherClusterTooltip": "This application is registered on a different cluster (`{host}`). To access this application, use the Console of the cluster that this application was registered on.", + "console.containers.autosuggest.index.noOptionsMessage": "No matching user or organization was found", + "console.containers.autosuggest.index.suggestions": "Suggestions", "console.containers.collaborators-table.index.id": "User / Organization ID", + "console.containers.deployment-component-status.index.availableComponents": "Available components", + "console.containers.deployment-component-status.index.versionInfo": "Deployment", + "console.containers.deployment-component-status.index.statusPage": "Go to status page", + "console.containers.deployment-component-status.index.seeChangelog": "See changelog", "console.containers.dev-addr-input.index.devAddrFetchingFailure": "There was an error and the end device address could not be generated", "console.containers.device-importer.index.proceed": "Proceed to end device list", "console.containers.device-importer.index.retry": "Retry from scratch", @@ -504,17 +523,17 @@ "console.containers.device-importer.index.conversionErrorTitle": "Could not import devices", "console.containers.device-importer.index.conversionErrorMessage": "An error occurred while processing the provided end device template. This could be due to invalid format, syntax or file encoding. Please check the provided template file and try again. See also our documentation on Importing End Devices for more information.", "console.containers.device-importer.index.incompleteWarningTitle": "Not all devices imported successfully", - "console.containers.device-importer.index.incompleteWarningMessage": "{count} {count, plural, one {end device} other {end devices}} could not be imported successfully, because {count, plural, one {its} other {their}} registration attempt resulted in an error", + "console.containers.device-importer.index.incompleteWarningMessage": "{count, number} {count, plural, one {end device} other {end devices}} could not be imported successfully, because {count, plural, one {its} other {their}} registration attempt resulted in an error", "console.containers.device-importer.index.incompleteStatus": "The registration of the following {count, plural, one {end device} other {end devices}} failed:", "console.containers.device-importer.index.noneWarningTitle": "No end device was created", "console.containers.device-importer.index.noneWarningMessage": "None of your specified end devices was imported, because each registration attempt resulted in an error", "console.containers.device-importer.index.processLog": "Process log", - "console.containers.device-importer.index.progress": "Successfully converted {errorCount} of {deviceCount} {deviceCount, plural, one {end device} other {end devices}}", + "console.containers.device-importer.index.progress": "Successfully converted {errorCount, number} of {deviceCount, number} {deviceCount, plural, one {end device} other {end devices}}", "console.containers.device-importer.index.successInfoTitle": "All end devices imported successfully", "console.containers.device-importer.index.successInfoMessage": "All of the specified end devices have been converted and imported successfully", "console.containers.device-importer.index.documentationHint": "Please also see our documentation on Importing End Devices for more information and possible resolutions.", "console.containers.device-importer.index.abortWarningTitle": "Device import aborted", - "console.containers.device-importer.index.abortWarningMessage": "The end device import was aborted and the remaining {count} {count, plural, one {end device} other {end devices}} have not been imported", + "console.containers.device-importer.index.abortWarningMessage": "The end device import was aborted and the remaining {count, number} {count, plural, one {end device} other {end devices}} have not been imported", "console.containers.device-importer.index.largeFileWarningMessage": "Providing files larger than {warningThreshold} can cause issues during the import process. We recommend you to split such files up into multiple smaller files and importing them one by one.", "console.containers.device-onboarding-form.messages.endDeviceType": "End device type", "console.containers.device-onboarding-form.messages.provisioningTitle": "Provisioning information", @@ -652,6 +671,12 @@ "console.containers.lora-cloud-gls-form.index.determineWindowSizeAutomatically": "Determine window size automatically", "console.containers.lora-cloud-gls-form.index.enableMultiFrame": "Enable multiframe", "console.containers.lora-cloud-gls-form.index.automaticMultiFrameDescription": "Determines the count of sent historical messages considered for geolocation based on the first byte of the payload", + "console.containers.network-information-container.index.openSourceInfo": "You are currently using The Things Stack Open Source. More features can be unlocked by using The Things Stack Cloud.", + "console.containers.network-information-container.index.plansButton": "Get started with The things Stack Cloud", + "console.containers.network-information-container.registry-totals.applicationsUsed": "Total applications", + "console.containers.network-information-container.registry-totals.gatewaysUsed": "Total gateways", + "console.containers.network-information-container.registry-totals.registeredUsers": "Registered users", + "console.containers.network-information-container.registry-totals.endDevicesAdded": "Total end devices", "console.containers.organization-form.form.orgDescPlaceholder": "Description for my new organization", "console.containers.organization-form.form.orgDescDescription": "Optional organization description; can also be used to save notes about the organization", "console.containers.organization-form.form.orgIdPlaceholder": "my-new-organization", @@ -682,6 +707,10 @@ "console.containers.webhook-edit.index.updateSuccess": "Webhook updated", "console.containers.webhook-edit.index.deleteSuccess": "Webhook deleted", "console.containers.webhook-edit.index.reactivateSuccess": "Webhook activated", + "console.containers.webhook-edit.webhook-edit.editWebhook": "Edit webhook", + "console.containers.webhook-edit.webhook-edit.updateSuccess": "Webhook updated", + "console.containers.webhook-edit.webhook-edit.deleteSuccess": "Webhook deleted", + "console.containers.webhook-edit.webhook-edit.reactivateSuccess": "Webhook activated", "console.containers.webhook-formats-select.index.title": "Webhook format", "console.containers.webhook-formats-select.index.warning": "Webhook formats unavailable", "console.containers.webhooks-table.index.templateId": "Template ID", @@ -809,9 +838,11 @@ "console.views.admin-packet-broker.messages.gatewayVisibilityInformation": "You can use the checkboxes to control what information of your gateways will be visible. Note that this information will be visible to the public and not only to registered networks.", "console.views.admin-packet-broker.messages.defaultGatewayVisibilitySet": "Default gateway visibility set", "console.views.admin-packet-broker.messages.packetBrokerStatusPage": "Packet Broker Status page", + "console.views.admin-panel-network-information.index.title": "Network information", "console.views.admin-user-management-edit.index.updateSuccess": "User updated", "console.views.admin-user-management-edit.index.deleteSuccess": "User deleted", "console.views.application-add.index.appDescription": "Within applications, you can register and manage end devices and their network data. After setting up your device fleet, use one of our many integration options to pass relevant data to your external services.{break}Learn more in our guide on Adding Applications.", + "console.views.application-data.application-data.appData": "Application data", "console.views.application-data.index.appData": "Application data", "console.views.application-integrations-lora-cloud.index.loraCloudInfoText": "Lora Cloud provides value added APIs that enable simple solutions for common tasks related to LoRaWAN networks and LoRa-based devices. You can setup our LoRaCloud integrations below.", "console.views.application-integrations-lora-cloud.index.officialLoRaCloudDocumentation": "Official LoRa Cloud documentation", @@ -829,18 +860,31 @@ "console.views.application-integrations-mqtt.index.mqttServer": "MQTT server", "console.views.application-integrations-mqtt.index.host": "MQTT server host", "console.views.application-integrations-mqtt.index.connectionInfo": "Connection information", + "console.views.application-integrations-pubsub-edit.application-integrations-pubsub-edit.editPubsub": "Edit Pub/Sub", + "console.views.application-integrations-pubsub-edit.application-integrations-pubsub-edit.updateSuccess": "Pub/Sub updated", + "console.views.application-integrations-pubsub-edit.application-integrations-pubsub-edit.deleteSuccess": "Pub/Sub deleted", "console.views.application-integrations-pubsub-edit.index.editPubsub": "Edit Pub/Sub", "console.views.application-integrations-pubsub-edit.index.updateSuccess": "Pub/Sub updated", "console.views.application-integrations-pubsub-edit.index.deleteSuccess": "Pub/Sub deleted", + "console.views.application-integrations-webhook-add-choose.application-integrations-webhook-add-choose.chooseTemplate": "Choose webhook template", + "console.views.application-integrations-webhook-add-choose.application-integrations-webhook-add-choose.customTileDescription": "Create a custom webhook without template", "console.views.application-integrations-webhook-add-choose.index.chooseTemplate": "Choose webhook template", "console.views.application-integrations-webhook-add-choose.index.customTileDescription": "Create a custom webhook without template", + "console.views.application-integrations-webhook-add-form.application-integrations-webhook-add-form.addCustomWebhook": "Add custom webhook", + "console.views.application-integrations-webhook-add-form.application-integrations-webhook-add-form.addWebhookViaTemplate": "Add webhook for {templateName}", + "console.views.application-integrations-webhook-add-form.application-integrations-webhook-add-form.customWebhook": "Custom webhook", "console.views.application-integrations-webhook-add-form.index.addCustomWebhook": "Add custom webhook", "console.views.application-integrations-webhook-add-form.index.addWebhookViaTemplate": "Add webhook for {templateName}", "console.views.application-integrations-webhook-add-form.index.customWebhook": "Custom webhook", + "console.views.application-integrations-webhook-edit.application-integrations-webhook-edit.editWebhook": "Edit webhook", + "console.views.application-integrations-webhook-edit.application-integrations-webhook-edit.updateSuccess": "Webhook updated", + "console.views.application-integrations-webhook-edit.application-integrations-webhook-edit.deleteSuccess": "Webhook deleted", + "console.views.application-integrations-webhook-edit.application-integrations-webhook-edit.reactivateSuccess": "Webhook activated", "console.views.application-integrations-webhook-edit.index.editWebhook": "Edit webhook", "console.views.application-integrations-webhook-edit.index.updateSuccess": "Webhook updated", "console.views.application-integrations-webhook-edit.index.deleteSuccess": "Webhook deleted", "console.views.application-integrations-webhook-edit.index.reactivateSuccess": "Webhook activated", + "console.views.application-overview.application-overview.failedAccessOtherHostApplication": "The application you attempted to visit is registered on a different cluster and needs to be accessed using its host Console.", "console.views.application-overview.index.failedAccessOtherHostApplication": "The application you attempted to visit is registered on a different cluster and needs to be accessed using its host Console.", "console.views.device-general-settings.application-server-form.index.skip": "Enforce skipping payload crypto", "console.views.device-general-settings.application-server-form.index.include": "Enforce payload crypto", @@ -912,6 +956,11 @@ "console.views.gateway-general-settings.messages.deleteGateway": "Delete gateway", "console.views.gateway-general-settings.messages.modalWarning": "Are you sure you want to delete \"{gtwName}\"? This action cannot be undone and it will not be possible to reuse the gateway ID.", "console.views.gateway-general-settings.messages.disablePacketBrokerForwarding": "Disable forwarding uplink messages received from this gateway to the Packet Broker", + "console.views.gateway-overview.gateway-overview.downloadGlobalConf": "Download global_conf.json", + "console.views.gateway-overview.gateway-overview.globalConf": "Global configuration", + "console.views.gateway-overview.gateway-overview.globalConfFailed": "Failed to download global_conf.json", + "console.views.gateway-overview.gateway-overview.globalConfFailedMessage": "An unknown error occurred and the global_conf.json could not be downloaded", + "console.views.gateway-overview.gateway-overview.globalConfUnavailable": "Unavailable for gateways without frequency plan", "console.views.gateway-overview.index.downloadGlobalConf": "Download global_conf.json", "console.views.gateway-overview.index.globalConf": "Global configuration", "console.views.gateway-overview.index.globalConfFailed": "Failed to download global_conf.json", @@ -919,18 +968,22 @@ "console.views.gateway-overview.index.globalConfUnavailable": "Unavailable for gateways without frequency plan", "console.views.organization-add.index.orgDescription": "Organizations are used to group multiple users and assigning collective rights for them. An organization can then be set as collaborator of applications or gateways. This makes it easy to grant or revoke rights to entities for a group of users.{break} Learn more in our guide on Organization Management.", "console.views.organization-data.index.orgData": "Organization data", + "console.views.organization-data.organization-data.orgData": "Organization data", "console.views.overview.help-link.needHelp": "Need help? Have a look at our {documentationLink} or {supportLink}.", "console.views.overview.help-link.needHelpShort": "Need help? Have a look at our {link}.", "console.views.overview.index.createApplication": "Create an application", "console.views.overview.index.createGateway": "Register a gateway", "console.views.overview.index.gotoApplications": "Go to applications", "console.views.overview.index.gotoGateways": "Go to gateways", + "console.views.overview.index.needHelp": "Need help? Have a look at our {documentationLink} or {supportLink}.", + "console.views.overview.index.needHelpShort": "Need help? Have a look at our {link}.", "console.views.overview.index.welcome": "Welcome to the Console!", "console.views.overview.index.welcomeBack": "Welcome back, {userName}! 👋", "console.views.overview.index.getStarted": "Get started right away by creating an application or registering a gateway.", "console.views.overview.index.continueWorking": "Walk right through to your applications and/or gateways.", "console.views.overview.index.componentStatus": "Component status", "console.views.overview.index.versionInfo": "Version info", + "containers.collaborator-form.collaborator-form.collaboratorIdPlaceholder": "Type to choose a collaborator", "containers.fetch-table.index.errorMessage": "There was an error and the list of {entity, select, applications {applications} organizations {organizations} keys {API keys} collaborators {collaborators} devices {end devices} gateways {gateways} users {users} webhooks {webhooks} other {entities}} could not be displayed", "lib.components.date-time.relative.justNow": "just now", "lib.components.init.initializing": "Initializing…", @@ -1111,6 +1164,7 @@ "lib.shared-messages.addressPlaceholder": "host", "lib.shared-messages.addWebhook": "Add webhook", "lib.shared-messages.admin": "Admin", + "lib.shared-messages.adminPanel": "Admin panel", "lib.shared-messages.all": "All", "lib.shared-messages.allAdmin": "All (Admin)", "lib.shared-messages.altitude": "Altitude", @@ -1164,7 +1218,7 @@ "lib.shared-messages.collaboratorIdPlaceholder": "collaborator-id", "lib.shared-messages.collaboratorWarningSelf": "Changing your own rights could result in loss of access", "lib.shared-messages.collaboratorWarningAdmin": "This user is an administrator that will retain all rights to all entities regardless of collaborator status", - "lib.shared-messages.collaboratorWarningAdminSelf": "As an administrator, you always retain all rights to all entities regardless of collaborator status", + "lib.shared-messages.collaboratorWarningAdminSelf": "As an administrator, you aways retain all rights to all entities regardless of collaborator status", "lib.shared-messages.collaboratorModalWarning": "Are you sure you want to remove {collaboratorId} as a collaborator?", "lib.shared-messages.collaboratorModalWarningSelf": "Are you sure you want to remove yourself as a collaborator? Access to this entity will be lost until someone else adds you as a collaborator again.", "lib.shared-messages.collaboratorRemove": "Collaborator remove",