diff --git a/CHANGELOG.md b/CHANGELOG.md
index a49ccce58f..9e71365dfd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,22 @@ with the exception that this project **does not** follow Semantic Versioning.
For details about compatibility between different releases, see the **Commitments and Releases** section of our README.
+## [Unreleased]
+
+### Added
+
+### Changed
+
+### Deprecated
+
+### Removed
+
+### Fixed
+
+- Resolve scroll jumps when selecting different tabs of a table in the Console.
+
+### Security
+
## [3.28.0] - 2023-10-31
### Added
diff --git a/config/messages.json b/config/messages.json
index 9f89f6d34a..ac9f78406f 100644
--- a/config/messages.json
+++ b/config/messages.json
@@ -3509,6 +3509,42 @@
"file": "shared.go"
}
},
+ "error:pkg/console/internal/events/protocol:message_type": {
+ "translations": {
+ "en": "invalid message type `{type}`"
+ },
+ "description": {
+ "package": "pkg/console/internal/events/protocol",
+ "file": "protocol.go"
+ }
+ },
+ "error:pkg/console/internal/events/subscriptions:already_subscribed": {
+ "translations": {
+ "en": "already subscribed with ID `{id}`"
+ },
+ "description": {
+ "package": "pkg/console/internal/events/subscriptions",
+ "file": "subscriptions.go"
+ }
+ },
+ "error:pkg/console/internal/events/subscriptions:no_identifiers": {
+ "translations": {
+ "en": "no identifiers"
+ },
+ "description": {
+ "package": "pkg/console/internal/events/subscriptions",
+ "file": "subscriptions.go"
+ }
+ },
+ "error:pkg/console/internal/events/subscriptions:not_subscribed": {
+ "translations": {
+ "en": "not subscribed with ID `{id}`"
+ },
+ "description": {
+ "package": "pkg/console/internal/events/subscriptions",
+ "file": "subscriptions.go"
+ }
+ },
"error:pkg/crypto/cryptoservices:no_app_key": {
"translations": {
"en": "no AppKey specified"
@@ -4382,67 +4418,67 @@
"file": "conversion.go"
}
},
- "error:pkg/events/grpc:invalid_regexp": {
+ "error:pkg/events/grpc:no_identifiers": {
"translations": {
- "en": "invalid regexp"
+ "en": "no identifiers"
},
"description": {
"package": "pkg/events/grpc",
"file": "grpc.go"
}
},
- "error:pkg/events/grpc:no_identifiers": {
+ "error:pkg/events/grpc:storage_disabled": {
"translations": {
- "en": "no identifiers"
+ "en": "events storage is not not enabled"
},
"description": {
"package": "pkg/events/grpc",
"file": "grpc.go"
}
},
- "error:pkg/events/grpc:no_matching_events": {
+ "error:pkg/events/redis:channel_closed": {
"translations": {
- "en": "no matching events for regexp `{regexp}`"
+ "en": "channel closed"
},
"description": {
- "package": "pkg/events/grpc",
- "file": "grpc.go"
+ "package": "pkg/events/redis",
+ "file": "redis.go"
}
},
- "error:pkg/events/grpc:storage_disabled": {
+ "error:pkg/events/redis:unknown_encoding": {
"translations": {
- "en": "events storage is not not enabled"
+ "en": "unknown encoding"
},
"description": {
- "package": "pkg/events/grpc",
- "file": "grpc.go"
+ "package": "pkg/events/redis",
+ "file": "codec.go"
}
},
- "error:pkg/events/grpc:unknown_event_name": {
+ "error:pkg/events:invalid_regexp": {
"translations": {
- "en": "unknown event `{name}`"
+ "en": "invalid regexp"
},
"description": {
- "package": "pkg/events/grpc",
- "file": "grpc.go"
+ "package": "pkg/events",
+ "file": "pattern.go"
}
},
- "error:pkg/events/redis:channel_closed": {
+ "error:pkg/events:no_matching_events": {
"translations": {
- "en": "channel closed"
+ "en": "no matching events for regexp `{regexp}`"
},
"description": {
- "package": "pkg/events/redis",
- "file": "redis.go"
+ "package": "pkg/events",
+ "file": "pattern.go"
}
},
- "error:pkg/events/redis:unknown_encoding": {
+ "error:pkg/events:unknown_event_name": {
"translations": {
- "en": "unknown encoding"
+ "en": "unknown event `{name}`"
},
"description": {
- "package": "pkg/events/redis",
- "file": "codec.go"
+ "package": "pkg/events",
+ "file": "pattern.go"
}
},
"error:pkg/fetch:fetch_file": {
diff --git a/cypress/e2e/console/admin-panel/packet-broker/networks.spec.js b/cypress/e2e/console/admin-panel/packet-broker/networks.spec.js
index db702ac160..e3a83f2526 100644
--- a/cypress/e2e/console/admin-panel/packet-broker/networks.spec.js
+++ b/cypress/e2e/console/admin-panel/packet-broker/networks.spec.js
@@ -45,7 +45,12 @@ describe('Packet Broker networks', () => {
cy.intercept('/api/v3/pba/home-networks/policies*', {
fixture: 'console/packet-broker/policies-home-network.json',
})
- cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker/networks`)
+ cy.visit(
+ `${Cypress.config(
+ 'consoleRootPath',
+ )}/admin-panel/packet-broker/routing-configuration/networks`,
+ )
+ cy.findByLabelText('Use custom routing policies').check()
const { networks } = this.networks
const networksFiltered = networks.filter(
@@ -73,7 +78,11 @@ describe('Packet Broker networks', () => {
n => n.forwarder_id.net_id === 19 && n.forwarder_id.tenant_id === 'johan',
)
- cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker/networks/19/johan`)
+ cy.visit(
+ `${Cypress.config(
+ 'consoleRootPath',
+ )}/admin-panel/packet-broker/routing-configuration/networks/19/johan`,
+ )
cy.findAllByText(`${network.id.net_id.toString(16).padStart(6, '0')}/${network.id.tenant_id}`)
cy.findByText(
diff --git a/cypress/e2e/console/admin-panel/packet-broker/registration.spec.js b/cypress/e2e/console/admin-panel/packet-broker/registration.spec.js
index 5f4afc5366..4bfd68d939 100644
--- a/cypress/e2e/console/admin-panel/packet-broker/registration.spec.js
+++ b/cypress/e2e/console/admin-panel/packet-broker/registration.spec.js
@@ -61,15 +61,15 @@ describe('Packet Broker registration', () => {
cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker`)
cy.findByText('Packet Broker', { selector: 'h1' }).should('be.visible')
- cy.findByText(/Packet Broker can be used to exchange traffic/).should('be.visible')
- cy.findByText('Packet Broker documentation', { selector: 'a' }).should('be.visible')
+ cy.findByText(/Packet Broker is a service by The Things Industries/).should('be.visible')
+ cy.findByText('Packet Broker', { selector: 'a' }).should('be.visible')
cy.findByText('Packet Broker website', { selector: 'a' }).should('be.visible')
- cy.findByText('Register network', { selector: 'span' }).should('be.visible')
+ cy.findByText('Enable Packet Broker', { selector: 'span' }).should('be.visible')
cy.findByTestId('switch')
.should('be.visible')
.and('not.be.checked')
.and('not.have.attr', 'disabled')
- cy.findByText(/To enable peering/).should('be.visible')
+ cy.findByText(/Enabling will allow/).should('be.visible')
cy.findByText('Default routing policy').should('not.exist')
cy.findByText('Networks').should('not.exist')
@@ -89,18 +89,20 @@ describe('Packet Broker registration', () => {
cy.loginConsole({ user_id: 'admin', password: 'admin' })
cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker`)
- cy.findByText('Register network').click()
- cy.findByText('Register network').next().findByTestId('switch').should('be.checked')
- cy.findByText('List network publicly')
+ cy.findByText('Enable Packet Broker').click()
+ cy.findByText('Enable Packet Broker').next().findByTestId('switch').should('be.checked')
+ cy.findByText('List my network in Packet Broker publicly')
.should('be.visible')
.next()
.findByTestId('switch')
.should('be.checked')
- cy.findByTestId('feature-info-forwarder-enabled').should('be.visible')
- cy.findByTestId('feature-info-home-network-enabled').should('be.visible')
- cy.findByTestId('tabs').findByText('Default routing policy').should('be.visible')
- cy.findByTestId('tabs').findByText('Networks').should('be.visible')
- cy.findByLabelText('Do not use a default routing policy for this network').should('be.checked')
+ cy.findByLabelText('Forward traffic to all networks registered in Packet Broker').should(
+ 'exist',
+ )
+ cy.findByLabelText(
+ 'Forward traffic to The Things Stack Sandbox (community network) only',
+ ).should('exist')
+ cy.findByLabelText('Use custom routing policies').should('exist')
cy.findByTestId('error-notification').should('not.exist')
})
diff --git a/cypress/e2e/console/admin-panel/packet-broker/routing-policies.spec.js b/cypress/e2e/console/admin-panel/packet-broker/routing-policies.spec.js
index 9b9f519f12..be0fee18a5 100644
--- a/cypress/e2e/console/admin-panel/packet-broker/routing-policies.spec.js
+++ b/cypress/e2e/console/admin-panel/packet-broker/routing-policies.spec.js
@@ -32,12 +32,63 @@ describe('Packet Broker routing policies', () => {
cy.loginConsole({ user_id: 'admin', password: 'admin' })
})
- it('succeeds setting a default routing policy', () => {
+ it('succeeds setting a "traffic to all networks" routing configuration', () => {
cy.intercept('GET', '/api/v3/pba/home-networks/policies/default', { statusCode: 404 })
cy.intercept('PUT', '/api/v3/pba/home-networks/policies/default', {})
+ cy.intercept('DELETE', '/api/v3/pba/home-networks/policies/19', {})
+ cy.intercept('DELETE', '/api/v3/pba/home-networks/policies/19/johan', {})
+ cy.intercept('/api/v3/pba/networks*', { fixture: 'console/packet-broker/networks.json' })
+ cy.intercept('/api/v3/pba/home-networks/policies*', {
+ fixture: 'console/packet-broker/policies-home-network.json',
+ })
cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker`)
- cy.findByLabelText('Use default routing policy for this network').check()
+ cy.findByLabelText('Forward traffic to all networks registered in Packet Broker').check()
+ cy.findByRole('button', { name: 'Save routing configuration' }).click()
+
+ cy.findByTestId('error-notification').should('not.exist')
+ cy.findByTestId('toast-notification')
+ .should('be.visible')
+ .findByText('Default routing configuration set')
+ .should('be.visible')
+ })
+
+ it('succeeds setting a "only ttn" routing configuration', () => {
+ cy.intercept('GET', '/api/v3/pba/home-networks/policies/default', { statusCode: 404 })
+ cy.intercept('/api/v3/pba/networks*', { fixture: 'console/packet-broker/networks.json' })
+ cy.intercept('/api/v3/pba/home-networks/policies*', {
+ fixture: 'console/packet-broker/policies-home-network.json',
+ })
+ cy.intercept('DELETE', '/api/v3/pba/home-networks/policies/default', {})
+ cy.intercept('DELETE', '/api/v3/pba/home-networks/policies/19', {})
+ cy.intercept('DELETE', '/api/v3/pba/home-networks/policies/19/johan', {})
+ cy.intercept('PUT', '/api/v3/pba/home-networks/policies/19/ttn', {})
+ cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker`)
+
+ cy.findByLabelText(
+ 'Forward traffic to The Things Stack Sandbox (community network) only',
+ ).check()
+ cy.findByRole('button', { name: 'Save routing configuration' }).click()
+
+ cy.findByTestId('error-notification').should('not.exist')
+ cy.findByTestId('toast-notification')
+ .should('be.visible')
+ .findByText('Default routing configuration set')
+ .should('be.visible')
+ })
+
+ it('succeeds setting a custom routing configuration with a default routing policy', () => {
+ cy.intercept('GET', '/api/v3/pba/home-networks/policies/default', {
+ fixture: 'console/packet-broker/default-policy.json',
+ })
+ cy.intercept('PUT', '/api/v3/pba/home-networks/policies/default', {})
+ cy.intercept('/api/v3/pba/networks*', { fixture: 'console/packet-broker/networks.json' })
+ cy.intercept('/api/v3/pba/home-networks/policies*', {
+ fixture: 'console/packet-broker/policies-home-network.json',
+ })
+ cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker`)
+
+ cy.findByLabelText('Use custom routing policies').check()
// Check routing policy form checkboxes.
cy.findByText('Uplink')
@@ -56,30 +107,35 @@ describe('Packet Broker routing policies', () => {
cy.findByLabelText('MAC data').check()
cy.findByLabelText('Application data').check()
})
- cy.findByRole('button', { name: 'Save default policy' }).click()
+ cy.findByRole('button', { name: 'Save routing configuration' }).click()
cy.findByTestId('error-notification').should('not.exist')
cy.findByTestId('toast-notification')
.should('be.visible')
- .findByText('Default routing policy set')
+ .findByText('Default routing configuration set')
.should('be.visible')
})
it('succeeds unsetting a default routing policy', () => {
+ cy.intercept('PUT', '/api/v3/pba/home-networks/policies/default', {})
+ cy.intercept('/api/v3/pba/networks*', { fixture: 'console/packet-broker/networks.json' })
+ cy.intercept('/api/v3/pba/home-networks/policies*', {
+ fixture: 'console/packet-broker/policies-home-network.json',
+ })
cy.intercept('GET', '/api/v3/pba/home-networks/policies/default', {
- fixture: 'console/packet-broker/default-policy.json',
+ fixture: 'console/packet-broker/default-custom-policy.json',
})
cy.intercept('DELETE', '/api/v3/pba/home-networks/policies/default', {})
cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker`)
cy.findByLabelText('Do not use a default routing policy for this network').check()
- cy.findByRole('button', { name: 'Save default policy' }).click()
+ cy.findByRole('button', { name: 'Save routing configuration' }).click()
cy.findByTestId('error-notification').should('not.exist')
cy.findByTestId('toast-notification')
.should('be.visible')
- .findByText('Default routing policy set')
+ .findByText('Default routing configuration set')
.should('be.visible')
})
@@ -93,7 +149,11 @@ describe('Packet Broker routing policies', () => {
})
cy.intercept('PUT', '/api/v3/pba/home-networks/policies/19', {})
- cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker/networks/19`)
+ cy.visit(
+ `${Cypress.config(
+ 'consoleRootPath',
+ )}/admin-panel/packet-broker/routing-configuration/networks/19`,
+ )
// Check routing policy form checkboxes.
cy.findByLabelText('Use network specific routing policy').check()
@@ -138,7 +198,11 @@ describe('Packet Broker routing policies', () => {
})
cy.intercept('DELETE', '/api/v3/pba/home-networks/policies/19', {})
- cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker/networks/19`)
+ cy.visit(
+ `${Cypress.config(
+ 'consoleRootPath',
+ )}/admin-panel/packet-broker/routing-configuration/networks/19`,
+ )
cy.findByLabelText('Do not use a routing policy for this network').check()
cy.findByRole('button', { name: 'Save routing policy' }).click()
@@ -161,7 +225,11 @@ describe('Packet Broker routing policies', () => {
})
cy.intercept('DELETE', '/api/v3/pba/home-networks/policies/19', {})
- cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker/networks/19`)
+ cy.visit(
+ `${Cypress.config(
+ 'consoleRootPath',
+ )}/admin-panel/packet-broker/routing-configuration/networks/19`,
+ )
cy.findByLabelText('Use default routing policy for this network').check()
cy.findByRole('button', { name: 'Save routing policy' }).click()
diff --git a/cypress/fixtures/console/packet-broker/default-custom-policy.json b/cypress/fixtures/console/packet-broker/default-custom-policy.json
new file mode 100644
index 0000000000..edd8728179
--- /dev/null
+++ b/cypress/fixtures/console/packet-broker/default-custom-policy.json
@@ -0,0 +1,11 @@
+{
+ "updated_at": "2021-06-21T12:09:26.810087Z",
+ "uplink": {
+ "join_request": true,
+ "mac_data": false,
+ "application_data": true,
+ "signal_quality": false,
+ "localization": false
+ },
+ "downlink": { "join_accept": true, "mac_data": false, "application_data": true }
+}
diff --git a/go.mod b/go.mod
index 20ab02d6a5..5ad3ab087a 100644
--- a/go.mod
+++ b/go.mod
@@ -106,11 +106,12 @@ require (
google.golang.org/genproto v0.0.0-20230911183012-2d3300fd4832
google.golang.org/genproto/googleapis/api v0.0.0-20230911183012-2d3300fd4832
google.golang.org/genproto/googleapis/rpc v0.0.0-20230911183012-2d3300fd4832
- google.golang.org/grpc v1.58.2
+ google.golang.org/grpc v1.58.3
google.golang.org/protobuf v1.31.0
gopkg.in/mail.v2 v2.3.1
gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v2 v2.4.0
+ nhooyr.io/websocket v1.8.10
)
require (
diff --git a/go.sum b/go.sum
index fde42d7502..259f3ae6ca 100644
--- a/go.sum
+++ b/go.sum
@@ -1194,8 +1194,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
-google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I=
-google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
+google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
+google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/grpc/examples v0.0.0-20210424002626-9572fd6faeae/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1254,6 +1254,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
+nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
+nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/pkg/auth/rights/auth_info.go b/pkg/auth/rights/auth_info.go
index f6b59c71ab..1a7158ffd3 100644
--- a/pkg/auth/rights/auth_info.go
+++ b/pkg/auth/rights/auth_info.go
@@ -51,16 +51,14 @@ func AuthInfo(ctx context.Context) (authInfo *ttnpb.AuthInfoResponse, err error)
var errUnauthenticated = errors.DefineUnauthenticated("unauthenticated", "unauthenticated")
-// RequireAuthentication confirms if the authentication information within a context contains any rights, if so,
+// RequireAuthenticated confirms if the authentication information within a context contains any rights, if so,
// the request is considered to be authenticated.
-func RequireAuthentication(ctx context.Context) error {
- log.FromContext(ctx).Debug("Authenticate request")
+func RequireAuthenticated(ctx context.Context) error {
authInfo, err := AuthInfo(ctx)
if err != nil {
log.FromContext(ctx).WithError(err).Debug("Failed to validate authentication information")
return errUnauthenticated.WithCause(err)
}
-
if authInfo.GetAccessMethod() == nil && len(authInfo.GetUniversalRights().GetRights()) == 0 {
return errUnauthenticated.New()
}
diff --git a/pkg/console/console.go b/pkg/console/console.go
index d012123d35..d1d26e654e 100644
--- a/pkg/console/console.go
+++ b/pkg/console/console.go
@@ -23,6 +23,7 @@ import (
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"go.thethings.network/lorawan-stack/v3/pkg/component"
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events"
"go.thethings.network/lorawan-stack/v3/pkg/web"
"go.thethings.network/lorawan-stack/v3/pkg/web/oauthclient"
"go.thethings.network/lorawan-stack/v3/pkg/webhandlers"
@@ -58,6 +59,7 @@ func New(c *component.Component, config Config) (*Console, error) {
}
c.RegisterWeb(console)
+ c.RegisterWeb(events.New(c))
return console, nil
}
diff --git a/pkg/console/internal/events/events.go b/pkg/console/internal/events/events.go
new file mode 100644
index 0000000000..caae64a0a4
--- /dev/null
+++ b/pkg/console/internal/events/events.go
@@ -0,0 +1,135 @@
+// 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.
+
+// Package events contains the internal events APi for the Console.
+package events
+
+import (
+ "context"
+ "net/http"
+ "sync"
+
+ "github.com/gorilla/mux"
+ "go.thethings.network/lorawan-stack/v3/pkg/auth/rights"
+ "go.thethings.network/lorawan-stack/v3/pkg/config"
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/eventsmux"
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/subscriptions"
+ "go.thethings.network/lorawan-stack/v3/pkg/events"
+ "go.thethings.network/lorawan-stack/v3/pkg/log"
+ "go.thethings.network/lorawan-stack/v3/pkg/ratelimit"
+ "go.thethings.network/lorawan-stack/v3/pkg/task"
+ "go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
+ "go.thethings.network/lorawan-stack/v3/pkg/web"
+ "go.thethings.network/lorawan-stack/v3/pkg/webhandlers"
+ "go.thethings.network/lorawan-stack/v3/pkg/webmiddleware"
+ "nhooyr.io/websocket"
+)
+
+// Component is the interface of the component to the events API handler.
+type Component interface {
+ task.Starter
+ Context() context.Context
+ RateLimiter() ratelimit.Interface
+ GetBaseConfig(context.Context) config.ServiceBase
+}
+
+type eventsHandler struct {
+ component Component
+ subscriber events.Subscriber
+ definedNames map[string]struct{}
+}
+
+var _ web.Registerer = (*eventsHandler)(nil)
+
+func (h *eventsHandler) RegisterRoutes(server *web.Server) {
+ router := server.APIRouter().PathPrefix(ttnpb.HTTPAPIPrefix + "/console/internal/events/").Subrouter()
+ router.Use(
+ mux.MiddlewareFunc(webmiddleware.Namespace("console/internal/events")),
+ ratelimit.HTTPMiddleware(h.component.RateLimiter(), "http:console:internal:events"),
+ mux.MiddlewareFunc(webmiddleware.Metadata("Authorization")),
+ )
+ router.Path("/").HandlerFunc(h.handleEvents).Methods(http.MethodGet)
+}
+
+func (h *eventsHandler) handleEvents(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ logger := log.FromContext(ctx)
+
+ if err := rights.RequireAuthenticated(ctx); err != nil {
+ webhandlers.Error(w, r, err)
+ return
+ }
+
+ conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
+ InsecureSkipVerify: true, // CORS is not enabled for APIs.
+ CompressionMode: websocket.CompressionContextTakeover,
+ })
+ if err != nil {
+ logger.WithError(err).Debug("Failed to accept WebSocket")
+ return
+ }
+ defer conn.Close(websocket.StatusNormalClosure, "main task closed")
+
+ ctx, cancel := context.WithCancelCause(ctx)
+ defer cancel(nil)
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+
+ m := eventsmux.New(func(ctx context.Context, cancel func(error)) subscriptions.Interface {
+ return subscriptions.New(ctx, cancel, h.subscriber, h.definedNames, h.component)
+ })
+ for name, f := range map[string]func(context.Context) error{
+ "console_events_mux": makeMuxTask(m, cancel),
+ "console_events_read": makeReadTask(conn, m, cancel),
+ "console_events_write": makeWriteTask(conn, m, cancel),
+ } {
+ wg.Add(1)
+ h.component.StartTask(&task.Config{
+ Context: ctx,
+ ID: name,
+ Func: f,
+ Done: wg.Done,
+ Restart: task.RestartNever,
+ Backoff: task.DefaultBackoffConfig,
+ })
+ }
+}
+
+// Option configures the events API handler.
+type Option func(*eventsHandler)
+
+// WithSubscriber configures the Subscriber to use for events.
+func WithSubscriber(subscriber events.Subscriber) Option {
+ return func(h *eventsHandler) {
+ h.subscriber = subscriber
+ }
+}
+
+// New returns an events API handler for the Console.
+func New(c Component, opts ...Option) web.Registerer {
+ definedNames := make(map[string]struct{})
+ for _, def := range events.All().Definitions() {
+ definedNames[def.Name()] = struct{}{}
+ }
+ h := &eventsHandler{
+ component: c,
+ subscriber: events.DefaultPubSub(),
+ definedNames: definedNames,
+ }
+ for _, opt := range opts {
+ opt(h)
+ }
+ return h
+}
diff --git a/pkg/console/internal/events/eventsmux/mux.go b/pkg/console/internal/events/eventsmux/mux.go
new file mode 100644
index 0000000000..e0874f9c51
--- /dev/null
+++ b/pkg/console/internal/events/eventsmux/mux.go
@@ -0,0 +1,106 @@
+// 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.
+
+// Package eventsmux implements the events mux.
+package eventsmux
+
+import (
+ "context"
+
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/protocol"
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/subscriptions"
+ "go.thethings.network/lorawan-stack/v3/pkg/events"
+ "go.thethings.network/lorawan-stack/v3/pkg/log"
+)
+
+// Interface is the interface for the events mux.
+type Interface interface {
+ // Requests returns the channel for requests.
+ Requests() chan<- protocol.Request
+ // Responses returns the channel for responses.
+ Responses() <-chan protocol.Response
+
+ // Run runs the events mux.
+ Run(context.Context) error
+}
+
+type mux struct {
+ createSubs func(context.Context, func(error)) subscriptions.Interface
+
+ requestCh chan protocol.Request
+ responseCh chan protocol.Response
+}
+
+// Requests implements Interface.
+func (m *mux) Requests() chan<- protocol.Request {
+ return m.requestCh
+}
+
+// Responses implements Interface.
+func (m *mux) Responses() <-chan protocol.Response {
+ return m.responseCh
+}
+
+// Run implements Interface.
+func (m *mux) Run(ctx context.Context) (err error) {
+ ctx, cancel := context.WithCancelCause(ctx)
+ defer func() { cancel(err) }()
+ subs := m.createSubs(ctx, cancel)
+ defer subs.Close()
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case req := <-m.requestCh:
+ var resp protocol.Response
+ switch req := req.(type) {
+ case *protocol.SubscribeRequest:
+ resp = req.Response(subs.Subscribe(req.ID, req.Identifiers, req.After, req.Tail, req.Names))
+ case *protocol.UnsubscribeRequest:
+ resp = req.Response(subs.Unsubscribe(req.ID))
+ default:
+ panic("unreachable")
+ }
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case m.responseCh <- resp:
+ }
+ case subEvt := <-subs.SubscriptionEvents():
+ evtPB, err := events.Proto(subEvt.Event)
+ if err != nil {
+ log.FromContext(ctx).WithError(err).Warn("Failed to convert event to proto")
+ continue
+ }
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case m.responseCh <- &protocol.PublishResponse{
+ ID: subEvt.ID,
+ Event: evtPB,
+ }:
+ }
+ }
+ }
+}
+
+// New returns a new Interface.
+func New(createSubs func(context.Context, func(error)) subscriptions.Interface) Interface {
+ return &mux{
+ createSubs: createSubs,
+
+ requestCh: make(chan protocol.Request, 1),
+ responseCh: make(chan protocol.Response, 1),
+ }
+}
diff --git a/pkg/console/internal/events/eventsmux/mux_test.go b/pkg/console/internal/events/eventsmux/mux_test.go
new file mode 100644
index 0000000000..de220f52fa
--- /dev/null
+++ b/pkg/console/internal/events/eventsmux/mux_test.go
@@ -0,0 +1,315 @@
+// 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.
+
+package eventsmux_test
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "go.thethings.network/lorawan-stack/v3/pkg/auth/rights"
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/eventsmux"
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/protocol"
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/subscriptions"
+ "go.thethings.network/lorawan-stack/v3/pkg/events"
+ "go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
+ "go.thethings.network/lorawan-stack/v3/pkg/unique"
+ "go.thethings.network/lorawan-stack/v3/pkg/util/test"
+ "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/status"
+)
+
+type subscribeRequest struct {
+ ID uint64
+ Identifiers []*ttnpb.EntityIdentifiers
+ After *time.Time
+ Tail uint32
+ Names []string
+
+ Response chan<- error
+}
+
+type unsubscribeRequest struct {
+ ID uint64
+
+ Response chan<- error
+}
+
+type mockSubscriptions struct {
+ ctx context.Context
+ subReqs chan subscribeRequest
+ unsubReqs chan unsubscribeRequest
+ evsCh chan *subscriptions.SubscriptionEvent
+}
+
+// Subscribe implements subscriptions.Interface.
+func (m *mockSubscriptions) Subscribe(
+ id uint64, identifiers []*ttnpb.EntityIdentifiers, after *time.Time, tail uint32, names []string,
+) error {
+ ch := make(chan error, 1)
+ select {
+ case <-m.ctx.Done():
+ return m.ctx.Err()
+ case m.subReqs <- subscribeRequest{
+ ID: id,
+ Identifiers: identifiers,
+ After: after,
+ Tail: tail,
+ Names: names,
+
+ Response: ch,
+ }:
+ select {
+ case <-m.ctx.Done():
+ return m.ctx.Err()
+ case err := <-ch:
+ return err
+ }
+ }
+}
+
+// Unsubscribe implements subscriptions.Interface.
+func (m *mockSubscriptions) Unsubscribe(id uint64) error {
+ ch := make(chan error, 1)
+ select {
+ case <-m.ctx.Done():
+ return m.ctx.Err()
+ case m.unsubReqs <- unsubscribeRequest{
+ ID: id,
+
+ Response: ch,
+ }:
+ select {
+ case <-m.ctx.Done():
+ return m.ctx.Err()
+ case err := <-ch:
+ return err
+ }
+ }
+}
+
+// SubscriptionEvents implements subscriptions.Interface.
+func (m *mockSubscriptions) SubscriptionEvents() <-chan *subscriptions.SubscriptionEvent {
+ return m.evsCh
+}
+
+// Close implements subscriptions.Interface.
+func (*mockSubscriptions) Close() error { return nil }
+
+var _ subscriptions.Interface = (*mockSubscriptions)(nil)
+
+func TestMux(t *testing.T) { // nolint:gocyclo
+ t.Parallel()
+
+ a, ctx := test.New(t)
+
+ appIDs := &ttnpb.ApplicationIdentifiers{
+ ApplicationId: "foo",
+ }
+ ctx = rights.NewContext(ctx, &rights.Rights{
+ ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{
+ unique.ID(ctx, appIDs): ttnpb.RightsFrom(ttnpb.Right_RIGHT_ALL),
+ }),
+ })
+
+ subs := &mockSubscriptions{
+ ctx: ctx,
+ subReqs: make(chan subscribeRequest, 1),
+ unsubReqs: make(chan unsubscribeRequest, 1),
+ evsCh: make(chan *subscriptions.SubscriptionEvent, 1),
+ }
+ m := eventsmux.New(func(context.Context, func(error)) subscriptions.Interface { return subs })
+
+ go m.Run(ctx) // nolint:errcheck
+
+ now := time.Now()
+ select {
+ case <-ctx.Done():
+ return
+ case m.Requests() <- &protocol.SubscribeRequest{
+ ID: 42,
+ Identifiers: []*ttnpb.EntityIdentifiers{
+ appIDs.GetEntityIdentifiers(),
+ },
+ After: &now,
+ Tail: 1,
+ Names: []string{"foo"},
+ }:
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case req := <-subs.subReqs:
+ a.So(req, should.Resemble, subscribeRequest{
+ ID: 42,
+ Identifiers: []*ttnpb.EntityIdentifiers{
+ appIDs.GetEntityIdentifiers(),
+ },
+ After: &now,
+ Tail: 1,
+ Names: []string{"foo"},
+
+ Response: req.Response,
+ })
+ select {
+ case <-ctx.Done():
+ return
+ case req.Response <- nil:
+ }
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case resp := <-m.Responses():
+ a.So(resp, should.Resemble, &protocol.SubscribeResponse{
+ ID: 42,
+ })
+ }
+
+ errAlreadySubscribed := errors.New("already subscribed")
+ select {
+ case <-ctx.Done():
+ return
+ case m.Requests() <- &protocol.SubscribeRequest{
+ ID: 42,
+ Identifiers: []*ttnpb.EntityIdentifiers{
+ appIDs.GetEntityIdentifiers(),
+ },
+ After: &now,
+ Tail: 1,
+ Names: []string{"foo"},
+ }:
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case req := <-subs.subReqs:
+ a.So(req, should.Resemble, subscribeRequest{
+ ID: 42,
+ Identifiers: []*ttnpb.EntityIdentifiers{
+ appIDs.GetEntityIdentifiers(),
+ },
+ After: &now,
+ Tail: 1,
+ Names: []string{"foo"},
+
+ Response: req.Response,
+ })
+ select {
+ case <-ctx.Done():
+ return
+ case req.Response <- errAlreadySubscribed:
+ }
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case resp := <-m.Responses():
+ a.So(resp, should.Resemble, &protocol.ErrorResponse{
+ ID: 42,
+ Error: status.New(codes.Unknown, "already subscribed"),
+ })
+ }
+
+ ev := events.New(
+ ctx,
+ "test.evt",
+ "test event",
+ events.WithIdentifiers(appIDs),
+ )
+ select {
+ case <-ctx.Done():
+ return
+ case subs.evsCh <- &subscriptions.SubscriptionEvent{
+ ID: 42,
+ Event: ev,
+ }:
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case resp := <-m.Responses():
+ a.So(resp, should.Resemble, &protocol.PublishResponse{
+ ID: 42,
+ Event: test.Must(events.Proto(ev)),
+ })
+ }
+
+ select {
+ case <-ctx.Done():
+ return
+ case m.Requests() <- &protocol.UnsubscribeRequest{
+ ID: 42,
+ }:
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case req := <-subs.unsubReqs:
+ a.So(req, should.Resemble, unsubscribeRequest{
+ ID: 42,
+
+ Response: req.Response,
+ })
+ select {
+ case <-ctx.Done():
+ return
+ case req.Response <- nil:
+ }
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case resp := <-m.Responses():
+ a.So(resp, should.Resemble, &protocol.UnsubscribeResponse{
+ ID: 42,
+ })
+ }
+
+ errNotSubscribed := errors.New("not subscribed")
+ select {
+ case <-ctx.Done():
+ return
+ case m.Requests() <- &protocol.UnsubscribeRequest{
+ ID: 42,
+ }:
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case req := <-subs.unsubReqs:
+ a.So(req, should.Resemble, unsubscribeRequest{
+ ID: 42,
+
+ Response: req.Response,
+ })
+ select {
+ case <-ctx.Done():
+ return
+ case req.Response <- errNotSubscribed:
+ }
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case resp := <-m.Responses():
+ a.So(resp, should.Resemble, &protocol.ErrorResponse{
+ ID: 42,
+ Error: status.New(codes.Unknown, "not subscribed"),
+ })
+ }
+}
diff --git a/pkg/console/internal/events/protocol/PROTOCOL.md b/pkg/console/internal/events/protocol/PROTOCOL.md
new file mode 100644
index 0000000000..168dc5980c
--- /dev/null
+++ b/pkg/console/internal/events/protocol/PROTOCOL.md
@@ -0,0 +1,191 @@
+### Internal Events API
+
+The Console internal events API is designed as an alternative to the `Events.Stream` gRPC API for event stream interactions. It allows multiple subscriptions to be multiplexed over a singular [WebSocket](https://en.wikipedia.org/wiki/WebSocket) connection.
+
+### Reasoning
+
+The `Events.Stream` gRPC API is available to HTTP clients via [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway). While translated to HTTP, it is visible as a long-polling request whose response body will contain the events as a series of JSON objects.
+
+This approach is efficient in the context of [HTTP/2](https://en.wikipedia.org/wiki/HTTP/2) which supports multiplexing multiple requests over a singular TCP connection.
+
+Unfortunately the connection between a browser and The Things Stack is susceptible to proxies. Corporate environments are generally equipped with such proxies, and in their presence the connections are downgraded to HTTP/1.1 semantics.
+
+In HTTP/1.1 connections can be used for a singular request at a time - it is not possible to multiplex the requests over a singular connection, and only [keep-alive](https://en.wikipedia.org/wiki/HTTP_persistent_connection) connections are available.
+
+This is problematic as browsers have builtin limits for the number of concurrent connections that singular windows may use. This leads to hard to debug issues which are hardly reproducible.
+
+But, there is one silver lining - the connection limit _does not apply to WebSocket connections_. The internal events API is designed to deal with this limitation while providing an experience similar to the original `Events.Stream` gRPC API.
+
+### Endpoint
+
+The endpoint for the internal events API is `/api/v3/console/internal/events/`. Note that the trailing slash is not optional.
+
+### Semantics
+
+The protocol is [full-duplex](https://en.wikipedia.org/wiki/Duplex_(telecommunications)#Full_duplex) - the client side and server side may transmit messages at any time without waiting for a response from the other party.
+
+The protocol is centered around subscriptions. Subscriptions are identified by an unsigned numerical ID, which is selected by the client.
+
+A subscription is initiated by the client via a subscription request, which the server confirms either with a subscription response or an error response.
+
+Following a successful subscription, the server may send at any time publication responses containing the subscription identifier and an event. The subscription identifier can be used on the client side in order to route the event to the appropriate component or view.
+
+A subscription can be terminated via an unsubscribe request, which the server confirms either with an unsubscribe response or an error response.
+
+The client can expect that no publication responses will follow an unsubscribe response, but it is recommended that subscription identifiers are not recycled within the same session.
+
+Error responses can be expected when the request contents are invalid (lack of identifiers, or invalid identifiers), or the caller is not authorized to subscribe to the provided identifiers. It is also invalid to request a subscription with the same identifier as an existing subscription, or to unsubscribe using an identifier which is not subscribed.
+
+Error response are provided as a debugging facility, and the errors are generally not fixable by the Console user.
+
+A special case exists for situations in which the caller is no longer authorized to receive any events associated with the provided identifiers _after_ the subscription response has been sent. This can happen if the caller token has expired or the rights have been revoked while the stream is ongoing. In such situations the server will terminate the connection explicitly.
+
+### Authentication and Authorization
+
+The authentication for the internal API is similar to other APIs available in The Things Stack. Given a `Bearer` token `t`, the `Authorization` header should contain the value `Bearer t`.
+
+Upon connecting, no authorization will take place - the endpoint only will check that the provided token is valid (i.e. exists and it is not expired).
+
+### Message Format
+
+Both requests and responses sent over the WebSocket connection are JSON encoded. All messages are JSON objects and are required to contain at least the following two fields:
+
+- `type`: a string whose value must be either `subscribe`, `unsubscribe`, `publish` or `error`.
+- `id`: an unsigned integer which identifies the underlying subscription being served.
+
+Each of the following subsections describes an individual message and the message direction (client to server or server to client).
+
+#### `SubscribeRequest` [C -> S]
+
+- `type`: `subscribe`
+- `id`: the subscription identifier
+- `identifiers`, `tail`, `after`, `names`: semantically the same fields as those of the `StreamEventsRequest` Protobuf message.
+
+Example:
+
+```json
+{
+ "type": "subscribe",
+ "id": 1,
+ "tail": 10,
+ "identifiers": [
+ {
+ "application_ids": {
+ "application_id": "app1"
+ }
+ }
+ ]
+}
+```
+
+#### `SubscribeResponse` [S -> C]
+
+- `type`: `subscribe`
+- `id`: the subscription identifier
+
+Example:
+
+```json
+{
+ "type": "subscribe",
+ "id": 1
+}
+```
+
+#### `UnsubscribeRequest` [C -> S]
+
+- `type`: `unsubscribe`
+- `id`: the subscription identifier
+
+Example:
+
+```json
+{
+ "type": "unsubscribe",
+ "id": 1
+}
+```
+
+#### `UnsubscribeResponse` [S -> C]
+
+- `type`: `unsubscribe`
+- `id`: the subscription identifier
+
+Example:
+
+```json
+{
+ "type": "unsubscribe",
+ "id": 1
+}
+```
+
+#### `PublishResponse` [S -> C]
+
+- `type`: `publish`
+- `id`: the subscription identifier
+- `event`: an `Event` Protobuf message encoded as a JSON object
+
+Example:
+
+```json
+{
+ "type": "publish",
+ "id": 1,
+ "event": {
+ "name": "as.up.data.forward",
+ "time": "2023-10-26T16:27:14.103854Z",
+ "identifiers": [
+ {
+ "device_ids": {
+ "device_id": "eui-0000000000000003",
+ "application_ids": {
+ "application_id": "app1"
+ }
+ }
+ }
+ ],
+ "context": {
+ "tenant-id": "Cgl0aGV0aGluZ3M="
+ },
+ "visibility": {
+ "rights": [
+ "RIGHT_APPLICATION_TRAFFIC_READ"
+ ]
+ },
+ "unique_id": "01HDPCZDSQ358JMHD4SC2BQAB8"
+ }
+}
+```
+
+#### ErrorResponse [S -> C]
+
+- `type`: `error`
+- `id`: the subscription identifier
+- `error`: a `Status` Protobuf message encoded as a JSON object
+
+Example:
+
+```json
+{
+ "type": "error",
+ "id": 1,
+ "error": {
+ "code": 6,
+ "message": "error:pkg/console/internal/events/subscriptions:already_subscribed (already subscribed with ID `1`)",
+ "details": [
+ {
+ "@type": "type.googleapis.com/ttn.lorawan.v3.ErrorDetails",
+ "namespace": "pkg/console/internal/events/subscriptions",
+ "name": "already_subscribed",
+ "message_format": "already subscribed with ID `{id}`",
+ "attributes": {
+ "id": "1"
+ },
+ "correlation_id": "5da004b9f61f479aafe5bbcae4551e63",
+ "code": 6
+ }
+ ]
+ }
+}
+```
diff --git a/pkg/console/internal/events/protocol/protocol.go b/pkg/console/internal/events/protocol/protocol.go
new file mode 100644
index 0000000000..3348a8dc82
--- /dev/null
+++ b/pkg/console/internal/events/protocol/protocol.go
@@ -0,0 +1,331 @@
+// 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.
+
+// Package protocol implements the protocol for the events package.
+package protocol
+
+import (
+ "encoding/json"
+ "time"
+
+ "go.thethings.network/lorawan-stack/v3/pkg/errors"
+ "go.thethings.network/lorawan-stack/v3/pkg/jsonpb"
+ "go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
+ statuspb "google.golang.org/genproto/googleapis/rpc/status"
+ "google.golang.org/grpc/status"
+)
+
+var (
+ errMessageType = errors.DefineInvalidArgument("message_type", "invalid message type `{type}`")
+
+ _ json.Marshaler = (*ttnpb.EntityIdentifiers)(nil)
+ _ json.Unmarshaler = (*ttnpb.EntityIdentifiers)(nil)
+
+ _ json.Marshaler = (*ttnpb.Event)(nil)
+ _ json.Unmarshaler = (*ttnpb.Event)(nil)
+)
+
+// MessageType is the type of a message.
+type MessageType int
+
+const (
+ // MessageTypeSubscribe is the type of a subscribe message.
+ MessageTypeSubscribe MessageType = iota
+ // MessageTypeUnsubscribe is the type of an unsubscribe message.
+ MessageTypeUnsubscribe
+ // MessageTypePublish is the type of a publish message.
+ MessageTypePublish
+ // MessageTypeError is the type of an error message.
+ MessageTypeError
+)
+
+// MarshalJSON implements json.Marshaler.
+func (m MessageType) MarshalJSON() ([]byte, error) {
+ switch m {
+ case MessageTypeSubscribe:
+ return []byte(`"subscribe"`), nil
+ case MessageTypeUnsubscribe:
+ return []byte(`"unsubscribe"`), nil
+ case MessageTypePublish:
+ return []byte(`"publish"`), nil
+ case MessageTypeError:
+ return []byte(`"error"`), nil
+ default:
+ return nil, errMessageType.WithAttributes("type", m)
+ }
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (m *MessageType) UnmarshalJSON(data []byte) error {
+ switch string(data) {
+ case `"subscribe"`:
+ *m = MessageTypeSubscribe
+ case `"unsubscribe"`:
+ *m = MessageTypeUnsubscribe
+ case `"publish"`:
+ *m = MessageTypePublish
+ case `"error"`:
+ *m = MessageTypeError
+ default:
+ return errMessageType.WithAttributes("type", string(data))
+ }
+ return nil
+}
+
+// Request is a request message.
+type Request interface {
+ _requestMessage()
+}
+
+// Response is a response message.
+type Response interface {
+ _responseMessage()
+}
+
+// SubscribeRequest is the request to subscribe to events.
+type SubscribeRequest struct {
+ ID uint64 `json:"id"`
+ Identifiers []*ttnpb.EntityIdentifiers `json:"identifiers"`
+ Tail uint32 `json:"tail"`
+ After *time.Time `json:"after"`
+ Names []string `json:"names"`
+}
+
+func (SubscribeRequest) _requestMessage() {}
+
+// Response builds a response to the request.
+func (m SubscribeRequest) Response(err error) Response {
+ if err != nil {
+ return newErrorResponse(m.ID, err)
+ }
+ return &SubscribeResponse{
+ ID: m.ID,
+ }
+}
+
+// MarshalJSON implements json.Marshaler.
+func (m SubscribeRequest) MarshalJSON() ([]byte, error) {
+ type alias SubscribeRequest
+ return jsonpb.TTN().Marshal(struct {
+ Type MessageType `json:"type"`
+ alias
+ }{
+ Type: MessageTypeSubscribe,
+ alias: alias(m),
+ })
+}
+
+// SubscribeResponse is the response to a subscribe request.
+type SubscribeResponse struct {
+ ID uint64 `json:"id"`
+}
+
+func (SubscribeResponse) _responseMessage() {}
+
+// MarshalJSON implements json.Marshaler.
+func (m SubscribeResponse) MarshalJSON() ([]byte, error) {
+ type alias SubscribeResponse
+ return jsonpb.TTN().Marshal(struct {
+ Type MessageType `json:"type"`
+ alias
+ }{
+ Type: MessageTypeSubscribe,
+ alias: alias(m),
+ })
+}
+
+// UnsubscribeRequest is the request to unsubscribe from events.
+type UnsubscribeRequest struct {
+ ID uint64 `json:"id"`
+}
+
+func (UnsubscribeRequest) _requestMessage() {}
+
+// MarshalJSON implements json.Marshaler.
+func (m UnsubscribeRequest) MarshalJSON() ([]byte, error) {
+ type alias UnsubscribeRequest
+ return jsonpb.TTN().Marshal(struct {
+ Type MessageType `json:"type"`
+ alias
+ }{
+ Type: MessageTypeUnsubscribe,
+ alias: alias(m),
+ })
+}
+
+// UnsubscribeResponse is the response to an unsubscribe request.
+type UnsubscribeResponse struct {
+ ID uint64 `json:"id"`
+}
+
+func (UnsubscribeResponse) _responseMessage() {}
+
+// Response builds a response to the request.
+func (m UnsubscribeRequest) Response(err error) Response {
+ if err != nil {
+ return newErrorResponse(m.ID, err)
+ }
+ return &UnsubscribeResponse{
+ ID: m.ID,
+ }
+}
+
+// MarshalJSON implements json.Marshaler.
+func (m UnsubscribeResponse) MarshalJSON() ([]byte, error) {
+ type alias UnsubscribeResponse
+ return jsonpb.TTN().Marshal(struct {
+ Type MessageType `json:"type"`
+ alias
+ }{
+ Type: MessageTypeUnsubscribe,
+ alias: alias(m),
+ })
+}
+
+// PublishResponse is the request to publish an event.
+type PublishResponse struct {
+ ID uint64 `json:"id"`
+ Event *ttnpb.Event `json:"event"`
+}
+
+func (PublishResponse) _responseMessage() {}
+
+// MarshalJSON implements json.Marshaler.
+func (m PublishResponse) MarshalJSON() ([]byte, error) {
+ type alias PublishResponse
+ return jsonpb.TTN().Marshal(struct {
+ Type MessageType `json:"type"`
+ alias
+ }{
+ Type: MessageTypePublish,
+ alias: alias(m),
+ })
+}
+
+// ErrorResponse is the response to an error.
+type ErrorResponse struct {
+ ID uint64
+ Error *status.Status
+}
+
+func (ErrorResponse) _responseMessage() {}
+
+// statusAlias is an alias of status.Status which supports JSON marshaling.
+type statusAlias statuspb.Status
+
+// MarshalJSON implements json.Marshaler.
+func (s *statusAlias) MarshalJSON() ([]byte, error) {
+ return jsonpb.TTN().Marshal((*statuspb.Status)(s))
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (s *statusAlias) UnmarshalJSON(data []byte) error {
+ return jsonpb.TTN().Unmarshal(data, (*statuspb.Status)(s))
+}
+
+// MarshalJSON implements json.Marshaler.
+func (m ErrorResponse) MarshalJSON() ([]byte, error) {
+ return jsonpb.TTN().Marshal(struct {
+ Type MessageType `json:"type"`
+ ID uint64 `json:"id"`
+ Error *statusAlias `json:"error"`
+ }{
+ Type: MessageTypeError,
+ ID: m.ID,
+ Error: (*statusAlias)(m.Error.Proto()),
+ })
+}
+
+func newErrorResponse(id uint64, err error) Response {
+ return &ErrorResponse{
+ ID: id,
+ Error: status.Convert(err),
+ }
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (m *ErrorResponse) UnmarshalJSON(data []byte) error {
+ var alias struct {
+ ID uint64 `json:"id"`
+ Error *statusAlias `json:"error"`
+ }
+ if err := jsonpb.TTN().Unmarshal(data, &alias); err != nil {
+ return err
+ }
+ m.ID = alias.ID
+ m.Error = status.FromProto((*statuspb.Status)(alias.Error))
+ return nil
+}
+
+// RequestWrapper wraps a request to be sent over the websocket.
+type RequestWrapper struct {
+ Contents Request
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (m *RequestWrapper) UnmarshalJSON(data []byte) error {
+ var contents struct {
+ Type MessageType `json:"type"`
+ }
+ if err := jsonpb.TTN().Unmarshal(data, &contents); err != nil {
+ return err
+ }
+ switch contents.Type {
+ case MessageTypeSubscribe:
+ m.Contents = &SubscribeRequest{}
+ case MessageTypeUnsubscribe:
+ m.Contents = &UnsubscribeRequest{}
+ default:
+ return errMessageType.WithAttributes("type", contents.Type)
+ }
+ return jsonpb.TTN().Unmarshal(data, m.Contents)
+}
+
+// MarshalJSON implements json.Marshaler.
+func (m RequestWrapper) MarshalJSON() ([]byte, error) {
+ return json.Marshal(m.Contents)
+}
+
+// ResponseWrapper wraps a response to be sent over the websocket.
+type ResponseWrapper struct {
+ Contents Response
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (m *ResponseWrapper) UnmarshalJSON(data []byte) error {
+ var contents struct {
+ Type MessageType `json:"type"`
+ }
+ if err := jsonpb.TTN().Unmarshal(data, &contents); err != nil {
+ return err
+ }
+ switch contents.Type {
+ case MessageTypeSubscribe:
+ m.Contents = &SubscribeResponse{}
+ case MessageTypeUnsubscribe:
+ m.Contents = &UnsubscribeResponse{}
+ case MessageTypePublish:
+ m.Contents = &PublishResponse{}
+ case MessageTypeError:
+ m.Contents = &ErrorResponse{}
+ default:
+ return errMessageType.WithAttributes("type", contents.Type)
+ }
+ return jsonpb.TTN().Unmarshal(data, m.Contents)
+}
+
+// MarshalJSON implements json.Marshaler.
+func (m ResponseWrapper) MarshalJSON() ([]byte, error) {
+ return json.Marshal(m.Contents)
+}
diff --git a/pkg/console/internal/events/protocol/protocol_test.go b/pkg/console/internal/events/protocol/protocol_test.go
new file mode 100644
index 0000000000..bec85bc950
--- /dev/null
+++ b/pkg/console/internal/events/protocol/protocol_test.go
@@ -0,0 +1,220 @@
+// 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.
+
+package protocol_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/smarty/assertions"
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/protocol"
+ "go.thethings.network/lorawan-stack/v3/pkg/errors"
+ "go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
+ "go.thethings.network/lorawan-stack/v3/pkg/util/test"
+ "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should"
+ "google.golang.org/grpc/status"
+ "google.golang.org/protobuf/types/known/anypb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+func TestMarshal(t *testing.T) {
+ t.Parallel()
+
+ a := assertions.New(t)
+
+ b, err := json.Marshal(protocol.MessageTypePublish)
+ if a.So(err, should.BeNil) {
+ a.So(b, should.Resemble, []byte(`"publish"`))
+ }
+ var tp protocol.MessageType
+ err = json.Unmarshal([]byte(`"publish"`), &tp)
+ if a.So(err, should.BeNil) {
+ a.So(tp, should.Equal, protocol.MessageTypePublish)
+ }
+
+ b, err = json.Marshal(&protocol.SubscribeRequest{
+ ID: 0x42,
+ Identifiers: []*ttnpb.EntityIdentifiers{
+ (&ttnpb.ApplicationIdentifiers{ApplicationId: "foo"}).GetEntityIdentifiers(),
+ (&ttnpb.ClientIdentifiers{ClientId: "bar"}).GetEntityIdentifiers(),
+ },
+ Tail: 10,
+ After: timePtr(time.UnixMilli(123456789012).UTC()),
+ Names: []string{"foo", "bar"},
+ })
+ if a.So(err, should.BeNil) {
+ a.So(
+ b,
+ should.Resemble,
+ []byte(`{"type":"subscribe","id":66,"identifiers":[{"application_ids":{"application_id":"foo"}},{"client_ids":{"client_id":"bar"}}],"tail":10,"after":"1973-11-29T21:33:09.012Z","names":["foo","bar"]}`), // nolint:lll
+ )
+ }
+ var subReq protocol.SubscribeRequest
+ err = json.Unmarshal(
+ []byte(`{"type":"subscribe","id":66,"identifiers":[{"application_ids":{"application_id":"foo"}},{"client_ids":{"client_id":"bar"}}],"tail":10,"after":"1973-11-29T21:33:09.012Z","names":["foo","bar"]}`), // nolint:lll
+ &subReq,
+ )
+ if a.So(err, should.BeNil) {
+ a.So(subReq, should.Resemble, protocol.SubscribeRequest{
+ ID: 0x42,
+ Identifiers: []*ttnpb.EntityIdentifiers{
+ (&ttnpb.ApplicationIdentifiers{ApplicationId: "foo"}).GetEntityIdentifiers(),
+ (&ttnpb.ClientIdentifiers{ClientId: "bar"}).GetEntityIdentifiers(),
+ },
+ Tail: 10,
+ After: timePtr(time.UnixMilli(123456789012).UTC()),
+ Names: []string{"foo", "bar"},
+ })
+ }
+
+ b, err = json.Marshal(&protocol.SubscribeResponse{
+ ID: 0x42,
+ })
+ if a.So(err, should.BeNil) {
+ a.So(b, should.Resemble, []byte(`{"type":"subscribe","id":66}`))
+ }
+ var subResp protocol.SubscribeResponse
+ err = json.Unmarshal([]byte(`{"type":"subscribe","id":66}`), &subResp)
+ if a.So(err, should.BeNil) {
+ a.So(subResp, should.Resemble, protocol.SubscribeResponse{ID: 0x42})
+ }
+
+ b, err = json.Marshal(&protocol.UnsubscribeRequest{
+ ID: 0x42,
+ })
+ if a.So(err, should.BeNil) {
+ a.So(b, should.Resemble, []byte(`{"type":"unsubscribe","id":66}`))
+ }
+ var unsubReq protocol.UnsubscribeRequest
+ err = json.Unmarshal([]byte(`{"type":"unsubscribe","id":66}`), &unsubReq)
+ if a.So(err, should.BeNil) {
+ a.So(unsubReq, should.Resemble, protocol.UnsubscribeRequest{ID: 0x42})
+ }
+
+ b, err = json.Marshal(&protocol.UnsubscribeResponse{
+ ID: 0x42,
+ })
+ if a.So(err, should.BeNil) {
+ a.So(b, should.Resemble, []byte(`{"type":"unsubscribe","id":66}`))
+ }
+ var unsubResp protocol.UnsubscribeResponse
+ err = json.Unmarshal([]byte(`{"type":"unsubscribe","id":66}`), &unsubResp)
+ if a.So(err, should.BeNil) {
+ a.So(unsubResp, should.Resemble, protocol.UnsubscribeResponse{ID: 0x42})
+ }
+
+ b, err = json.Marshal(&protocol.PublishResponse{
+ ID: 0x42,
+ Event: &ttnpb.Event{
+ Name: "foo",
+ Time: timestamppb.New(time.UnixMilli(123456789012).UTC()),
+ Identifiers: []*ttnpb.EntityIdentifiers{
+ (&ttnpb.ApplicationIdentifiers{ApplicationId: "foo"}).GetEntityIdentifiers(),
+ },
+ Data: test.Must(anypb.New(&ttnpb.ApplicationUp{
+ Up: &ttnpb.ApplicationUp_UplinkMessage{
+ UplinkMessage: &ttnpb.ApplicationUplink{},
+ },
+ })),
+ CorrelationIds: []string{"foo", "bar"},
+ },
+ })
+ if a.So(err, should.BeNil) {
+ a.So(
+ b,
+ should.Resemble,
+ []byte(`{"type":"publish","id":66,"event":{"name":"foo","time":"1973-11-29T21:33:09.012Z","identifiers":[{"application_ids":{"application_id":"foo"}}],"data":{"@type":"type.googleapis.com/ttn.lorawan.v3.ApplicationUp","uplink_message":{}},"correlation_ids":["foo","bar"]}}`), // nolint:lll
+ )
+ }
+ var pubResp protocol.PublishResponse
+ err = json.Unmarshal(
+ []byte(`{"type":"publish","id":66,"event":{"name":"foo","time":"1973-11-29T21:33:09.012Z","identifiers":[{"application_ids":{"application_id":"foo"}}],"data":{"@type":"type.googleapis.com/ttn.lorawan.v3.ApplicationUp","uplink_message":{}},"correlation_ids":["foo","bar"]}}`), // nolint:lll
+ &pubResp,
+ )
+ if a.So(err, should.BeNil) {
+ a.So(pubResp, should.Resemble, protocol.PublishResponse{
+ ID: 0x42,
+ Event: &ttnpb.Event{
+ Name: "foo",
+ Time: timestamppb.New(time.UnixMilli(123456789012).UTC()),
+ Identifiers: []*ttnpb.EntityIdentifiers{
+ (&ttnpb.ApplicationIdentifiers{ApplicationId: "foo"}).GetEntityIdentifiers(),
+ },
+ Data: test.Must(anypb.New(&ttnpb.ApplicationUp{
+ Up: &ttnpb.ApplicationUp_UplinkMessage{
+ UplinkMessage: &ttnpb.ApplicationUplink{},
+ },
+ })),
+ CorrelationIds: []string{"foo", "bar"},
+ },
+ })
+ }
+
+ errDefinition := errors.DefineInvalidArgument("bad_argument", "bad argument `{argument}`")
+ errInstance := errDefinition.WithAttributes("argument", "foo")
+ errStatus := status.Convert(errInstance)
+ errJSON := test.Must(json.Marshal(errInstance))
+ b, err = json.Marshal(&protocol.ErrorResponse{
+ ID: 0x42,
+ Error: errStatus,
+ })
+ if a.So(err, should.BeNil) {
+ a.So(b, should.Resemble, []byte(fmt.Sprintf(`{"type":"error","id":66,"error":%v}`, string(errJSON)))) // nolint:lll
+ }
+ var errResp protocol.ErrorResponse
+ err = json.Unmarshal([]byte(fmt.Sprintf(`{"type":"error","id":66,"error":%v}`, string(errJSON))), &errResp) // nolint:lll
+ if a.So(err, should.BeNil) {
+ a.So(errResp, should.Resemble, protocol.ErrorResponse{
+ ID: 0x42,
+ Error: errStatus,
+ })
+ }
+
+ var reqWrapper protocol.RequestWrapper
+ err = json.Unmarshal(
+ []byte(`{"type":"subscribe","id":66,"identifiers":[{"application_ids":{"application_id":"foo"}},{"client_ids":{"client_id":"bar"}}],"tail":10,"after":"1973-11-29T21:33:09.012Z","names":["foo","bar"]}`), // nolint:lll
+ &reqWrapper,
+ )
+ if a.So(err, should.BeNil) {
+ a.So(reqWrapper, should.Resemble, protocol.RequestWrapper{
+ Contents: &protocol.SubscribeRequest{
+ ID: 0x42,
+ Identifiers: []*ttnpb.EntityIdentifiers{
+ (&ttnpb.ApplicationIdentifiers{ApplicationId: "foo"}).GetEntityIdentifiers(),
+ (&ttnpb.ClientIdentifiers{ClientId: "bar"}).GetEntityIdentifiers(),
+ },
+ Tail: 10,
+ After: timePtr(time.UnixMilli(123456789012).UTC()),
+ Names: []string{"foo", "bar"},
+ },
+ })
+ }
+
+ var respWrapper protocol.ResponseWrapper
+ err = json.Unmarshal([]byte(`{"type":"subscribe","id":66}`), &respWrapper)
+ if a.So(err, should.BeNil) {
+ a.So(respWrapper, should.Resemble, protocol.ResponseWrapper{
+ Contents: &protocol.SubscribeResponse{
+ ID: 0x42,
+ },
+ })
+ }
+}
+
+func timePtr(t time.Time) *time.Time {
+ return &t
+}
diff --git a/pkg/console/internal/events/subscriptions/subscriptions.go b/pkg/console/internal/events/subscriptions/subscriptions.go
new file mode 100644
index 0000000000..0a099fffdd
--- /dev/null
+++ b/pkg/console/internal/events/subscriptions/subscriptions.go
@@ -0,0 +1,255 @@
+// 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.
+
+// Package subscriptions implements the events mux subscriptions.
+package subscriptions
+
+import (
+ "context"
+ "sync"
+ "time"
+
+ "go.thethings.network/lorawan-stack/v3/pkg/auth/rights"
+ "go.thethings.network/lorawan-stack/v3/pkg/auth/rights/rightsutil"
+ "go.thethings.network/lorawan-stack/v3/pkg/errors"
+ "go.thethings.network/lorawan-stack/v3/pkg/events"
+ "go.thethings.network/lorawan-stack/v3/pkg/log"
+ "go.thethings.network/lorawan-stack/v3/pkg/task"
+ "go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
+)
+
+// SubscriptionEvent wraps an events.Event with a subscription ID.
+type SubscriptionEvent struct {
+ ID uint64
+ Event events.Event
+}
+
+// Interface is the interface for the events mux subscriptions.
+type Interface interface {
+ // Subscribe subscribes to events.
+ Subscribe(
+ id uint64, identifiers []*ttnpb.EntityIdentifiers, after *time.Time, tail uint32, names []string,
+ ) error
+ // Unsubscribe unsubscribe to events.
+ Unsubscribe(id uint64) error
+
+ // SubscriptionEvents provides the events for the underlying subscriptions.
+ SubscriptionEvents() <-chan *SubscriptionEvent
+
+ // Close closes all of the underlying subscriptions and waits for the background tasks to finish.
+ Close() error
+}
+
+type subscription struct {
+ id uint64
+ cancel func(error)
+ wg sync.WaitGroup
+ cancelParent func(error)
+ inputCh <-chan events.Event
+ outputCh chan<- *SubscriptionEvent
+}
+
+func (s *subscription) run(ctx context.Context) (err error) {
+ defer func() {
+ select {
+ case <-ctx.Done():
+ default:
+ s.cancelParent(err)
+ }
+ }()
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case evt := <-s.inputCh:
+ isVisible, err := rightsutil.EventIsVisible(ctx, evt)
+ if err != nil {
+ if err := rights.RequireAny(ctx, evt.Identifiers()...); err != nil {
+ return err
+ }
+ log.FromContext(ctx).WithError(err).Warn("Failed to check event visibility")
+ continue
+ }
+ if !isVisible {
+ continue
+ }
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case s.outputCh <- &SubscriptionEvent{
+ ID: s.id,
+ Event: evt,
+ }:
+ }
+ }
+ }
+}
+
+type subscriptions struct {
+ ctx context.Context
+ cancel func(error)
+ subscriber events.Subscriber
+ definedNames map[string]struct{}
+ taskStarter task.Starter
+
+ wg sync.WaitGroup
+ ch chan *SubscriptionEvent
+ subs map[uint64]*subscription
+}
+
+var _ Interface = (*subscriptions)(nil)
+
+// Close implements Interface.
+func (s *subscriptions) Close() error {
+ for id, sub := range s.subs {
+ delete(s.subs, id)
+ sub.cancel(nil)
+ sub.wg.Wait()
+ }
+ s.wg.Wait()
+ return nil
+}
+
+// SubscriptionEvents implements Interface.
+func (s *subscriptions) SubscriptionEvents() <-chan *SubscriptionEvent { return s.ch }
+
+var (
+ errAlreadySubscribed = errors.DefineAlreadyExists("already_subscribed", "already subscribed with ID `{id}`")
+ errNoIdentifiers = errors.DefineInvalidArgument("no_identifiers", "no identifiers")
+)
+
+// Subscribe implements Interface.
+func (s *subscriptions) Subscribe(
+ id uint64, identifiers []*ttnpb.EntityIdentifiers, after *time.Time, tail uint32, names []string,
+) (err error) {
+ if err := s.validateSubscribe(id, identifiers); err != nil {
+ return err
+ }
+ names, err = events.NamesFromPatterns(s.definedNames, names)
+ if err != nil {
+ return err
+ }
+ ch := make(chan events.Event, channelSize(tail))
+ ctx, cancel := context.WithCancelCause(s.ctx)
+ defer func() {
+ if err != nil {
+ cancel(err)
+ }
+ }()
+ if store, hasStore := s.subscriber.(events.Store); hasStore {
+ if after == nil && tail == 0 {
+ now := time.Now()
+ after = &now
+ }
+ f := func(ctx context.Context) (err error) {
+ defer func() {
+ select {
+ case <-ctx.Done():
+ default:
+ s.cancel(err)
+ }
+ }()
+ return store.SubscribeWithHistory(ctx, names, identifiers, after, int(tail), events.Channel(ch))
+ }
+ s.wg.Add(1)
+ s.taskStarter.StartTask(&task.Config{
+ Context: ctx,
+ ID: "console_events_subscribe",
+ Func: f,
+ Done: s.wg.Done,
+ Restart: task.RestartNever,
+ Backoff: task.DefaultBackoffConfig,
+ })
+ } else {
+ if err := s.subscriber.Subscribe(ctx, names, identifiers, events.Channel(ch)); err != nil {
+ return err
+ }
+ }
+ sub := &subscription{
+ id: id,
+ cancel: cancel,
+ cancelParent: s.cancel,
+ inputCh: ch,
+ outputCh: s.ch,
+ }
+ sub.wg.Add(1)
+ s.taskStarter.StartTask(&task.Config{
+ Context: ctx,
+ ID: "console_events_filter",
+ Func: sub.run,
+ Done: sub.wg.Done,
+ Restart: task.RestartNever,
+ Backoff: task.DefaultBackoffConfig,
+ })
+ s.subs[id] = sub
+ return nil
+}
+
+var errNotSubscribed = errors.DefineNotFound("not_subscribed", "not subscribed with ID `{id}`")
+
+// Unsubscribe implements Interface.
+func (s *subscriptions) Unsubscribe(id uint64) error {
+ sub, ok := s.subs[id]
+ if !ok {
+ return errNotSubscribed.WithAttributes("id", id)
+ }
+ delete(s.subs, id)
+ sub.cancel(nil)
+ sub.wg.Wait()
+ return nil
+}
+
+// New returns a new Interface.
+func New(
+ ctx context.Context,
+ cancel func(error),
+ subscriber events.Subscriber,
+ definedNames map[string]struct{},
+ taskStarter task.Starter,
+) Interface {
+ return &subscriptions{
+ ctx: ctx,
+ cancel: cancel,
+ subscriber: subscriber,
+ definedNames: definedNames,
+ taskStarter: taskStarter,
+ ch: make(chan *SubscriptionEvent, 1),
+ subs: make(map[uint64]*subscription),
+ }
+}
+
+func (s *subscriptions) validateSubscribe(id uint64, identifiers []*ttnpb.EntityIdentifiers) error {
+ if _, ok := s.subs[id]; ok {
+ return errAlreadySubscribed.WithAttributes("id", id)
+ }
+ if len(identifiers) == 0 {
+ return errNoIdentifiers.New()
+ }
+ for _, ids := range identifiers {
+ if err := ids.ValidateFields(); err != nil {
+ return err
+ }
+ }
+ return rights.RequireAny(s.ctx, identifiers...)
+}
+
+func channelSize(n uint32) uint32 {
+ if n < 8 {
+ n = 8
+ }
+ if n > 1024 {
+ n = 1024
+ }
+ return n
+}
diff --git a/pkg/console/internal/events/subscriptions/subscriptions_test.go b/pkg/console/internal/events/subscriptions/subscriptions_test.go
new file mode 100644
index 0000000000..10aaf738bb
--- /dev/null
+++ b/pkg/console/internal/events/subscriptions/subscriptions_test.go
@@ -0,0 +1,303 @@
+// 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.
+
+package subscriptions_test
+
+import (
+ "context"
+ "sync"
+ "testing"
+ "time"
+
+ "go.thethings.network/lorawan-stack/v3/pkg/auth/rights"
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/subscriptions"
+ "go.thethings.network/lorawan-stack/v3/pkg/events"
+ "go.thethings.network/lorawan-stack/v3/pkg/task"
+ "go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
+ "go.thethings.network/lorawan-stack/v3/pkg/unique"
+ "go.thethings.network/lorawan-stack/v3/pkg/util/test"
+ "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should"
+)
+
+type subscribeRequest struct {
+ Context context.Context
+ Names []string
+ Identifiers []*ttnpb.EntityIdentifiers
+ After *time.Time
+ Tail int
+ Handler events.Handler
+
+ Response chan<- error
+}
+
+type mockSubscriber struct {
+ subReqs chan subscribeRequest
+}
+
+func (m *mockSubscriber) subscribeRequests() <-chan subscribeRequest { return m.subReqs }
+
+// Subscribe implements events.Subscriber.
+func (m *mockSubscriber) Subscribe(
+ ctx context.Context, names []string, identifiers []*ttnpb.EntityIdentifiers, hdl events.Handler,
+) error {
+ ch := make(chan error, 1)
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case m.subReqs <- subscribeRequest{
+ Context: ctx,
+ Names: names,
+ Identifiers: identifiers,
+ Handler: hdl,
+
+ Response: ch,
+ }:
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case err := <-ch:
+ return err
+ }
+ }
+}
+
+var _ events.Subscriber = (*mockSubscriber)(nil)
+
+type mockPubSubStore struct {
+ subReqs chan subscribeRequest
+}
+
+func (m *mockPubSubStore) subscribeRequests() <-chan subscribeRequest { return m.subReqs }
+
+func (*mockPubSubStore) historical() {}
+
+// Publish implements events.Store.
+func (*mockPubSubStore) Publish(...events.Event) { panic("not implemented") }
+
+// Subscribe implements events.Store.
+func (*mockPubSubStore) Subscribe(context.Context, []string, []*ttnpb.EntityIdentifiers, events.Handler) error {
+ panic("not implemented")
+}
+
+// FindRelated implements events.Store.
+func (*mockPubSubStore) FindRelated(context.Context, string) ([]events.Event, error) {
+ panic("not implemented")
+}
+
+// FetchHistory implements events.Store.
+func (*mockPubSubStore) FetchHistory(
+ context.Context, []string, []*ttnpb.EntityIdentifiers, *time.Time, int,
+) ([]events.Event, error) {
+ panic("not implemented")
+}
+
+// SubscribeWithHistory implements events.Store.
+func (m *mockPubSubStore) SubscribeWithHistory(
+ ctx context.Context, names []string, ids []*ttnpb.EntityIdentifiers, after *time.Time, tail int, hdl events.Handler,
+) error {
+ ch := make(chan error, 1)
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case m.subReqs <- subscribeRequest{
+ Context: ctx,
+ Names: names,
+ Identifiers: ids,
+ After: after,
+ Tail: tail,
+ Handler: hdl,
+
+ Response: ch,
+ }:
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case err := <-ch:
+ return err
+ }
+ }
+}
+
+var _ events.Store = (*mockPubSubStore)(nil)
+
+func runTestSubscriptions(
+ t *testing.T,
+ subscriber interface {
+ events.Subscriber
+ subscribeRequests() <-chan subscribeRequest
+ },
+) {
+ t.Helper()
+
+ _, historical := subscriber.(interface{ historical() })
+
+ a, ctx := test.New(t)
+ ctx, cancel := context.WithCancelCause(ctx)
+ defer cancel(nil)
+
+ timeout := test.Delay << 3
+ app1IDs, app2IDs := &ttnpb.ApplicationIdentifiers{
+ ApplicationId: "foo",
+ }, &ttnpb.ApplicationIdentifiers{
+ ApplicationId: "bar",
+ }
+ ctx = rights.NewContext(ctx, &rights.Rights{
+ ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{
+ unique.ID(ctx, app1IDs): ttnpb.RightsFrom(ttnpb.Right_RIGHT_APPLICATION_ALL),
+ }),
+ })
+
+ sub := subscriptions.New(
+ ctx,
+ cancel,
+ subscriber,
+ map[string]struct{}{
+ "test": {},
+ },
+ task.StartTaskFunc(task.DefaultStartTask),
+ )
+ defer sub.Close()
+
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(timeout):
+ case req := <-subscriber.subscribeRequests():
+ t.Fatal("Unexpected subscribe request", req)
+ }
+
+ now := time.Now()
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ err := sub.Subscribe(
+ 1,
+ []*ttnpb.EntityIdentifiers{
+ app1IDs.GetEntityIdentifiers(),
+ },
+ &now,
+ 10,
+ []string{"test"},
+ )
+ a.So(err, should.BeNil)
+ }()
+ var handler events.Handler
+ select {
+ case <-ctx.Done():
+ return
+ case req := <-subscriber.subscribeRequests():
+ a.So(req.Context, should.HaveParentContextOrEqual, ctx)
+ a.So(req.Names, should.Resemble, []string{"test"})
+ a.So(req.Identifiers, should.Resemble, []*ttnpb.EntityIdentifiers{
+ app1IDs.GetEntityIdentifiers(),
+ })
+ if historical {
+ a.So(req.After, should.Resemble, &now)
+ a.So(req.Tail, should.Equal, 10)
+ }
+ a.So(req.Handler, should.NotBeNil)
+ if !historical {
+ select {
+ case <-ctx.Done():
+ return
+ case req.Response <- nil:
+ }
+ }
+ handler = req.Handler
+ }
+ wg.Wait()
+
+ err := sub.Subscribe(
+ 1,
+ []*ttnpb.EntityIdentifiers{
+ app1IDs.GetEntityIdentifiers(),
+ },
+ &now,
+ 10,
+ []string{"test"},
+ )
+ a.So(err, should.NotBeNil)
+
+ evt := events.New(
+ ctx,
+ "test",
+ "test",
+ events.WithIdentifiers(app2IDs),
+ events.WithVisibility(ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ),
+ )
+ handler.Notify(evt)
+
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(timeout):
+ case subEvt := <-sub.SubscriptionEvents():
+ t.Fatal("Unexpected subscription event", subEvt)
+ }
+
+ evt = events.New(
+ ctx,
+ "test",
+ "test",
+ events.WithIdentifiers(app1IDs),
+ events.WithVisibility(ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ),
+ )
+ handler.Notify(evt)
+
+ select {
+ case <-ctx.Done():
+ return
+ case subEvt := <-sub.SubscriptionEvents():
+ a.So(subEvt.ID, should.Equal, 1)
+ a.So(subEvt.Event, should.ResembleEvent, evt)
+ }
+
+ err = sub.Unsubscribe(1)
+ a.So(err, should.BeNil)
+
+ err = sub.Unsubscribe(1)
+ a.So(err, should.NotBeNil)
+
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(timeout):
+ case subEvt := <-sub.SubscriptionEvents():
+ t.Fatal("Unexpected subscription event", subEvt)
+ }
+}
+
+func TestSubscriptions(t *testing.T) {
+ t.Parallel()
+ runTestSubscriptions(
+ t,
+ &mockSubscriber{
+ subReqs: make(chan subscribeRequest, 1),
+ },
+ )
+}
+
+func TestStoreSubscriptions(t *testing.T) {
+ t.Parallel()
+ runTestSubscriptions(
+ t,
+ &mockPubSubStore{
+ subReqs: make(chan subscribeRequest, 1),
+ },
+ )
+}
diff --git a/pkg/console/internal/events/tasks.go b/pkg/console/internal/events/tasks.go
new file mode 100644
index 0000000000..1130b7297b
--- /dev/null
+++ b/pkg/console/internal/events/tasks.go
@@ -0,0 +1,76 @@
+// 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.
+
+package events
+
+import (
+ "context"
+ "errors"
+ "io"
+
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/eventsmux"
+ "go.thethings.network/lorawan-stack/v3/pkg/console/internal/events/protocol"
+ "go.thethings.network/lorawan-stack/v3/pkg/log"
+ "nhooyr.io/websocket"
+ "nhooyr.io/websocket/wsjson"
+)
+
+func makeMuxTask(m eventsmux.Interface, cancel func(error)) func(context.Context) error {
+ return func(ctx context.Context) (err error) {
+ defer func() { cancel(err) }()
+ return m.Run(ctx)
+ }
+}
+
+func makeReadTask(conn *websocket.Conn, m eventsmux.Interface, cancel func(error)) func(context.Context) error {
+ return func(ctx context.Context) (err error) {
+ defer func() { cancel(err) }()
+ defer func() {
+ if closeErr := (websocket.CloseError{}); errors.As(err, &closeErr) {
+ log.FromContext(ctx).WithFields(log.Fields(
+ "code", closeErr.Code,
+ "reason", closeErr.Reason,
+ )).Debug("WebSocket closed")
+ err = io.EOF
+ }
+ }()
+ for {
+ var request protocol.RequestWrapper
+ if err := wsjson.Read(ctx, conn, &request); err != nil {
+ return err
+ }
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case m.Requests() <- request.Contents:
+ }
+ }
+ }
+}
+
+func makeWriteTask(conn *websocket.Conn, m eventsmux.Interface, cancel func(error)) func(context.Context) error {
+ return func(ctx context.Context) (err error) {
+ defer func() { cancel(err) }()
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case response := <-m.Responses():
+ if err := wsjson.Write(ctx, conn, response); err != nil {
+ return err
+ }
+ }
+ }
+ }
+}
diff --git a/pkg/devicerepository/grpc.go b/pkg/devicerepository/grpc.go
index c0d490b751..bad776016a 100644
--- a/pkg/devicerepository/grpc.go
+++ b/pkg/devicerepository/grpc.go
@@ -65,7 +65,7 @@ func (dr *DeviceRepository) ListBrands(
ctx context.Context,
req *ttnpb.ListEndDeviceBrandsRequest,
) (*ttnpb.ListEndDeviceBrandsResponse, error) {
- if err := rights.RequireAuthentication(ctx); err != nil {
+ if err := rights.RequireAuthenticated(ctx); err != nil {
return nil, err
}
if req.Limit > defaultLimit || req.Limit == 0 {
@@ -97,7 +97,7 @@ func (dr *DeviceRepository) GetBrand(
ctx context.Context,
req *ttnpb.GetEndDeviceBrandRequest,
) (*ttnpb.EndDeviceBrand, error) {
- if err := rights.RequireAuthentication(ctx); err != nil {
+ if err := rights.RequireAuthenticated(ctx); err != nil {
return nil, err
}
response, err := dr.store.GetBrands(store.GetBrandsRequest{
@@ -121,7 +121,7 @@ func (dr *DeviceRepository) ListModels(
ctx context.Context,
req *ttnpb.ListEndDeviceModelsRequest,
) (*ttnpb.ListEndDeviceModelsResponse, error) {
- if err := rights.RequireAuthentication(ctx); err != nil {
+ if err := rights.RequireAuthenticated(ctx); err != nil {
return nil, err
}
if req.Limit > defaultLimit || req.Limit == 0 {
@@ -152,7 +152,7 @@ func (dr *DeviceRepository) GetModel(
ctx context.Context,
req *ttnpb.GetEndDeviceModelRequest,
) (*ttnpb.EndDeviceModel, error) {
- if err := rights.RequireAuthentication(ctx); err != nil {
+ if err := rights.RequireAuthenticated(ctx); err != nil {
return nil, err
}
response, err := dr.store.GetModels(store.GetModelsRequest{
@@ -177,7 +177,7 @@ func (dr *DeviceRepository) GetTemplate(
ctx context.Context,
req *ttnpb.GetTemplateRequest,
) (*ttnpb.EndDeviceTemplate, error) {
- if err := rights.RequireAuthentication(ctx); err != nil {
+ if err := rights.RequireAuthenticated(ctx); err != nil {
return nil, err
}
return dr.store.GetTemplate(req, nil)
@@ -189,7 +189,7 @@ func getDecoder(
f func(store.GetCodecRequest) (*ttnpb.MessagePayloadDecoder, error),
) (*ttnpb.MessagePayloadDecoder, error) {
if clusterauth.Authorized(ctx) != nil {
- if err := rights.RequireAuthentication(ctx); err != nil {
+ if err := rights.RequireAuthenticated(ctx); err != nil {
return nil, err
}
}
@@ -218,7 +218,7 @@ func (dr *DeviceRepository) GetDownlinkEncoder(
req *ttnpb.GetPayloadFormatterRequest,
) (*ttnpb.MessagePayloadEncoder, error) {
if clusterauth.Authorized(ctx) != nil {
- if err := rights.RequireAuthentication(ctx); err != nil {
+ if err := rights.RequireAuthenticated(ctx); err != nil {
return nil, err
}
}
diff --git a/pkg/events/grpc/grpc.go b/pkg/events/grpc/grpc.go
index 0de69e6009..be250151c4 100644
--- a/pkg/events/grpc/grpc.go
+++ b/pkg/events/grpc/grpc.go
@@ -19,9 +19,6 @@ package grpc
import (
"context"
"os"
- "regexp"
- "sort"
- "strings"
"time"
grpc_runtime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
@@ -65,58 +62,6 @@ type EventsServer struct {
definedNames map[string]struct{}
}
-var (
- errInvalidRegexp = errors.DefineInvalidArgument("invalid_regexp", "invalid regexp")
- errNoMatchingEvents = errors.DefineInvalidArgument("no_matching_events", "no matching events for regexp `{regexp}`")
- errUnknownEventName = errors.DefineInvalidArgument("unknown_event_name", "unknown event `{name}`")
-)
-
-func (srv *EventsServer) processNames(names ...string) ([]string, error) {
- if len(names) == 0 {
- return nil, nil
- }
- nameMap := make(map[string]struct{})
- for _, name := range names {
- if strings.HasPrefix(name, "/") && strings.HasSuffix(name, "/") {
- re, err := regexp.Compile(strings.Trim(name, "/"))
- if err != nil {
- return nil, errInvalidRegexp.WithCause(err)
- }
- var found bool
- for defined := range srv.definedNames {
- if re.MatchString(defined) {
- nameMap[defined] = struct{}{}
- found = true
- }
- }
- if !found {
- return nil, errNoMatchingEvents.WithAttributes("regexp", re.String())
- }
- } else {
- var found bool
- for defined := range srv.definedNames {
- if name == defined {
- nameMap[name] = struct{}{}
- found = true
- break
- }
- }
- if !found {
- return nil, errUnknownEventName.WithAttributes("name", name)
- }
- }
- }
- if len(nameMap) == 0 {
- return nil, nil
- }
- out := make([]string, 0, len(nameMap))
- for name := range nameMap {
- out = append(out, name)
- }
- sort.Strings(out)
- return out, nil
-}
-
var errNoIdentifiers = errors.DefineInvalidArgument("no_identifiers", "no identifiers")
// Stream implements the EventsServer interface.
@@ -125,7 +70,7 @@ func (srv *EventsServer) Stream(req *ttnpb.StreamEventsRequest, stream ttnpb.Eve
return errNoIdentifiers.New()
}
- names, err := srv.processNames(req.Names...)
+ names, err := events.NamesFromPatterns(srv.definedNames, req.Names)
if err != nil {
return err
}
diff --git a/pkg/events/pattern.go b/pkg/events/pattern.go
new file mode 100644
index 0000000000..bb3d5389c3
--- /dev/null
+++ b/pkg/events/pattern.go
@@ -0,0 +1,77 @@
+// 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.
+
+package events
+
+import (
+ "regexp"
+ "sort"
+ "strings"
+
+ "go.thethings.network/lorawan-stack/v3/pkg/errors"
+)
+
+var (
+ errInvalidRegexp = errors.DefineInvalidArgument("invalid_regexp", "invalid regexp")
+ errNoMatchingEvents = errors.DefineInvalidArgument("no_matching_events", "no matching events for regexp `{regexp}`")
+ errUnknownEventName = errors.DefineInvalidArgument("unknown_event_name", "unknown event `{name}`")
+)
+
+// NamesFromPatterns returns the event names which match the given patterns.
+// The defined names are a set of event names which are used to match the patterns.
+func NamesFromPatterns(definedNames map[string]struct{}, patterns []string) ([]string, error) {
+ if len(patterns) == 0 {
+ return nil, nil
+ }
+ nameMap := make(map[string]struct{})
+ for _, name := range patterns {
+ if strings.HasPrefix(name, "/") && strings.HasSuffix(name, "/") {
+ re, err := regexp.Compile(strings.Trim(name, "/"))
+ if err != nil {
+ return nil, errInvalidRegexp.WithCause(err)
+ }
+ var found bool
+ for defined := range definedNames {
+ if re.MatchString(defined) {
+ nameMap[defined] = struct{}{}
+ found = true
+ }
+ }
+ if !found {
+ return nil, errNoMatchingEvents.WithAttributes("regexp", re.String())
+ }
+ } else {
+ var found bool
+ for defined := range definedNames {
+ if name == defined {
+ nameMap[name] = struct{}{}
+ found = true
+ break
+ }
+ }
+ if !found {
+ return nil, errUnknownEventName.WithAttributes("name", name)
+ }
+ }
+ }
+ if len(nameMap) == 0 {
+ return nil, nil
+ }
+ out := make([]string, 0, len(nameMap))
+ for name := range nameMap {
+ out = append(out, name)
+ }
+ sort.Strings(out)
+ return out, nil
+}
diff --git a/pkg/networkserver/grpc_deviceregistry.go b/pkg/networkserver/grpc_deviceregistry.go
index b89a10d032..2ef6b5748d 100644
--- a/pkg/networkserver/grpc_deviceregistry.go
+++ b/pkg/networkserver/grpc_deviceregistry.go
@@ -948,40 +948,39 @@ var (
legacyADRSettingsFields = []string{
"mac_settings.adr_margin",
- "mac_settings.use_adr",
"mac_settings.use_adr.value",
+ "mac_settings.use_adr",
}
adrSettingsFields = []string{
- "mac_settings.adr",
- "mac_settings.adr.mode",
"mac_settings.adr.mode.disabled",
- "mac_settings.adr.mode.dynamic",
- "mac_settings.adr.mode.dynamic.channel_steering",
- "mac_settings.adr.mode.dynamic.channel_steering.mode",
"mac_settings.adr.mode.dynamic.channel_steering.mode.disabled",
"mac_settings.adr.mode.dynamic.channel_steering.mode.lora_narrow",
+ "mac_settings.adr.mode.dynamic.channel_steering.mode",
+ "mac_settings.adr.mode.dynamic.channel_steering",
"mac_settings.adr.mode.dynamic.margin",
- "mac_settings.adr.mode.dynamic.max_data_rate_index",
"mac_settings.adr.mode.dynamic.max_data_rate_index.value",
+ "mac_settings.adr.mode.dynamic.max_data_rate_index",
"mac_settings.adr.mode.dynamic.max_nb_trans",
"mac_settings.adr.mode.dynamic.max_tx_power_index",
- "mac_settings.adr.mode.dynamic.min_data_rate_index",
"mac_settings.adr.mode.dynamic.min_data_rate_index.value",
+ "mac_settings.adr.mode.dynamic.min_data_rate_index",
"mac_settings.adr.mode.dynamic.min_nb_trans",
"mac_settings.adr.mode.dynamic.min_tx_power_index",
- "mac_settings.adr.mode.static",
+ "mac_settings.adr.mode.dynamic",
"mac_settings.adr.mode.static.data_rate_index",
"mac_settings.adr.mode.static.nb_trans",
"mac_settings.adr.mode.static.tx_power_index",
+ "mac_settings.adr.mode.static",
+ "mac_settings.adr.mode",
+ "mac_settings.adr",
}
dynamicADRSettingsFields = []string{
- "mac_settings.adr.mode.dynamic",
- "mac_settings.adr.mode.dynamic.channel_steering",
- "mac_settings.adr.mode.dynamic.channel_steering.mode",
"mac_settings.adr.mode.dynamic.channel_steering.mode.disabled",
"mac_settings.adr.mode.dynamic.channel_steering.mode.lora_narrow",
+ "mac_settings.adr.mode.dynamic.channel_steering.mode",
+ "mac_settings.adr.mode.dynamic.channel_steering",
"mac_settings.adr.mode.dynamic.margin",
"mac_settings.adr.mode.dynamic.max_data_rate_index.value",
"mac_settings.adr.mode.dynamic.max_nb_trans",
@@ -989,6 +988,7 @@ var (
"mac_settings.adr.mode.dynamic.min_data_rate_index.value",
"mac_settings.adr.mode.dynamic.min_nb_trans",
"mac_settings.adr.mode.dynamic.min_tx_power_index",
+ "mac_settings.adr.mode.dynamic",
}
)
@@ -1244,36 +1244,36 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
if st.HasSetField(
"frequency_plan_id",
"lorawan_phy_version",
- "mac_settings.adr",
- "mac_settings.adr.mode",
"mac_settings.adr.mode.disabled",
- "mac_settings.adr.mode.dynamic",
- "mac_settings.adr.mode.dynamic.channel_steering",
- "mac_settings.adr.mode.dynamic.channel_steering.mode",
"mac_settings.adr.mode.dynamic.channel_steering.mode.disabled",
"mac_settings.adr.mode.dynamic.channel_steering.mode.lora_narrow",
+ "mac_settings.adr.mode.dynamic.channel_steering.mode",
+ "mac_settings.adr.mode.dynamic.channel_steering",
"mac_settings.adr.mode.dynamic.margin",
- "mac_settings.adr.mode.dynamic.max_data_rate_index",
"mac_settings.adr.mode.dynamic.max_data_rate_index.value",
+ "mac_settings.adr.mode.dynamic.max_data_rate_index",
"mac_settings.adr.mode.dynamic.max_nb_trans",
"mac_settings.adr.mode.dynamic.max_tx_power_index",
- "mac_settings.adr.mode.dynamic.min_data_rate_index",
"mac_settings.adr.mode.dynamic.min_data_rate_index.value",
+ "mac_settings.adr.mode.dynamic.min_data_rate_index",
"mac_settings.adr.mode.dynamic.min_nb_trans",
"mac_settings.adr.mode.dynamic.min_tx_power_index",
- "mac_settings.adr.mode.static",
+ "mac_settings.adr.mode.dynamic",
"mac_settings.adr.mode.static.data_rate_index",
"mac_settings.adr.mode.static.nb_trans",
"mac_settings.adr.mode.static.tx_power_index",
+ "mac_settings.adr.mode.static",
+ "mac_settings.adr.mode",
+ "mac_settings.adr",
+ "mac_settings.desired_ping_slot_data_rate_index.value",
+ "mac_settings.desired_rx2_data_rate_index.value",
+ "mac_settings.downlink_dwell_time.value",
"mac_settings.factory_preset_frequencies",
+ "mac_settings.ping_slot_data_rate_index.value",
"mac_settings.ping_slot_frequency.value",
- "mac_settings.use_adr.value",
"mac_settings.rx2_data_rate_index.value",
- "mac_settings.desired_rx2_data_rate_index.value",
- "mac_settings.ping_slot_data_rate_index.value",
- "mac_settings.desired_ping_slot_data_rate_index.value",
"mac_settings.uplink_dwell_time.value",
- "mac_settings.downlink_dwell_time.value",
+ "mac_settings.use_adr.value",
"mac_state.current_parameters.adr_data_rate_index",
"mac_state.current_parameters.adr_tx_power_index",
"mac_state.current_parameters.channels",
@@ -1333,18 +1333,8 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
"frequency_plan_id",
"lorawan_phy_version",
)
-
- hasSetFieldWithFallback := func(field, fallbackField string) (fieldToRetrieve string, validate bool) {
- if st.HasSetField(field) {
- return field, true
- }
- return fallbackField, hasPHYUpdate
- }
hasSetField := func(field string) (fieldToRetrieve string, validate bool) {
- return hasSetFieldWithFallback(field, field)
- }
- hasSetADRField := func(field string) (fieldToRetrieve string, validate bool) {
- return hasSetFieldWithFallback(field, "mac_settings.adr.mode")
+ return field, st.HasSetField(field) || hasPHYUpdate
}
setFields := func(fields ...string) []string {
@@ -1449,7 +1439,7 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
return nil, err
}
}
- if field, validate := hasSetADRField("mac_settings.adr.mode.dynamic.max_data_rate_index.value"); validate {
+ if field, validate := hasSetField("mac_settings.adr.mode.dynamic.max_data_rate_index.value"); validate {
if err := st.WithField(func(dev *ttnpb.EndDevice) error {
return withPHY(func(phy *band.Band, _ *frequencyplans.FrequencyPlan) error {
if dev.GetMacSettings().GetAdr().GetDynamic().GetMaxDataRateIndex() == nil {
@@ -1468,7 +1458,7 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
return nil, err
}
}
- if field, validate := hasSetADRField("mac_settings.adr.mode.dynamic.min_data_rate_index.value"); validate {
+ if field, validate := hasSetField("mac_settings.adr.mode.dynamic.min_data_rate_index.value"); validate {
if err := st.WithField(func(dev *ttnpb.EndDevice) error {
return withPHY(func(phy *band.Band, _ *frequencyplans.FrequencyPlan) error {
if dev.GetMacSettings().GetAdr().GetDynamic().GetMinDataRateIndex() == nil {
@@ -1487,7 +1477,7 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
return nil, err
}
}
- if field, validate := hasSetADRField("mac_settings.adr.mode.dynamic.max_tx_power_index"); validate {
+ if field, validate := hasSetField("mac_settings.adr.mode.dynamic.max_tx_power_index"); validate {
if err := st.WithField(func(dev *ttnpb.EndDevice) error {
return withPHY(func(phy *band.Band, _ *frequencyplans.FrequencyPlan) error {
if dev.GetMacSettings().GetAdr().GetDynamic().GetMaxTxPowerIndex() == nil {
@@ -1504,7 +1494,7 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
return nil, err
}
}
- if field, validate := hasSetADRField("mac_settings.adr.mode.dynamic.min_tx_power_index"); validate {
+ if field, validate := hasSetField("mac_settings.adr.mode.dynamic.min_tx_power_index"); validate {
if err := st.WithField(func(dev *ttnpb.EndDevice) error {
return withPHY(func(phy *band.Band, _ *frequencyplans.FrequencyPlan) error {
if dev.GetMacSettings().GetAdr().GetDynamic().GetMinTxPowerIndex() == nil {
@@ -1544,7 +1534,7 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
return nil, err
}
}
- if field, validate := hasSetADRField("mac_settings.adr.mode.static.data_rate_index"); validate {
+ if field, validate := hasSetField("mac_settings.adr.mode.static.data_rate_index"); validate {
if err := st.WithField(func(dev *ttnpb.EndDevice) error {
return withPHY(func(phy *band.Band, _ *frequencyplans.FrequencyPlan) error {
if dev.GetMacSettings().GetAdr().GetStatic() == nil {
@@ -1562,7 +1552,7 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
return nil, err
}
}
- if field, validate := hasSetADRField("mac_settings.adr.mode.static.tx_power_index"); validate {
+ if field, validate := hasSetField("mac_settings.adr.mode.static.tx_power_index"); validate {
if err := st.WithField(func(dev *ttnpb.EndDevice) error {
return withPHY(func(phy *band.Band, _ *frequencyplans.FrequencyPlan) error {
if dev.GetMacSettings().GetAdr().GetStatic() == nil {
@@ -2682,7 +2672,14 @@ func (ns *NetworkServer) Set(ctx context.Context, req *ttnpb.SetEndDeviceRequest
}
var evt events.Event
- dev, ctx, err := ns.devices.SetByID(ctx, st.Device.Ids.ApplicationIds, st.Device.Ids.DeviceId, st.GetFields(), st.SetFunc(func(ctx context.Context, stored *ttnpb.EndDevice) error {
+ dev, ctx, err := ns.devices.SetByID(ctx, st.Device.Ids.ApplicationIds, st.Device.Ids.DeviceId, ttnpb.EndDeviceFieldPathsTopLevel, st.SetFunc(func(ctx context.Context, stored *ttnpb.EndDevice) error {
+ if nonZeroFields := ttnpb.NonZeroFields(stored, st.GetFields()...); len(nonZeroFields) > 0 {
+ newStored := &ttnpb.EndDevice{}
+ if err := newStored.SetFields(stored, nonZeroFields...); err != nil {
+ return err
+ }
+ stored = newStored
+ }
if hasSession {
macVersion := stored.GetMacState().GetLorawanVersion()
if stored.GetMacState() == nil && !st.HasSetField("mac_state") {
diff --git a/pkg/packetbrokeragent/agent.go b/pkg/packetbrokeragent/agent.go
index c8a99fced2..cb000bfc4b 100644
--- a/pkg/packetbrokeragent/agent.go
+++ b/pkg/packetbrokeragent/agent.go
@@ -178,7 +178,7 @@ func New(c *component.Component, conf *Config, opts ...Option) (*Agent, error) {
}
devAddrPrefix := types.DevAddrPrefix{
DevAddr: devAddr,
- Length: uint8(conf.NetID.IDBits()),
+ Length: uint8(32 - types.NwkAddrBits(conf.NetID)),
}
devAddrPrefixes = append(devAddrPrefixes, devAddrPrefix)
}
diff --git a/pkg/webui/account/views/app/index.js b/pkg/webui/account/views/app/index.js
index 8f4fd0adaa..284175f326 100644
--- a/pkg/webui/account/views/app/index.js
+++ b/pkg/webui/account/views/app/index.js
@@ -14,7 +14,15 @@
import { useSelector, useDispatch } from 'react-redux'
import React, { useEffect } from 'react'
-import { Routes, Route, BrowserRouter } from 'react-router-dom'
+import {
+ Routes,
+ Route,
+ BrowserRouter,
+ ScrollRestoration,
+ createBrowserRouter,
+ RouterProvider,
+ Outlet,
+} from 'react-router-dom'
import { Helmet } from 'react-helmet'
import { ToastContainer } from '@ttn-lw/components/toast'
@@ -27,7 +35,6 @@ import Header from '@account/containers/header'
import Landing from '@account/views/landing'
import Authorize from '@account/views/authorize'
-import PropTypes from '@ttn-lw/lib/prop-types'
import {
selectApplicationSiteName,
selectApplicationSiteTitle,
@@ -44,8 +51,34 @@ const siteTitle = selectApplicationSiteTitle()
const pageData = selectPageData()
const errorRender = error =>
{freqPlan}
frequency plan could not be loaded',
macVersion: 'LoRaWAN version',
@@ -459,6 +460,7 @@ export default defineMessages({
sessions: 'Sessions',
setLoRaCloudToken: 'Set LoRa Cloud token',
settings: 'Settings',
+ setup: 'Setup',
shareGatewayInfo: 'Share gateway information',
skipCryptoDescription: 'Skip decryption of uplink payloads and encryption of downlink payloads',
skipCryptoPlaceholder: 'Encryption/decryption disabled',
@@ -507,6 +509,7 @@ export default defineMessages({
uplinksReceived: 'Uplinks received',
uploadAnImage: 'Upload an image',
used: '{currentValue}/{maxValue} used',
+ useDefaultPolicy: 'Use default routing policy for this network',
user: 'User',
userAdd: 'Add user',
userDelete: 'Delete user',
diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json
index b1d61a2403..4ea7546a91 100644
--- a/pkg/webui/locales/en.json
+++ b/pkg/webui/locales/en.json
@@ -191,6 +191,7 @@
"console.components.application-general-settings-form.index.useAlcsync": "Use Application Layer Clock Synchronization",
"console.components.application-general-settings-form.index.adminContactDescription": "Administrative contact information for this application. Typically used to indicate who to contact with administrative questions about the application.",
"console.components.application-general-settings-form.index.techContactDescription": "Technical contact information for this application. Typically used to indicate who to contact with technical/security questions about the application.",
+ "console.components.default-routing-policy-form.index.doNotUseADefaultPolicy": "Do not use a default routing policy for this network",
"console.components.device-import-form.index.file": "File",
"console.components.device-import-form.index.formatInfo": "Format information",
"console.components.device-import-form.index.selectAFile": "Please select a template file",
@@ -304,7 +305,6 @@
"console.components.payload-formatters-form.index.grpcFieldDescription": "The address of the service to connect to",
"console.components.payload-formatters-form.index.appFormatter": "Use application payload formatter",
"console.components.payload-formatters-form.index.appFormatterWarning": "This option will affect both uplink and downlink formatter",
- "console.components.payload-formatters-form.index.setupSubTitle": "Setup",
"console.components.payload-formatters-form.index.defaultFormatter": "Click here to modify the default payload formatter for this application. The payload formatter of this application is currently set to `{defaultFormatter}`",
"console.components.payload-formatters-form.index.pasteRepositoryFormatter": "Paste repository formatter",
"console.components.payload-formatters-form.index.pasteApplicationFormatter": "Paste application formatter",
@@ -354,9 +354,7 @@
"console.components.pubsub-form.messages.useSecureConnection": "Use secure connection",
"console.components.pubsub-form.messages.pubsubsDescription": "The Pub/Sub integration allows the Application Server to publish and subscribe to topics, using The Things Stack built-in MQTT client or NATS client. Learn more in our Pub/Sub guide.",
"console.components.routing-policy-form.index.saveDefaultPolicy": "Save default policy",
- "console.components.routing-policy-form.index.useDefaultPolicy": "Use default routing policy for this network",
"console.components.routing-policy-form.index.useSpecificPolicy": "Use network specific routing policy",
- "console.components.routing-policy-form.index.doNotUseADefaultPolicy": "Do not use a default routing policy for this network",
"console.components.routing-policy-form.index.doNotUseAPolicy": "Do not use a routing policy for this network",
"console.components.uplink-form.uplink-form.simulateUplink": "Simulate uplink",
"console.components.uplink-form.uplink-form.payloadDescription": "The desired payload bytes of the uplink message",
@@ -640,7 +638,6 @@
"console.lib.packet-broker.messages.joinRequestDesc": "Forward join-request messages",
"console.lib.packet-broker.messages.localizationInformation": "Localization information",
"console.lib.packet-broker.messages.localizationInformationDesc": "Forward gateway location, RSSI, SNR and fine timestamp",
- "console.lib.packet-broker.messages.macData": "MAC data",
"console.lib.packet-broker.messages.macDataAllowDesc": "Allow downlink messages with FPort of 0",
"console.lib.packet-broker.messages.macDataDesc": "Forward uplink messages with FPort 0",
"console.lib.packet-broker.messages.signalQualityInformation": "Signal quality information",
@@ -677,26 +674,24 @@
"console.store.middleware.logics.users.errEmailValidationActionSuccess": "Validation email sent (please also check your spam folder)",
"console.store.middleware.logics.users.errEmailValidationActionFailure": "There was an error and the validation email could not be sent.",
"console.store.middleware.logics.users.errEmailValidationAlreadySent": "A validation email has already been sent recently to your email address. Please also check your spam folder.",
- "console.views.admin-packet-broker.messages.packetBrokerInfoText": "Packet Broker can be used to exchange traffic (peering) with other LoRaWAN networks to share coverage and improve the overall network performance.",
+ "console.views.admin-packet-broker.messages.packetBrokerInfoText": "Packet Broker is a service by The Things Industries to facilitate peering between LoRaWAN networks. This extends network coverage and improves overall network performance and device battery lifetime.",
"console.views.admin-packet-broker.messages.packetBrokerWebsite": "Packet Broker website",
- "console.views.admin-packet-broker.messages.registrationStatus": "Registration status",
- "console.views.admin-packet-broker.messages.registerNetwork": "Register network",
- "console.views.admin-packet-broker.messages.networkVisibility": "Network visibility",
- "console.views.admin-packet-broker.messages.packetBrokerRegistrationDesc": "To enable peering from or to your home network, it is necessary to register your network. This will make your network known to Packet Broker and enable you to configure your network's peering behavior.",
+ "console.views.admin-packet-broker.messages.learnMore": "Learn more",
+ "console.views.admin-packet-broker.messages.whyNetworkPeeringTitle": "Why choose network peering?",
+ "console.views.admin-packet-broker.messages.whyNetworkPeeringText": "Since LoRaWAN uses shared spectrum, gateways receive messages from devices registered on other LoRaWAN networks. Instead of discarding this traffic, these messages can be forwarded via Packet Broker to the home network of these devices. This extends coverage of networks and allows devices to use higher data rates that reduce channel utilization and increase battery life. No sensitive data is exposed as LoRaWAN is end-to-end encrypted and integrity protected.",
+ "console.views.admin-packet-broker.messages.enbaling": "Enable forwarding via the options below or define custom routing policies. In the Networks tab below (visible by selecting the option \"Use custom routing policies\"), you can see which other networks are forwarding data to this network.",
"console.views.admin-packet-broker.messages.packetBrokerDisabledDesc": "The Things Stack is not set up to use Packet Broker. Please refer to the documentation link above for instructions on how to set up The Things Stack for peering with Packet Broker.",
- "console.views.admin-packet-broker.messages.packetBrokerRegistrationDisabledDesc": "The Things Stack is set up to use Packet Broker, but security settings disallow (de)registering your network here. Please contact Packet Broker to manage your registration. Refer to the documentation link above for contact information.",
+ "console.views.admin-packet-broker.messages.enablePacketBroker": "Enable Packet Broker",
+ "console.views.admin-packet-broker.messages.packetBrokerRegistrationDesc": "Enabling will allow other networks to send traffic to you as well as you forwarding traffic to them, based on the exact routing policy.",
+ "console.views.admin-packet-broker.messages.routingConfig": "Routing configuration",
"console.views.admin-packet-broker.messages.network": "Network: {network}",
- "console.views.admin-packet-broker.messages.homeNetworkEnabled": "Home network enabled",
- "console.views.admin-packet-broker.messages.homeNetworkDisabled": "Home network disabled",
- "console.views.admin-packet-broker.messages.forwarderEnabled": "Forwarder enabled",
- "console.views.admin-packet-broker.messages.forwarderDisabled": "Forwarder disabled",
- "console.views.admin-packet-broker.messages.listNetwork": "List network publicly",
- "console.views.admin-packet-broker.messages.listNetworkDesc": "Listing your network allows other network administrators to see your network. This allows them to easily configure routing policies with your network.",
+ "console.views.admin-packet-broker.messages.listNetwork": "List my network in Packet Broker publicly",
+ "console.views.admin-packet-broker.messages.listNetworkDesc": "Public listing will make it easier for other network operators to set up routing policies for your network. Hence public listing is generally recommended.",
"console.views.admin-packet-broker.messages.unlistNetwork": "Unlist this network",
"console.views.admin-packet-broker.messages.confirmUnlist": "Confirm unlist",
"console.views.admin-packet-broker.messages.Are you sure you want to unlist your network in Packet Broker?{lineBreak}This will hide your network. Other network administrators will not be able to see your network to configure routing policies.": "Are you sure you want to unlist your network in Packet Broker?{lineBreak}This will hide your network. Other network administrators will not be able to see your network to configure routing policies.",
"console.views.admin-packet-broker.messages.routingPolicyInformation": "You can use the checkboxes below to control the default forwarding behavior of your network. You can additionally set up individual per-network routing policies via the Networks tab.",
- "console.views.admin-packet-broker.messages.defaultRoutingPolicySet": "Default routing policy set",
+ "console.views.admin-packet-broker.messages.defaultRoutingPolicySet": "Default routing configuration set",
"console.views.admin-packet-broker.messages.routingPolicySet": "Routing policy set",
"console.views.admin-packet-broker.messages.defaultRoutingPolicy": "Default routing policy",
"console.views.admin-packet-broker.messages.devAddressBlock": "Device address block",
@@ -791,7 +786,7 @@
"console.views.device-overview.index.latestData": "Latest data",
"console.views.device-overview.index.keysNotExposed": "Keys are not exposed",
"console.views.device-overview.index.failedAccessOtherHostDevice": "The end device you attempted to visit is registered on a different cluster and needs to be accessed using its host Console.",
- "console.views.device-overview.index.macData": "Download MAC data",
+ "console.views.device-overview.index.downloadMacData": "Download MAC data",
"console.views.device-overview.index.sensitiveDataWarning": "The MAC data can contain sensitive information such as session keys that can be used to decrypt messages. Do not share this information publicly.",
"console.views.device-overview.index.noSessionWarning": "The end device is currently not connected to the network (no active session). The MAC data will hence only contain the current MAC settings.",
"console.views.device-overview.index.macStateError": "There was an error and MAC state could not be included in the MAC data.",
@@ -1269,6 +1264,7 @@
"lib.shared-messages.lorawanInformation": "LoRaWAN information",
"lib.shared-messages.lorawanOptions": "LoRaWAN options",
"lib.shared-messages.lorawanPhyVersionDescription": "The LoRaWAN PHY version of the end device",
+ "lib.shared-messages.macData": "MAC data",
"lib.shared-messages.macSettingsError": "There was an error and the default MAC settings for the {freqPlan}
frequency plan could not be loaded",
"lib.shared-messages.macVersion": "LoRaWAN version",
"lib.shared-messages.messageTypes": "Message types",
@@ -1390,6 +1386,7 @@
"lib.shared-messages.sessions": "Sessions",
"lib.shared-messages.setLoRaCloudToken": "Set LoRa Cloud token",
"lib.shared-messages.settings": "Settings",
+ "lib.shared-messages.setup": "Setup",
"lib.shared-messages.shareGatewayInfo": "Share gateway information",
"lib.shared-messages.skipCryptoDescription": "Skip decryption of uplink payloads and encryption of downlink payloads",
"lib.shared-messages.skipCryptoPlaceholder": "Encryption/decryption disabled",
@@ -1438,6 +1435,7 @@
"lib.shared-messages.uplinksReceived": "Uplinks received",
"lib.shared-messages.uploadAnImage": "Upload an image",
"lib.shared-messages.used": "{currentValue}/{maxValue} used",
+ "lib.shared-messages.useDefaultPolicy": "Use default routing policy for this network",
"lib.shared-messages.user": "User",
"lib.shared-messages.userAdd": "Add user",
"lib.shared-messages.userDelete": "Delete user",
diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json
index bcac6340bf..869ac64aff 100644
--- a/pkg/webui/locales/ja.json
+++ b/pkg/webui/locales/ja.json
@@ -191,6 +191,7 @@
"console.components.application-general-settings-form.index.useAlcsync": "LoRaアプリケーションレイヤー・クロックシンクロパッケージの使用",
"console.components.application-general-settings-form.index.adminContactDescription": "このアプリケーションの管理者の連絡先情報。通常、アプリケーションに関する管理上の問い合わせ先を示すために使用されます。",
"console.components.application-general-settings-form.index.techContactDescription": "このアプリケーションの技術的な連絡先情報。通常、アプリケーションに関する技術/セキュリティの質問の連絡先を示すために使用されます。",
+ "console.components.default-routing-policy-form.index.doNotUseADefaultPolicy": "",
"console.components.device-import-form.index.file": "ファイル",
"console.components.device-import-form.index.formatInfo": "フォーマット情報",
"console.components.device-import-form.index.selectAFile": "テンプレートファイルを選択してください",
@@ -246,13 +247,13 @@
"console.components.events.messages.eventUnavailable": "このイベントはもう利用できません。メモリを節約するために切り捨てられたのでしょう",
"console.components.events.messages.verboseStream": "冗長ストリーム",
"console.components.events.messages.confirmedUplink": "アップリンク確認済み",
- "console.components.events.previews.shared.json-payload.index.invalid": "無効なJSON",
+ "console.components.events.previews.shared.json-payload.index.invalid": "",
"console.components.gateway-api-keys-modal.index.modalTitle": "ダウンロードゲートウェイAPIキー",
"console.components.gateway-api-keys-modal.index.buttonMessage": "キーをダウンロードしました",
"console.components.gateway-api-keys-modal.index.description": "注意:このウィンドウを閉じると、これらのAPIキーはダウンロードできなくなります。必ずダウンロードして保存してください!",
"console.components.gateway-api-keys-modal.index.downloadLns": "LNSキーをダウンロード",
"console.components.gateway-api-keys-modal.index.downloadCups": "CUPSキーをダウンロード",
- "console.components.gateway-visibility-form.index.saveDefaultGatewayVisibility": "デフォルトゲートウェイの可視性を保存",
+ "console.components.gateway-visibility-form.index.saveDefaultGatewayVisibility": "",
"console.components.location-form.index.deleteAllLocations": "すべての位置情報を削除",
"console.components.location-form.index.deleteFailure": "エラーが発生し、その場所を削除できませんでした",
"console.components.location-form.index.deleteLocation": "場所入力を削除",
@@ -304,7 +305,6 @@
"console.components.payload-formatters-form.index.grpcFieldDescription": "接続先サービスのアドレス",
"console.components.payload-formatters-form.index.appFormatter": "アプリケーションペイロードフォーマッタを使用",
"console.components.payload-formatters-form.index.appFormatterWarning": "このオプションは、アップリンクとダウンリンクの両方のフォーマッタをアプリケーションリンクのデフォルトに設定します",
- "console.components.payload-formatters-form.index.setupSubTitle": "セットアップ",
"console.components.payload-formatters-form.index.defaultFormatter": "ここ をクリックすると、このアプリケーションのデフォルトのペイロードフォーマッタを変更することができます。このアプリケーションのペイロードフォーマッタは現在 `{defaultFormatter}` に設定されています",
"console.components.payload-formatters-form.index.pasteRepositoryFormatter": "リポジトリフォーマッター貼り付け",
"console.components.payload-formatters-form.index.pasteApplicationFormatter": "アプリケーションフォーマッター貼り付け",
@@ -353,10 +353,8 @@
"console.components.pubsub-form.messages.replacePubsub": "Pub/Subを置き換え",
"console.components.pubsub-form.messages.useSecureConnection": "安全な接続を使用",
"console.components.pubsub-form.messages.pubsubsDescription": "Pub/Sub統合により、The Things Stack内蔵のMQTTクライアントまたはNATSクライアントを使用して、アプリケーションサーバーがトピックを公開および購読することができます。詳しくは、<リンク>Pub/Subガイドリンク>をご覧ください",
- "console.components.routing-policy-form.index.saveDefaultPolicy": "デフォルトポリシーの保存",
- "console.components.routing-policy-form.index.useDefaultPolicy": "このネットワークでデフォルトのルーティングポリシーを使用",
+ "console.components.routing-policy-form.index.saveDefaultPolicy": "",
"console.components.routing-policy-form.index.useSpecificPolicy": "ネットワーク固有のルーティングポリシーを使用",
- "console.components.routing-policy-form.index.doNotUseADefaultPolicy": "このネットワークでは、デフォルトのルーティングポリシーを使用しないでください",
"console.components.routing-policy-form.index.doNotUseAPolicy": "このネットワークには、ルーティングポリシーを使用しないでください",
"console.components.uplink-form.uplink-form.simulateUplink": "アップリンクのシミュレーション",
"console.components.uplink-form.uplink-form.payloadDescription": "アップリンクメッセージの希望するペイロードバイト数",
@@ -420,7 +418,7 @@
"console.containers.deployment-component-status.index.versionInfo": "デプロイメント",
"console.containers.deployment-component-status.index.statusPage": "ステータスページに移動します",
"console.containers.deployment-component-status.index.seeChangelog": "変更履歴を見る",
- "console.containers.dev-addr-input.index.devAddrFetchingFailure": "エラーが発生し、エンドデバイスのアドレスを生成できませんでした",
+ "console.containers.dev-addr-input.index.devAddrFetchingFailure": "",
"console.containers.dev-eui-component.index.unknownError": "",
"console.containers.device-importer.messages.proceed": "エンドデバイスリストへ進む",
"console.containers.device-importer.messages.retry": "ゼロからやり直す",
@@ -477,34 +475,34 @@
"console.containers.device-onboarding-form.messages.resetQRCodeData": "QRコードデータをリセット",
"console.containers.device-onboarding-form.messages.resetConfirm": "本当にQRコードデータを破棄していいのですか?スキャンした端末は登録されず、フォームがリセットされま",
"console.containers.device-onboarding-form.messages.scanSuccess": "QRコードの読み取りが成功しました",
- "console.containers.device-onboarding-form.provisioning-form-section.claiming-form-section.validation-schema.validateCode": "クレーム認証コードは数字と文字のみで構成されている必要があります",
- "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.advancedSectionTitle": "高度なアクティベーション、LoRaWANクラス、クラスタの設定を表示します",
- "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.classA": "なし(クラスAのみ)",
- "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.classB": "クラスB(ビーコン)",
- "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.classC": "クラスC(連続使用)",
- "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.classBandC": "クラスBとクラスC",
- "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.skipJsRegistration": "ジョインサーバーでの登録をスキップ",
- "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.multicastClassCapabilities": "マルチキャスト・ダウンリンク用LoRaWANクラス",
- "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.register": "手動で登録",
+ "console.containers.device-onboarding-form.provisioning-form-section.claiming-form-section.validation-schema.validateCode": "",
+ "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.advancedSectionTitle": "",
+ "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.classA": "",
+ "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.classB": "",
+ "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.classC": "",
+ "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.classBandC": "",
+ "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.skipJsRegistration": "",
+ "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.multicastClassCapabilities": "",
+ "console.containers.device-onboarding-form.type-form-section.manual-form-section.advanced-settings-section.register": "",
"console.containers.device-onboarding-form.warning-tooltip.desiredDescription": "The network will use a different desired value of {value}
for this property.",
"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": "クラスA",
- "console.containers.device-profile-section.device-card.device-card.classB": "クラスB",
- "console.containers.device-profile-section.device-card.device-card.classC": "クラスC",
+ "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-selection.band-select.index.title": "プロフィール(リージョン)",
- "console.containers.device-profile-section.device-selection.brand-select.brand-select.title": "エンドデバイスのブランド",
- "console.containers.device-profile-section.device-selection.brand-select.brand-select.noOptionsMessage": "該当するブランドはありません",
- "console.containers.device-profile-section.device-selection.fw-version-select.fw-version-select.title": "ファームウェアのバージョン",
- "console.containers.device-profile-section.device-selection.hw-version-select.hw-version-select.title": "ハードウェアのバージョン",
- "console.containers.device-profile-section.device-selection.model-select.model-select.noOptionsMessage": "該当する機種がありません",
+ "console.containers.device-profile-section.device-selection.brand-select.brand-select.title": "",
+ "console.containers.device-profile-section.device-selection.brand-select.brand-select.noOptionsMessage": "",
+ "console.containers.device-profile-section.device-selection.fw-version-select.fw-version-select.title": "",
+ "console.containers.device-profile-section.device-selection.hw-version-select.hw-version-select.title": "",
+ "console.containers.device-profile-section.device-selection.model-select.model-select.noOptionsMessage": "",
"console.containers.device-profile-section.hints.other-hint.hintTitle": "お客様のエンドデバイスはすぐに追加されます!",
"console.containers.device-profile-section.hints.other-hint.hintMessage": "申し訳ありませんが、あなたのデバイスはまだLoRaWANデバイスリポジトリの一部ではありません。エンドデバイスの製造元が提供する情報(製品のデータシートなど)を使用して、上記のenter end device specifics manuallyオプションを使用することができます。また、デバイスの追加に関するドキュメントも参照してください",
- "console.containers.device-profile-section.hints.progress-hint.hintMessage": "あなたの正確なエンドデバイスを見つけることができませんか? ここで助けを得て、上のオプションでエンドデバイスの仕様を手動で入力してみてください。",
- "console.containers.device-profile-section.hints.progress-hint.hintNoSupportMessage": "あなたの正確なエンドデバイスを見つけることができませんか? 上のオプションでエンドデバイスの仕様を手動で入力してみてください",
+ "console.containers.device-profile-section.hints.progress-hint.hintMessage": "",
+ "console.containers.device-profile-section.hints.progress-hint.hintNoSupportMessage": "",
"console.containers.device-template-format-select.index.title": "ファイルフォーマット",
"console.containers.device-template-format-select.index.warning": "エンドデバイスのテンプレートフォーマットが利用できません",
"console.containers.device-title-section.device-title-section.uplinkDownlinkTooltip": "前回のフレームカウンタリセット以降、このエンドデバイスの送信アップリンクと受信ダウンリンクの数です",
@@ -521,17 +519,17 @@
"console.containers.gateway-connection.gateway-connection.connectedTooltip": "このゲートウェイはゲートウェイサーバーに接続されていますが、ネットワークはまだゲートウェイからのアクティビティ(アップリンクやステータスメッセージの送信)を登録していません",
"console.containers.gateway-connection.gateway-connection.otherClusterTooltip": "このゲートウェイは、このクラスタのメッセージを処理しない外部のゲートウェイサーバーに接続されています。そのため、このゲートウェイからのアクティビティを見ることはできません",
"console.containers.gateway-connection.gateway-connection.messageCountTooltip": "最後の(再)接続以降、このゲートウェイの受信アップリンクと送信ダウンリンクの量です。ゲートウェイの種類によっては、頻繁に再接続するため、カウンタがリセットされることに注意してください",
- "console.containers.gateway-location-form.gateway-location-form.updateLocationFromStatus": "ステータスメッセージから更新",
- "console.containers.gateway-location-form.gateway-location-form.updateLocationFromStatusDescription": "受信ステータスメッセージに基づいて、このゲートウェイの位置を更新します",
- "console.containers.gateway-location-form.gateway-location-form.setGatewayLocation": "ゲートウェイアンテナ位置の設定",
- "console.containers.gateway-location-form.gateway-location-form.locationSource": "位置情報ソース",
- "console.containers.gateway-location-form.gateway-location-form.locationPrivacy": "位置情報プライバシー",
- "console.containers.gateway-location-form.gateway-location-form.placement": "配置",
- "console.containers.gateway-location-form.gateway-location-form.indoor": "屋内",
- "console.containers.gateway-location-form.gateway-location-form.outdoor": "屋外",
- "console.containers.gateway-location-form.gateway-location-form.locationFromStatusMessage": "ステータスメッセージから自動的に位置情報を設定",
- "console.containers.gateway-location-form.gateway-location-form.setLocationManually": "位置情報を手動で設定",
- "console.containers.gateway-location-form.gateway-location-form.noLocationSetInfo": "このゲートウェイには位置情報が設定されていません",
+ "console.containers.gateway-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-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": "",
@@ -564,36 +562,36 @@
"console.containers.move-away-modal.move-away-modal.modalMessage": "本当にこのままでよいのですか?あなたの現在の変更はまだ保存されていません",
"console.containers.network-information-container.index.openSourceInfo": "現在、The Things Stack Open Sourceを使用しています。The Things Stack Cloudを使用することで、さらなる機能を利用できます。",
"console.containers.network-information-container.index.plansButton": "The Things Stack Cloudを始めましょう",
- "console.containers.network-information-container.registry-totals.applicationsUsed": "使用アプリケーション",
- "console.containers.network-information-container.registry-totals.gatewaysUsed": "使用ゲートウェイ",
- "console.containers.network-information-container.registry-totals.registeredUsers": "登録ユーザー",
- "console.containers.network-information-container.registry-totals.endDevicesAdded": "エンドデバイスの追加",
- "console.containers.organization-form.form.orgDescPlaceholder": "新しい組織の説明",
- "console.containers.organization-form.form.orgDescDescription": "組織の説明(任意)。組織に関するメモを保存するためにも使用できます",
- "console.containers.organization-form.form.orgIdPlaceholder": "my-new-organization",
- "console.containers.organization-form.form.orgNamePlaceholder": "私の新組織",
- "console.containers.organization-form.form.adminContactDescription": "この組織の管理者連絡先情報。通常、組織に関する管理的な質問をするべき連絡先を示すために使用されます。",
- "console.containers.organization-form.form.techContactDescription": "この組織の技術連絡先情報。通常、組織に関する技術/セキュリティの質問をするべき連絡先を示すために使用されます。",
- "console.containers.organization-form.update.deleteOrg": "組織を削除",
- "console.containers.organization-form.update.updateSuccess": "組織が更新されました",
- "console.containers.organization-form.update.deleteSuccess": "削除された組織",
+ "console.containers.network-information-container.registry-totals.applicationsUsed": "",
+ "console.containers.network-information-container.registry-totals.gatewaysUsed": "",
+ "console.containers.network-information-container.registry-totals.registeredUsers": "",
+ "console.containers.network-information-container.registry-totals.endDevicesAdded": "",
+ "console.containers.organization-form.form.orgDescPlaceholder": "",
+ "console.containers.organization-form.form.orgDescDescription": "",
+ "console.containers.organization-form.form.orgIdPlaceholder": "",
+ "console.containers.organization-form.form.orgNamePlaceholder": "",
+ "console.containers.organization-form.form.adminContactDescription": "",
+ "console.containers.organization-form.form.techContactDescription": "",
+ "console.containers.organization-form.update.deleteOrg": "",
+ "console.containers.organization-form.update.updateSuccess": "",
+ "console.containers.organization-form.update.deleteSuccess": "",
"console.containers.organizations-table.index.restoreSuccess": "復元された組織",
"console.containers.organizations-table.index.restoreFail": "エラーが発生し、組織を復元することができませんでした",
"console.containers.organizations-table.index.purgeSuccess": "パージされた組織",
"console.containers.organizations-table.index.purgeFail": "エラーが発生したため、組織をパージすることができませんでした",
- "console.containers.owners-select.owners-select.title": "所有者",
- "console.containers.owners-select.owners-select.warning": "エラーが発生し、組織の一覧を表示できませんでした",
+ "console.containers.owners-select.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": "私たちに対する彼らのルーティングポリシー",
"console.containers.packet-broker-networks-table.index.homeNetworkPolicy": "それらに対する当社のルーティングポリシー",
"console.containers.pubsub-formats-select.index.warning": "Pub/Subフォーマットが無効です",
"console.containers.pubsubs-table.index.host": "サーバーホスト",
- "console.containers.user-data-form.edit.deleteWarning": "これにより、このアカウントは永久に削除され、再登録のためにユーザーIDおよび電子メールがロックされます。このユーザーが所有し、他の協力者がいない関連エンティティ(ゲートウェイ、アプリケーション、エンドデバイスなど)は接続不能となり、同じIDまたはEUIを持つエンティティを再び登録することはできなくなります。このようなエンティティを継続して使用する場合は、必ず新しいコラボレーターを割り当ててください",
- "console.containers.user-data-form.edit.purgeWarning": "これにより、このアカウントは永久に削除されます。このユーザーが所有し、他の協力者がいない関連エンティティ(ゲートウェイ、アプリケーション、エンドデバイスなど)は接続不能となり、同じIDまたはEUIを持つエンティティを再び登録することはできなくなります。このようなエンティティの使用を継続する場合は、必ず新しい共同作業者を割り当ててください",
- "console.containers.user-data-form.edit.deleteConfirmMessage": "確認のため、このユーザーのユーザーIDを入力してください",
- "console.containers.user-data-form.edit.updateSuccess": "ユーザーが更新しました",
- "console.containers.user-data-form.edit.deleteSuccess": "ユーザー削除",
+ "console.containers.user-data-form.edit.deleteWarning": "",
+ "console.containers.user-data-form.edit.purgeWarning": "",
+ "console.containers.user-data-form.edit.deleteConfirmMessage": "",
+ "console.containers.user-data-form.edit.updateSuccess": "",
+ "console.containers.user-data-form.edit.deleteSuccess": "",
"console.containers.users-table.index.invite": "ユーザーを招待",
"console.containers.users-table.index.revokeInvitation": "この招待を取り消し",
"console.containers.users-table.index.sentAt": "送信済み",
@@ -640,7 +638,6 @@
"console.lib.packet-broker.messages.joinRequestDesc": "ジョインリクエストメッセージの転送",
"console.lib.packet-broker.messages.localizationInformation": "ローカライズ情報",
"console.lib.packet-broker.messages.localizationInformationDesc": "フォワードゲートウェイの位置、RSSI、SNR、ファインタイムスタンプ",
- "console.lib.packet-broker.messages.macData": "MACデータ",
"console.lib.packet-broker.messages.macDataAllowDesc": "FPortが0であるダウンリンクメッセージを許可します",
"console.lib.packet-broker.messages.macDataDesc": "FPort 0 でアップリンクメッセージを転送します",
"console.lib.packet-broker.messages.signalQualityInformation": "信号品質に関する情報",
@@ -672,51 +669,49 @@
"console.lib.packet-broker.messages.gatewayStatusDescription": "ゲートウェイがオンラインかオフラインかを表示します",
"console.lib.packet-broker.messages.gatewayPacketRatesLabel": "パケットレート",
"console.lib.packet-broker.messages.gatewayPacketRatesDescription": "受信・送信パケットレート",
- "console.store.middleware.logics.packet-broker.unauthenticatedErrorTitle": "オーソライズできません",
- "console.store.middleware.logics.packet-broker.unauthenticatedErrorMessage": "コンソールは、Packet Brokerエージェントへのリクエストを認証することができません。The Things StackのPacket Broker機能が正しく設定されていることを確認してください。上記のドキュメントを参照することで、より詳細なガイダンスが得られます",
- "console.store.middleware.logics.users.errEmailValidationActionSuccess": "確認メールが送信されました(迷惑メールフォルダも確認してください)",
- "console.store.middleware.logics.users.errEmailValidationActionFailure": "エラーが発生し、確認メールを送信できませんでした。",
- "console.store.middleware.logics.users.errEmailValidationAlreadySent": "最近、あなたのメールアドレスに確認メールが送信されました。迷惑メールフォルダも確認してください。",
- "console.views.admin-packet-broker.messages.packetBrokerInfoText": "Packet Brokerは、他のLoRaWANネットワークとトラフィックを交換(ピアリング)することで、カバレッジを共有し、ネットワーク全体のパフォーマンスを向上させるために使用することができます",
- "console.views.admin-packet-broker.messages.packetBrokerWebsite": "Packer Brokerのサイト",
- "console.views.admin-packet-broker.messages.registrationStatus": "登録状況",
- "console.views.admin-packet-broker.messages.registerNetwork": "ネットワーク登録",
- "console.views.admin-packet-broker.messages.networkVisibility": "ネットワークの可視化",
- "console.views.admin-packet-broker.messages.packetBrokerRegistrationDesc": "ご自宅のネットワークから、またはご自宅のネットワークへのピアリングを可能にするには、ご自宅のネットワークを登録する必要があります。これにより、お客様のネットワークがPacket Brokerに認識され、お客様のネットワークのピアリング動作を設定することができます",
- "console.views.admin-packet-broker.messages.packetBrokerDisabledDesc": "The Things Stackは、Packet Brokerを使用するための設定はされていません。Packet BrokerとピアリングするためのThe Things Stackのセットアップ方法については、上記のドキュメントリンクを参照してください",
- "console.views.admin-packet-broker.messages.packetBrokerRegistrationDisabledDesc": "The Things Stackは、Packet Brokerを使用するように設定されていますが、セキュリティの設定により、ここでネットワークを登録することはできません(解除)。登録の管理は、Packet Brokerにお問い合わせください。連絡先については、上記のドキュメントリンクを参照してください",
- "console.views.admin-packet-broker.messages.network": "ネットワーク: {network}",
- "console.views.admin-packet-broker.messages.homeNetworkEnabled": "ホームネットワーク有効",
- "console.views.admin-packet-broker.messages.homeNetworkDisabled": "ホームネットワーク無効",
- "console.views.admin-packet-broker.messages.forwarderEnabled": "フォワ―ダ―ネットワーク 有効",
- "console.views.admin-packet-broker.messages.forwarderDisabled": "フォワ―ダ―ネットワーク 無効",
- "console.views.admin-packet-broker.messages.listNetwork": "ネットワーク公開で一覧表示",
- "console.views.admin-packet-broker.messages.listNetworkDesc": "ネットワークをリストアップすることで、他のネットワーク管理者があなたのネットワークを見ることができます。これにより、他のネットワーク管理者は、あなたのネットワークでルーティングポリシーを簡単に設定することができます",
- "console.views.admin-packet-broker.messages.unlistNetwork": "このネットワークをアンリスト化",
- "console.views.admin-packet-broker.messages.confirmUnlist": "アンリストを確認",
- "console.views.admin-packet-broker.messages.Are you sure you want to unlist your network in Packet Broker?{lineBreak}This will hide your network. Other network administrators will not be able to see your network to configure routing policies.": "PacketBrokerに登録されていないネットワークは、本当に登録されていないのでしょうか?他のネットワーク管理者は、ルーティングポリシーを設定するためにあなたのネットワークを見ることができなくなります",
- "console.views.admin-packet-broker.messages.routingPolicyInformation": "以下のチェックボックスを使用して、ネットワークのデフォルトの転送動作を制御することができます。さらに、[ネットワーク]タブでネットワークごとのルーティングポリシーを個別に設定することもできます",
- "console.views.admin-packet-broker.messages.defaultRoutingPolicySet": "デフォルトのルーティングポリシー設定",
- "console.views.admin-packet-broker.messages.routingPolicySet": "ルーティングポリシーセット",
- "console.views.admin-packet-broker.messages.defaultRoutingPolicy": "既定のルーティングポリシー",
- "console.views.admin-packet-broker.messages.devAddressBlock": "デバイスアドレスブロック",
- "console.views.admin-packet-broker.messages.devAddressBlocks": "デバイスアドレスブロック",
- "console.views.admin-packet-broker.messages.lastPolicyChange": "最後の方針変更",
- "console.views.admin-packet-broker.messages.networkId": "ネットワークID",
- "console.views.admin-packet-broker.messages.routingPolicyFromThisNetwork": "このネットワークの当社に対するルーティングポリシー",
- "console.views.admin-packet-broker.messages.routingPolicyToThisNetwork": "このネットワークに対するルーティングポリシーを設定します",
- "console.views.admin-packet-broker.messages.saveRoutingPolicy": "ルーティングポリシーの保存",
- "console.views.admin-packet-broker.messages.noPolicySet": "ポリシーはまだ決まっていません",
- "console.views.admin-packet-broker.messages.prefixes": "プレフィクス",
- "console.views.admin-packet-broker.messages.homeNetworkClusterId": "ホームネットワーククラスターID",
- "console.views.admin-packet-broker.messages.backToAllNetworks": "すべてのネットワークへ戻る",
- "console.views.admin-packet-broker.messages.deregisterNetwork": "このネットワークの登録解除",
- "console.views.admin-packet-broker.messages.confirmDeregister": "登録解除の確認",
- "console.views.admin-packet-broker.messages.Are you sure you want to deregister your network from Packet Broker?{lineBreak}This will permanently delete<.b> all routing policies and may stop traffic from flowing.{lineBreak}Traffic may still be forwarded to your network based on default routing policies configured by forwarders.": "Packet Brokerからネットワークの登録を解除してよろしいですか? {lineBreak}すべてのルーティングポリシーが永久に削除されるため、トラフィックが流れなくなる可能性があります。{lineBreak}フォワーダが設定したデフォルトのルーティングポリシーに基づき、トラフィックがネットワークに転送される可能性はあります",
- "console.views.admin-packet-broker.messages.defaultGatewayVisibility": "デフォルトゲートウェイの可視化",
- "console.views.admin-packet-broker.messages.gatewayVisibilityInformation": "チェックボックスを使用して、ゲートウェイの情報を表示するように制御することができます。この情報は、登録されたネットワークだけでなく、一般にも公開されることに注意してください",
- "console.views.admin-packet-broker.messages.defaultGatewayVisibilitySet": "デフォルトゲートウェイの可視化設定",
- "console.views.admin-packet-broker.messages.packetBrokerStatusPage": "Packet Broker ステータスページ",
+ "console.store.middleware.logics.packet-broker.unauthenticatedErrorTitle": "",
+ "console.store.middleware.logics.packet-broker.unauthenticatedErrorMessage": "",
+ "console.store.middleware.logics.users.errEmailValidationActionSuccess": "",
+ "console.store.middleware.logics.users.errEmailValidationActionFailure": "",
+ "console.store.middleware.logics.users.errEmailValidationAlreadySent": "",
+ "console.views.admin-packet-broker.messages.packetBrokerInfoText": "",
+ "console.views.admin-packet-broker.messages.packetBrokerWebsite": "",
+ "console.views.admin-packet-broker.messages.learnMore": "",
+ "console.views.admin-packet-broker.messages.whyNetworkPeeringTitle": "",
+ "console.views.admin-packet-broker.messages.whyNetworkPeeringText": "",
+ "console.views.admin-packet-broker.messages.enbaling": "",
+ "console.views.admin-packet-broker.messages.packetBrokerDisabledDesc": "",
+ "console.views.admin-packet-broker.messages.enablePacketBroker": "",
+ "console.views.admin-packet-broker.messages.packetBrokerRegistrationDesc": "",
+ "console.views.admin-packet-broker.messages.routingConfig": "",
+ "console.views.admin-packet-broker.messages.network": "",
+ "console.views.admin-packet-broker.messages.listNetwork": "",
+ "console.views.admin-packet-broker.messages.listNetworkDesc": "",
+ "console.views.admin-packet-broker.messages.unlistNetwork": "",
+ "console.views.admin-packet-broker.messages.confirmUnlist": "",
+ "console.views.admin-packet-broker.messages.Are you sure you want to unlist your network in Packet Broker?{lineBreak}This will hide your network. Other network administrators will not be able to see your network to configure routing policies.": "",
+ "console.views.admin-packet-broker.messages.routingPolicyInformation": "",
+ "console.views.admin-packet-broker.messages.defaultRoutingPolicySet": "",
+ "console.views.admin-packet-broker.messages.routingPolicySet": "",
+ "console.views.admin-packet-broker.messages.defaultRoutingPolicy": "",
+ "console.views.admin-packet-broker.messages.devAddressBlock": "",
+ "console.views.admin-packet-broker.messages.devAddressBlocks": "",
+ "console.views.admin-packet-broker.messages.lastPolicyChange": "",
+ "console.views.admin-packet-broker.messages.networkId": "",
+ "console.views.admin-packet-broker.messages.routingPolicyFromThisNetwork": "",
+ "console.views.admin-packet-broker.messages.routingPolicyToThisNetwork": "",
+ "console.views.admin-packet-broker.messages.saveRoutingPolicy": "",
+ "console.views.admin-packet-broker.messages.noPolicySet": "",
+ "console.views.admin-packet-broker.messages.prefixes": "",
+ "console.views.admin-packet-broker.messages.homeNetworkClusterId": "",
+ "console.views.admin-packet-broker.messages.backToAllNetworks": "",
+ "console.views.admin-packet-broker.messages.deregisterNetwork": "",
+ "console.views.admin-packet-broker.messages.confirmDeregister": "",
+ "console.views.admin-packet-broker.messages.Are you sure you want to deregister your network from Packet Broker?{lineBreak}This will permanently delete<.b> all routing policies and may stop traffic from flowing.{lineBreak}Traffic may still be forwarded to your network based on default routing policies configured by forwarders.": "",
+ "console.views.admin-packet-broker.messages.defaultGatewayVisibility": "",
+ "console.views.admin-packet-broker.messages.gatewayVisibilityInformation": "",
+ "console.views.admin-packet-broker.messages.defaultGatewayVisibilitySet": "",
+ "console.views.admin-packet-broker.messages.packetBrokerStatusPage": "",
"console.views.application-add.index.appDescription": "アプリケーション内では、エンドデバイスとそのネットワークデータを登録、管理することができます。デバイス群を設定した後、多くの統合オプションの1つを使用して、関連データを外部サービスに渡します。Adding Applications に関するガイドで詳しく説明しています",
"console.views.application-integrations-lora-cloud.index.loraCloudInfoText": "LoRaWANネットワークやLoRaベースのデバイスに関連する一般的なタスクのシンプルなソリューションを可能にする付加価値の高いAPIをローラクラウドが提供します。以下からLoRaCloudのインテグレーションを設定することができます",
"console.views.application-integrations-lora-cloud.index.officialLoRaCloudDocumentation": "オフィシャル LoRa Cloud ドキュメンテーション",
@@ -746,40 +741,40 @@
"console.views.device-general-settings.application-server-form.index.include": "ペイロード暗号の強制",
"console.views.device-general-settings.application-server-form.index.default": "アプリケーションのデフォルトを使用",
"console.views.device-general-settings.application-server-form.index.skipCryptoTitle": "ペイロード暗号の上書き",
- "console.views.device-general-settings.identity-server-form.index.unclaimAndDeleteDevice": "エンドデバイスのアンクレームと削除",
- "console.views.device-general-settings.identity-server-form.index.deleteDevice": "エンドデバイスの削除",
- "console.views.device-general-settings.identity-server-form.index.deleteWarning": "{deviceId} \"を削除してよろしいでしょうか?この操作は元に戻すことができず、終了したデバイスIDを再利用することはできません",
- "console.views.device-general-settings.messages.isTitle": "ベーシック",
- "console.views.device-general-settings.messages.isDescription": "説明、クラスタ情報、メタデータ",
- "console.views.device-general-settings.messages.isDescriptionMissing": "アイデンティティサーバーは利用できません",
- "console.views.device-general-settings.messages.asTitle": "アプリケーションレイヤー",
- "console.views.device-general-settings.messages.asDescription": "アプリケーションレイヤーの動作とセッション",
- "console.views.device-general-settings.messages.asDescriptionMissing": "アプリケーションサーバーは利用できません",
- "console.views.device-general-settings.messages.asDescriptionOTAA": "参加したOTAAエンドデバイスのキーのみがアプリケーションサーバに保存されます",
- "console.views.device-general-settings.messages.jsTitle": "Join設定",
- "console.views.device-general-settings.messages.jsDescription": "エンドデバイスのアクティベーションのためのルートキーとネットワーク設定",
- "console.views.device-general-settings.messages.jsDescriptionMissing": "Joinサーバーは利用できません",
- "console.views.device-general-settings.messages.jsDescriptionOTAA": "ABP/マルチキャストのエンドデバイスはJoinサーバーに保存されません",
- "console.views.device-general-settings.messages.nsTitle": "ネットワークレイヤー",
- "console.views.device-general-settings.messages.nsDescription": "LoRaWANネットワーク層の設定、動作、セッション",
- "console.views.device-general-settings.messages.nsDescriptionMissing": "ネットワークサーバーは利用できません",
- "console.views.device-general-settings.messages.deleteSuccess": "エンドデバイス削除",
- "console.views.device-general-settings.messages.deleteFailure": "エラーが発生し、エンドデバイスを削除できませんでした",
- "console.views.device-general-settings.messages.activationModeUnknown": "ネットワークサーバーが利用できないため、アクティベーションモードが不明です",
- "console.views.device-general-settings.messages.notInCluster": "このクラスタに登録されていません",
- "console.views.device-general-settings.messages.updateSuccess": "エンドデバイスの更新",
- "console.views.device-general-settings.messages.keysResetWarning": "エンドデバイスのキーの閲覧は禁止されていますが、上書きは許可されています",
- "console.views.device-general-settings.messages.unclaimFailure": "エラーが発生し、エンドデバイスをアンクレイムして削除することができませんでした",
- "console.views.device-general-settings.messages.validateSessionKey": "{field}は0以外の値でなければなりません",
- "console.views.device-general-settings.messages.resetUsedDevNonces": "使用したDevNoncesをリセット",
- "console.views.device-general-settings.messages.resetUsedDevNoncesModal": "{break}{break}使用済みDevNoncesをリセットすると、過去のNoncesを使用したリプレイアタックが可能になります。エンドデバイスのNVRAMをリセットしていない限り、このオプションは使用しないでください",
- "console.views.device-general-settings.messages.resetSuccess": "中古DevNoncesリセット",
- "console.views.device-general-settings.messages.resetFailure": "エラーが発生し、使用中のDevNoncesをリセットすることができませんでした",
- "console.views.device-general-settings.network-server-form.index.resetTitle": "セッションおよびMAC状態のリセット",
- "console.views.device-general-settings.network-server-form.index.resetButtonTitle": "セッションとMACの状態をリセット",
- "console.views.device-general-settings.network-server-form.index.resetSuccess": "エンドデバイスのリセット",
- "console.views.device-general-settings.network-server-form.index.resetFailure": "エラーが発生し、エンドデバイスのセッションとMACの状態をリセットすることができませんでした",
- "console.views.device-general-settings.network-server-form.index.modalMessage": "このエンドデバイスのセッションコンテキストとMACの状態をリセットしていいですか? これにより、以下のような影響があります:{entityId}を入力してください", + "lib.shared-messages.deleteModalDefaultMessage": "これにより、ENTITY自身と、共同研究者の関連付けを含むすべての関連するENTITYが永久に削除されます。また、ENTITY IDを再利用することもできなくなります", + "lib.shared-messages.deleteModalPurgeMessage": "これにより、そのエンティティ自身と、共同作業者の関連付けを含むすべての関連付けが永久に削除されます", + "lib.shared-messages.deleteModalPurgeWarning": "エンティティIDを公開すると、同じIDで新しいエンティティを登録することができるようになります。ただし、これは不可逆的であり、同じIDのエンティティを登録した場合、他のユーザーがそのエンティティの履歴データにアクセスできるようになる可能性があります。
{entityName}を削除しますか?", + "lib.shared-messages.deleted": "削除しました(管理者)", + "lib.shared-messages.description": "説明", + "lib.shared-messages.devAddr": "デバイスアドレス", + "lib.shared-messages.devDesc": "エンドデバイスの説明", + "lib.shared-messages.devEUI": "DevEUI", + "lib.shared-messages.devEUIBlockLimitReached": "DevEUI生成が限界到達", + "lib.shared-messages.devID": "エンドデバイスID", + "lib.shared-messages.devName": "エンドデバイス名", + "lib.shared-messages.device": "エンドデバイス", + "lib.shared-messages.deviceCounted": "{count, plural, one {エンドデバイス} other {エンドデバイスス}}", + "lib.shared-messages.deviceDescDescription": "エンドデバイスについてのメモを保存するために使用することもできます", + "lib.shared-messages.deviceDescPlaceholder": "私の新しいエンドデバイスの説明", "lib.shared-messages.deviceHardwareVersionAbsence": "", - "lib.shared-messages.deviceIdPlaceholder": "", - "lib.shared-messages.deviceNamePlaceholder": "", - "lib.shared-messages.deviceSimulationDisabledWarning": "", - "lib.shared-messages.devices": "", - "lib.shared-messages.disabled": "", - "lib.shared-messages.disconnected": "", - "lib.shared-messages.documentation": "", - "lib.shared-messages.downlink": "", - "lib.shared-messages.downlinkAck": "", - "lib.shared-messages.downlinkFailed": "", - "lib.shared-messages.downlinkFrameCount": "", - "lib.shared-messages.downlinkNack": "", - "lib.shared-messages.downlinkPush": "", - "lib.shared-messages.downlinkQueueInvalidated": "", - "lib.shared-messages.downlinkQueued": "", - "lib.shared-messages.downlinkReplace": "", - "lib.shared-messages.downlinkSent": "", - "lib.shared-messages.downlinksScheduled": "", - "lib.shared-messages.edit": "", - "lib.shared-messages.editWebhook": "", - "lib.shared-messages.email": "", - "lib.shared-messages.emailAddress": "", + "lib.shared-messages.deviceIdPlaceholder": "my-new-device", + "lib.shared-messages.deviceNamePlaceholder": "私の新しいエンドデバイス", + "lib.shared-messages.deviceSimulationDisabledWarning": "ペイロード暗号をスキップする機器では、シミュレーションは無効になります", + "lib.shared-messages.devices": "エンドデバイス", + "lib.shared-messages.disabled": "無効化", + "lib.shared-messages.disconnected": "切断されます", + "lib.shared-messages.documentation": "ドキュメンテーション", + "lib.shared-messages.downlink": "ダウンリンク", + "lib.shared-messages.downlinkAck": "ダウンリンク ACK", + "lib.shared-messages.downlinkFailed": "ダウンリンクに失敗しました。", + "lib.shared-messages.downlinkFrameCount": "ダウンリンクのフレームカウント", + "lib.shared-messages.downlinkNack": "裸のダウンリンク", + "lib.shared-messages.downlinkPush": "ダウンリンクプッシュ", + "lib.shared-messages.downlinkQueueInvalidated": "ダウンリンクキューが無効", + "lib.shared-messages.downlinkQueued": "ダウンリンクキュー", + "lib.shared-messages.downlinkReplace": "ダウンリンク交換", + "lib.shared-messages.downlinkSent": "ダウンリンク送信", + "lib.shared-messages.downlinksScheduled": "ダウンリンク(再)予定", + "lib.shared-messages.edit": "編集", + "lib.shared-messages.editWebhook": "Webhookの編集", + "lib.shared-messages.email": "Email", + "lib.shared-messages.emailAddress": "Emailアドレス", "lib.shared-messages.emailAddressDescription": "", "lib.shared-messages.emailAddressValidation": "", "lib.shared-messages.emailAddressValidationDescription": "", "lib.shared-messages.emailPlaceholder": "", - "lib.shared-messages.empty": "", - "lib.shared-messages.enabled": "", - "lib.shared-messages.endDeviceModelsUnavailable": "", - "lib.shared-messages.enforceDutyCycle": "", - "lib.shared-messages.enforceDutyCycleDescription": "", - "lib.shared-messages.entityId": "", - "lib.shared-messages.eventDownlinkAckDesc": "", - "lib.shared-messages.eventDownlinkFailedDesc": "", - "lib.shared-messages.eventDownlinkNackDesc": "", - "lib.shared-messages.eventDownlinkPushDesc": "", - "lib.shared-messages.eventDownlinkQueueInvalidatedDesc": "", - "lib.shared-messages.eventDownlinkQueuedDesc": "", - "lib.shared-messages.eventDownlinkReplaceDesc": "", - "lib.shared-messages.eventDownlinkSentDesc": "", - "lib.shared-messages.eventEnabledTypes": "", - "lib.shared-messages.eventJoinAcceptDesc": "", - "lib.shared-messages.eventLocationSolvedDesc": "", - "lib.shared-messages.eventServiceDataDesc": "", - "lib.shared-messages.eventUplinkMessageDesc": "", - "lib.shared-messages.eventUplinkNormalizedDesc": "", - "lib.shared-messages.eventsCannotShow": "", - "lib.shared-messages.expiry": "", - "lib.shared-messages.exportJson": "", - "lib.shared-messages.external": "", - "lib.shared-messages.externalJoinServer": "", + "lib.shared-messages.empty": "空の状態", + "lib.shared-messages.enabled": "有効化", + "lib.shared-messages.endDeviceModelsUnavailable": "エンドデバイスモデルが利用できません", + "lib.shared-messages.enforceDutyCycle": "デューティサイクルの強制", + "lib.shared-messages.enforceDutyCycleDescription": "スペクトラム規制を尊重するため、すべてのゲートウェイに推奨されています", + "lib.shared-messages.entityId": "エンティティID", + "lib.shared-messages.eventDownlinkAckDesc": "確認されたダウンリンクは、エンドデバイスによって確認されます", + "lib.shared-messages.eventDownlinkFailedDesc": "ダウンリンクは送信できません", + "lib.shared-messages.eventDownlinkNackDesc": "送信された確認済みダウンリンクが、エンドデバイスの確認に失敗した場合", + "lib.shared-messages.eventDownlinkPushDesc": "ダウンリンクがダウンリンクキューにプッシュされます", + "lib.shared-messages.eventDownlinkQueueInvalidatedDesc": "フレームカウンターの不一致によりダウンリンクキューがリセットされます", + "lib.shared-messages.eventDownlinkQueuedDesc": "ダウンリンクがダウンリンクキューに追加されます", + "lib.shared-messages.eventDownlinkReplaceDesc": "ダウンリンクは、ダウンリンクのキューを置き換えるために使用されます", + "lib.shared-messages.eventDownlinkSentDesc": "エンドデバイスまたはマルチキャストグループにダウンリンクが送信されます", + "lib.shared-messages.eventEnabledTypes": "有効なイベントタイプ", + "lib.shared-messages.eventJoinAcceptDesc": "エンドデバイスがネットワークに正常に参加し、セッションを開始した場合", + "lib.shared-messages.eventLocationSolvedDesc": "エンドデバイスの位置を特定することに成功したインテグレーション", + "lib.shared-messages.eventServiceDataDesc": "インテグレーションがイベントを発信します", + "lib.shared-messages.eventUplinkMessageDesc": "アップリンクメッセージをアプリケーションで受信した場合", + "lib.shared-messages.eventUplinkNormalizedDesc": "正規化されたアップリンクペイロード", + "lib.shared-messages.eventsCannotShow": "イベントを表示できません", + "lib.shared-messages.expiry": "期限切れ", + "lib.shared-messages.exportJson": "JSONとしてエクスポート", + "lib.shared-messages.external": "外部", + "lib.shared-messages.externalJoinServer": "外部Joinサーバー", "lib.shared-messages.fNwkSIntKey": "", "lib.shared-messages.factoryPresetFrequencies": "", - "lib.shared-messages.fetching": "", - "lib.shared-messages.firmwareVersion": "", - "lib.shared-messages.format": "", - "lib.shared-messages.fpNotFoundError": "", - "lib.shared-messages.frameCounterWidth": "", - "lib.shared-messages.freqAdd": "", - "lib.shared-messages.frequencyPlaceholder": "", - "lib.shared-messages.frequencyPlan": "", - "lib.shared-messages.frequencyPlanWarning": "", - "lib.shared-messages.furtherResources": "", - "lib.shared-messages.gateway": "", - "lib.shared-messages.gatewayDescDescription": "", - "lib.shared-messages.gatewayDescPlaceholder": "", - "lib.shared-messages.gatewayDescription": "", - "lib.shared-messages.gatewayEUI": "", - "lib.shared-messages.gatewayID": "", - "lib.shared-messages.gatewayIdPlaceholder": "", - "lib.shared-messages.gatewayLocation": "", - "lib.shared-messages.gatewayLocationPublic": "", - "lib.shared-messages.gatewayName": "", - "lib.shared-messages.gatewayNamePlaceholder": "", - "lib.shared-messages.gatewayScheduleDownlinkLate": "", - "lib.shared-messages.gatewayServerAddress": "", - "lib.shared-messages.gatewayStatus": "", - "lib.shared-messages.gatewayStatusPublic": "", - "lib.shared-messages.gatewayUpdateOptions": "", - "lib.shared-messages.gateways": "", - "lib.shared-messages.general": "", - "lib.shared-messages.generalInformation": "", - "lib.shared-messages.generalSettings": "", - "lib.shared-messages.generateAPIKeyCups": "", - "lib.shared-messages.generateAPIKeyLNS": "", - "lib.shared-messages.getSupport": "", - "lib.shared-messages.grantAdminStatus": "", - "lib.shared-messages.grpcService": "", - "lib.shared-messages.gsServerAddressDescription": "", - "lib.shared-messages.hardware": "", - "lib.shared-messages.hardwareVersion": "", - "lib.shared-messages.homeNetID": "", - "lib.shared-messages.homeNetIDDescription": "", - "lib.shared-messages.hours": "", - "lib.shared-messages.id": "", - "lib.shared-messages.idAlreadyExists": "", - "lib.shared-messages.import": "", - "lib.shared-messages.importDevices": "", - "lib.shared-messages.inputMethod": "", - "lib.shared-messages.insufficientAppKeyRights": "", - "lib.shared-messages.insufficientNwkKeyRights": "", - "lib.shared-messages.integrations": "", - "lib.shared-messages.invite": "", - "lib.shared-messages.joinAccept": "", - "lib.shared-messages.joinEUI": "", - "lib.shared-messages.joinServerAddress": "", - "lib.shared-messages.key": "", - "lib.shared-messages.keyEdit": "", - "lib.shared-messages.keyId": "", - "lib.shared-messages.lastSeen": "", - "lib.shared-messages.latitude": "", - "lib.shared-messages.latitudeDesc": "", - "lib.shared-messages.lbsLNSSecret": "", - "lib.shared-messages.lbsLNSSecretDescription": "", - "lib.shared-messages.link": "", - "lib.shared-messages.linked": "", - "lib.shared-messages.liveData": "", - "lib.shared-messages.location": "", - "lib.shared-messages.locationDescription": "", - "lib.shared-messages.locationMarkerDescriptionNonUser": "", - "lib.shared-messages.locationMarkerDescriptionUntrusted": "", - "lib.shared-messages.locationMarkerDescriptionUser": "", - "lib.shared-messages.locationSolved": "", - "lib.shared-messages.locationSourceBtRssi": "", - "lib.shared-messages.locationSourceCombined": "", - "lib.shared-messages.locationSourceGps": "", - "lib.shared-messages.locationSourceIpGeolocation": "", - "lib.shared-messages.locationSourceLoraRssi": "", - "lib.shared-messages.locationSourceLoraTdoa": "", - "lib.shared-messages.locationSourceRegistry": "", - "lib.shared-messages.locationSourceWifiRssi": "", - "lib.shared-messages.login": "", - "lib.shared-messages.loginFailed": "", - "lib.shared-messages.logout": "", - "lib.shared-messages.longitude": "", - "lib.shared-messages.longitudeDesc": "", - "lib.shared-messages.loraCloud": "", - "lib.shared-messages.loraCloudServerUrlDescription": "", - "lib.shared-messages.lorawanClassCapabilities": "", - "lib.shared-messages.lorawanInformation": "", - "lib.shared-messages.lorawanOptions": "", - "lib.shared-messages.lorawanPhyVersionDescription": "", - "lib.shared-messages.macSettingsError": "", - "lib.shared-messages.macVersion": "", - "lib.shared-messages.messageTypes": "", - "lib.shared-messages.messages": "", - "lib.shared-messages.messaging": "", - "lib.shared-messages.milliseconds": "", - "lib.shared-messages.minutes": "", - "lib.shared-messages.model": "", - "lib.shared-messages.moreInformation": "", - "lib.shared-messages.mqtt": "", - "lib.shared-messages.multicast": "", - "lib.shared-messages.name": "", - "lib.shared-messages.netId": "", + "lib.shared-messages.fetching": "データの取得...", + "lib.shared-messages.firmwareVersion": "ファームウェアバージョン", + "lib.shared-messages.format": "フォーマット", + "lib.shared-messages.fpNotFoundError": "指定された周波数プランは見つかりませんでした", + "lib.shared-messages.frameCounterWidth": "フレームカウンター幅", + "lib.shared-messages.freqAdd": "周波数を追加", + "lib.shared-messages.frequencyPlaceholder": "周波数(Hz)", + "lib.shared-messages.frequencyPlan": "周波数プラン", + "lib.shared-messages.frequencyPlanWarning": "周波数プランを選択しないと、ゲートウェイからのパケットは正しく処理されません", + "lib.shared-messages.furtherResources": "その他のリソース", + "lib.shared-messages.gateway": "ゲートウェイ", + "lib.shared-messages.gatewayDescDescription": "オプションのゲートウェイの説明;ゲートウェイについてのメモを保存するために使用することもできます", + "lib.shared-messages.gatewayDescPlaceholder": "新しいゲートウェイの説明", + "lib.shared-messages.gatewayDescription": "ゲートウェイの説明", + "lib.shared-messages.gatewayEUI": "ゲートウェイEUI", + "lib.shared-messages.gatewayID": "ゲートウェイID", + "lib.shared-messages.gatewayIdPlaceholder": "my-new-gateway", + "lib.shared-messages.gatewayLocation": "ゲートウェイの場所", + "lib.shared-messages.gatewayLocationPublic": "ネットワーク内での位置情報の共有", + "lib.shared-messages.gatewayName": "ゲートウェイ名", + "lib.shared-messages.gatewayNamePlaceholder": "私の新しいゲートウェイ", + "lib.shared-messages.gatewayScheduleDownlinkLate": "スケジュールダウンリンクの遅延", + "lib.shared-messages.gatewayServerAddress": "ゲートウェイサーバのアドレス", + "lib.shared-messages.gatewayStatus": "ゲートウェイの状態", + "lib.shared-messages.gatewayStatusPublic": "ネットワーク内でステータスを共有", + "lib.shared-messages.gatewayUpdateOptions": "ゲートウェイの更新", + "lib.shared-messages.gateways": "ゲートウェイ", + "lib.shared-messages.general": "一般", + "lib.shared-messages.generalInformation": "一般情報", + "lib.shared-messages.generalSettings": "一般設定", + "lib.shared-messages.generateAPIKeyCups": "CUPSのAPIキーを生成", + "lib.shared-messages.generateAPIKeyLNS": "LNSのAPIキーを生成", + "lib.shared-messages.getSupport": "サポートを受ける", + "lib.shared-messages.grantAdminStatus": "管理者権限を付与", + "lib.shared-messages.grpcService": "gRPCサービス", + "lib.shared-messages.gsServerAddressDescription": "接続先のゲートウェイサーバのアドレス", + "lib.shared-messages.hardware": "ハードウェア", + "lib.shared-messages.hardwareVersion": "ハードウェアバージョン", + "lib.shared-messages.homeNetID": "ホーム NetID", + "lib.shared-messages.homeNetIDDescription": "LoRaWANネットワークを識別するためのID", + "lib.shared-messages.hours": "時間", + "lib.shared-messages.id": "ID", + "lib.shared-messages.idAlreadyExists": "IDはすでに存在しています。", + "lib.shared-messages.import": "インポート", + "lib.shared-messages.importDevices": "エンドデバイスのインポート", + "lib.shared-messages.inputMethod": "入力方法", + "lib.shared-messages.insufficientAppKeyRights": "AppKeyを設定する権限が不足しています", + "lib.shared-messages.insufficientNwkKeyRights": "NwkKeyを設定する権限が不足しています", + "lib.shared-messages.integrations": "インテグレーション", + "lib.shared-messages.invite": "招待", + "lib.shared-messages.joinAccept": "Join受け入れ", + "lib.shared-messages.joinEUI": "JoinEUI", + "lib.shared-messages.joinServerAddress": "Joinサーバーアドレス", + "lib.shared-messages.key": "キー", + "lib.shared-messages.keyEdit": "APIキーの編集", + "lib.shared-messages.keyId": "キーID", + "lib.shared-messages.lastSeen": "最後に閲覧", + "lib.shared-messages.latitude": "緯度", + "lib.shared-messages.latitudeDesc": "南北の位置を度数で表したもので、0は赤道です", + "lib.shared-messages.lbsLNSSecret": "LoRa Basics Station LNS 認証キー", + "lib.shared-messages.lbsLNSSecretDescription": "LoRa Basics Station LNS 接続のための認証キーです。このフィールドは、他のゲートウェイでは無視されます", + "lib.shared-messages.link": "リンク", + "lib.shared-messages.linked": "リンク済", + "lib.shared-messages.liveData": "ライブデータ", + "lib.shared-messages.location": "場所", + "lib.shared-messages.locationDescription": "公開に設定すると、ゲートウェイの位置がネットワークの他のユーザーから見えることがあります", + "lib.shared-messages.locationMarkerDescriptionNonUser": "この位置は、このデバイスの受信メッセージから自動的に設定されました", + "lib.shared-messages.locationMarkerDescriptionUntrusted": "この位置は信頼できないステータスメッセージにより決定されたため、不正確である可能性があります", + "lib.shared-messages.locationMarkerDescriptionUser": "この場所は手動で設定されています(例:\"場所 \"タブを使用した場合)", + "lib.shared-messages.locationSolved": "解決済みの場所", + "lib.shared-messages.locationSourceBtRssi": "Bluetooth RSSIジオロケーション", + "lib.shared-messages.locationSourceCombined": "組み合わせたジオロケーション", + "lib.shared-messages.locationSourceGps": "GPSに基づく位置", + "lib.shared-messages.locationSourceIpGeolocation": "IPに基づくジオロケーション", + "lib.shared-messages.locationSourceLoraRssi": "LoRa RSSIジオロケーション", + "lib.shared-messages.locationSourceLoraTdoa": "LoRa TDOAジオロケーション", + "lib.shared-messages.locationSourceRegistry": "手動で設定された位置", + "lib.shared-messages.locationSourceWifiRssi": "Wifi RSSIジオロケーション", + "lib.shared-messages.login": "ログイン", + "lib.shared-messages.loginFailed": "ログインに失敗しました", + "lib.shared-messages.logout": "ログアウト", + "lib.shared-messages.longitude": "経度", + "lib.shared-messages.longitudeDesc": "東西の位置を度数で表したもので、0は主子午線(グリニッジ)です", + "lib.shared-messages.loraCloud": "LoRa Cloud", + "lib.shared-messages.loraCloudServerUrlDescription": "LoRa Cloud モデム・ジオロケーションサービスサーバーURL", + "lib.shared-messages.lorawanClassCapabilities": "LoRaWANのクラス機能", + "lib.shared-messages.lorawanInformation": "LoRaWAN情報", + "lib.shared-messages.lorawanOptions": "LoRaWANオプション", + "lib.shared-messages.lorawanPhyVersionDescription": "エンドデバイスのLoRaWAN PHYバージョン", + "lib.shared-messages.macData": "MACデータ", + "lib.shared-messages.macSettingsError": "MAC設定エラー", + "lib.shared-messages.macVersion": "LoRaWANバージョン", + "lib.shared-messages.messageTypes": "メッセージの種類", + "lib.shared-messages.messages": "メッセージ", + "lib.shared-messages.messaging": "メッセージング", + "lib.shared-messages.milliseconds": "ミリ秒", + "lib.shared-messages.minutes": "分", + "lib.shared-messages.model": "モデル", + "lib.shared-messages.moreInformation": "詳細情報", + "lib.shared-messages.mqtt": "MQTT", + "lib.shared-messages.multicast": "マルチキャスト", + "lib.shared-messages.name": "名前", + "lib.shared-messages.netId": "Net ID", "lib.shared-messages.networkInformation": "", - "lib.shared-messages.networkServerAddress": "", - "lib.shared-messages.networks": "", - "lib.shared-messages.never": "", - "lib.shared-messages.next": "", - "lib.shared-messages.noActivityYet": "", - "lib.shared-messages.noDesc": "", - "lib.shared-messages.noEvents": "", - "lib.shared-messages.noLocation": "", - "lib.shared-messages.noMatch": "", + "lib.shared-messages.networkServerAddress": "ネットワークサーバーアドレス", + "lib.shared-messages.networks": "ネットワーク", + "lib.shared-messages.never": "ネバー", + "lib.shared-messages.next": "次へ", + "lib.shared-messages.noActivityYet": "まだ活動はしていません", + "lib.shared-messages.noDesc": "このエンドデバイスには説明がありません", + "lib.shared-messages.noEvents": "
{entityId}からのイベント待ち...", + "lib.shared-messages.noLocation": "位置情報はありません", + "lib.shared-messages.noMatch": "アイテムが見つかりませんでした", "lib.shared-messages.noMatchingUserFound": "", - "lib.shared-messages.noRecentActivity": "", - "lib.shared-messages.none": "", - "lib.shared-messages.normalizedPayloadAir": "", - "lib.shared-messages.normalizedPayloadSoil": "", - "lib.shared-messages.normalizedPayloadWind": "", - "lib.shared-messages.notAvailable": "", - "lib.shared-messages.notLinked": "", - "lib.shared-messages.notSet": "", - "lib.shared-messages.nsEmptyDefault": "", - "lib.shared-messages.nsServerKekLabel": "", - "lib.shared-messages.nsServerKekLabelDescription": "", - "lib.shared-messages.nwkKey": "", - "lib.shared-messages.nwkSEncKey": "", - "lib.shared-messages.nwkSEncKeyDescription": "", - "lib.shared-messages.nwkSKey": "", - "lib.shared-messages.oauthClientAuthorizations": "", - "lib.shared-messages.oauthClientId": "", - "lib.shared-messages.oauthClients": "", - "lib.shared-messages.offline": "", - "lib.shared-messages.ok": "", - "lib.shared-messages.online": "", - "lib.shared-messages.options": "", - "lib.shared-messages.organization": "", - "lib.shared-messages.organizationId": "", - "lib.shared-messages.organizations": "", - "lib.shared-messages.otaa": "", - "lib.shared-messages.otherCluster": "", - "lib.shared-messages.otherOption": "", - "lib.shared-messages.overview": "", - "lib.shared-messages.packetBroker": "", - "lib.shared-messages.password": "", - "lib.shared-messages.passwordChanged": "", - "lib.shared-messages.pause": "", - "lib.shared-messages.payload": "", - "lib.shared-messages.payloadFormatters": "", - "lib.shared-messages.payloadFormattersDownlink": "", - "lib.shared-messages.payloadFormattersUpdateFailure": "", - "lib.shared-messages.payloadFormattersUpdateSuccess": "", - "lib.shared-messages.payloadFormattersUplink": "", - "lib.shared-messages.personalApiKeys": "", - "lib.shared-messages.phyVersion": "", - "lib.shared-messages.phyVersionDescription": "", + "lib.shared-messages.noRecentActivity": "最近の活動はありません", + "lib.shared-messages.none": "なし", + "lib.shared-messages.normalizedPayloadAir": "空気", + "lib.shared-messages.normalizedPayloadSoil": "土壌", + "lib.shared-messages.normalizedPayloadWind": "風", + "lib.shared-messages.notAvailable": "n/a", + "lib.shared-messages.notLinked": "リンクされていません", + "lib.shared-messages.notSet": "未設定", + "lib.shared-messages.nsEmptyDefault": "同じクラスタ内のネットワークサーバーにリンクするには空のままにします", + "lib.shared-messages.nsServerKekLabel": "ネットワークサーバーKEKラベル", + "lib.shared-messages.nsServerKekLabelDescription": "ネットワーク・セッション・キーのラッピングに使用するネットワーク・サーバの KEK ラベル", + "lib.shared-messages.nwkKey": "NwkKey", + "lib.shared-messages.nwkSEncKey": "NwkSEncKey", + "lib.shared-messages.nwkSEncKeyDescription": "ネットワークセッション暗号化キー", + "lib.shared-messages.nwkSKey": "NwkSKey", + "lib.shared-messages.oauthClientAuthorizations": "OAuthクライアント認証", + "lib.shared-messages.oauthClientId": "OAuth クライアント ID", + "lib.shared-messages.oauthClients": "OAuth クライアント", + "lib.shared-messages.offline": "オフライン", + "lib.shared-messages.ok": "OK", + "lib.shared-messages.online": "オンライン", + "lib.shared-messages.options": "オプション", + "lib.shared-messages.organization": "組織", + "lib.shared-messages.organizationId": "組織ID", + "lib.shared-messages.organizations": "組織", + "lib.shared-messages.otaa": "Over the air activation (OTAA)", + "lib.shared-messages.otherCluster": "他のクラスター", + "lib.shared-messages.otherOption": "その他…", + "lib.shared-messages.overview": "概要", + "lib.shared-messages.packetBroker": "Packet Broker", + "lib.shared-messages.password": "パスワード", + "lib.shared-messages.passwordChanged": "パスワード変更", + "lib.shared-messages.pause": "一時停止", + "lib.shared-messages.payload": "ペイロード", + "lib.shared-messages.payloadFormatters": "ペイロードフォーマッター", + "lib.shared-messages.payloadFormattersDownlink": "ダウンリンクペイロードフォーマッター", + "lib.shared-messages.payloadFormattersUpdateFailure": "ペイロードフォーマッターの更新にエラーがありました", + "lib.shared-messages.payloadFormattersUpdateSuccess": "ペイロードフォーマッターの更新", + "lib.shared-messages.payloadFormattersUplink": "アップリンクペイロードフォーマッター", + "lib.shared-messages.personalApiKeys": "個人用APIキー", + "lib.shared-messages.phyVersion": "地域パラメータのバージョン", + "lib.shared-messages.phyVersionDescription": "デバイスメーカーが提供する地域パラメータバージョン(PHY)", "lib.shared-messages.pingSlotFrequency": "", "lib.shared-messages.pingSlotPeriodicity": "", - "lib.shared-messages.port": "", - "lib.shared-messages.privacyPolicy": "", - "lib.shared-messages.profileSettings": "", - "lib.shared-messages.provider": "", - "lib.shared-messages.provisionedOnExternalJoinServer": "", - "lib.shared-messages.pubsubBaseTopic": "", - "lib.shared-messages.pubsubFormat": "", - "lib.shared-messages.pubsubId": "", - "lib.shared-messages.pubsubs": "", - "lib.shared-messages.purge": "", - "lib.shared-messages.redirecting": "", - "lib.shared-messages.refresh": "", - "lib.shared-messages.registerEndDevice": "", - "lib.shared-messages.registerGateway": "", + "lib.shared-messages.port": "ポート", + "lib.shared-messages.privacyPolicy": "個人情報保護方針", + "lib.shared-messages.profileSettings": "プロファイル設定", + "lib.shared-messages.provider": "プロバイダー", + "lib.shared-messages.provisionedOnExternalJoinServer": "外部のJoinサーバーでプロビジョニング", + "lib.shared-messages.pubsubBaseTopic": "基本トピック", + "lib.shared-messages.pubsubFormat": "Pub/Subフォーマット", + "lib.shared-messages.pubsubId": "Pub/Sub ID", + "lib.shared-messages.pubsubs": "Pub/Subs", + "lib.shared-messages.purge": "パージ(消去)", + "lib.shared-messages.redirecting": "リダイレクト中...", + "lib.shared-messages.refresh": "リフレッシュ", + "lib.shared-messages.registerEndDevice": "エンドデバイスの登録", + "lib.shared-messages.registerGateway": "ゲートウェイの登録", "lib.shared-messages.remove": "", - "lib.shared-messages.removeCollaborator": "", - "lib.shared-messages.removeCollaboratorLast": "", - "lib.shared-messages.removeCollaboratorSelf": "", - "lib.shared-messages.replaceWebhook": "", - "lib.shared-messages.requireAuthenticatedConnection": "", - "lib.shared-messages.requireAuthenticatedConnectionDescription": "", - "lib.shared-messages.reset": "", - "lib.shared-messages.resetWarning": "", - "lib.shared-messages.resetsFCnt": "", - "lib.shared-messages.resetsJoinNonces": "", - "lib.shared-messages.restartStream": "", - "lib.shared-messages.restore": "", - "lib.shared-messages.restrictedUser": "", - "lib.shared-messages.resume": "", - "lib.shared-messages.rights": "", - "lib.shared-messages.rootKeys": "", + "lib.shared-messages.removeCollaborator": "コラボレーターの削除", + "lib.shared-messages.removeCollaboratorLast": "最後のコラボレーターを削除できません", + "lib.shared-messages.removeCollaboratorSelf": "自分をコラボレーターとして外す", + "lib.shared-messages.replaceWebhook": "webhookの交換", + "lib.shared-messages.requireAuthenticatedConnection": "認証された接続を必要とします", + "lib.shared-messages.requireAuthenticatedConnectionDescription": "このゲートウェイが認証されたBasic StationまたはMQTT接続を使用する場合にのみ接続できるかどうかを制御します", + "lib.shared-messages.reset": "リセット", + "lib.shared-messages.resetWarning": "リセットは安全ではないため、エンドデバイスはリプレイ攻撃の影響を受けやすくなります", + "lib.shared-messages.resetsFCnt": "フレームカウンターのリセット", + "lib.shared-messages.resetsJoinNonces": "join noncesをリセットします", + "lib.shared-messages.restartStream": "ストリームの再起動", + "lib.shared-messages.restore": "復元", + "lib.shared-messages.restrictedUser": "自分自身を連絡先として設定することしかできません。別のコラボレーターを連絡先として設定したい場合は、そのコラボレーターに連絡して自己割り当てを依頼してください。", + "lib.shared-messages.resume": "履歴書", + "lib.shared-messages.rights": "著作権", + "lib.shared-messages.rootKeys": "ルートキー", "lib.shared-messages.rx1DataRateOffset": "", "lib.shared-messages.rx1Delay": "", "lib.shared-messages.rx2Frequency": "", - "lib.shared-messages.sNwkSIKey": "", - "lib.shared-messages.sNwkSIKeyDescription": "", - "lib.shared-messages.saveChanges": "", + "lib.shared-messages.sNwkSIKey": "SNwkSetKey", + "lib.shared-messages.sNwkSIKeyDescription": "ネットワークセッション整合性キーの提供", + "lib.shared-messages.saveChanges": "変更を保存", "lib.shared-messages.scanEndDevice": "", - "lib.shared-messages.scheduleAnyTimeDelay": "", - "lib.shared-messages.scheduleAnyTimeDescription": "", - "lib.shared-messages.scheduleDownlinkLateDescription": "", - "lib.shared-messages.search": "", + "lib.shared-messages.scheduleAnyTimeDelay": "任意の時間遅延をスケジュール", + "lib.shared-messages.scheduleAnyTimeDescription": "ゲートウェイ遅延の設定 (最小値: {minimumValue}ms, デフォルト値: {defaultValue}ms)", + "lib.shared-messages.scheduleDownlinkLateDescription": "ダウンリンクメッセージのサーバ側バッファを有効にします", + "lib.shared-messages.search": "検索", "lib.shared-messages.secondInterval": "", - "lib.shared-messages.seconds": "", - "lib.shared-messages.secondsAbbreviated": "", - "lib.shared-messages.secret": "", - "lib.shared-messages.secure": "", - "lib.shared-messages.sendInvitation": "", - "lib.shared-messages.serverUrl": "", - "lib.shared-messages.serviceData": "", - "lib.shared-messages.sessions": "", + "lib.shared-messages.seconds": "秒", + "lib.shared-messages.secondsAbbreviated": "sec", + "lib.shared-messages.secret": "秘密", + "lib.shared-messages.secure": "セキュア", + "lib.shared-messages.sendInvitation": "招待状送付", + "lib.shared-messages.serverUrl": "サーバーURL", + "lib.shared-messages.serviceData": "サービスデータ", + "lib.shared-messages.sessions": "セッション", "lib.shared-messages.setLoRaCloudToken": "", - "lib.shared-messages.settings": "", - "lib.shared-messages.shareGatewayInfo": "", - "lib.shared-messages.skipCryptoDescription": "", - "lib.shared-messages.skipCryptoPlaceholder": "", - "lib.shared-messages.skipCryptoTitle": "", - "lib.shared-messages.source": "", - "lib.shared-messages.stable": "", - "lib.shared-messages.state": "", - "lib.shared-messages.stateApproved": "", - "lib.shared-messages.stateDescription": "", - "lib.shared-messages.stateFlagged": "", - "lib.shared-messages.stateRejected": "", - "lib.shared-messages.stateRequested": "", - "lib.shared-messages.stateSuspended": "", - "lib.shared-messages.status": "", - "lib.shared-messages.statusDescription": "", - "lib.shared-messages.statusPage": "", - "lib.shared-messages.statusUnknown": "", - "lib.shared-messages.success": "", + "lib.shared-messages.settings": "設定", + "lib.shared-messages.setup": "", + "lib.shared-messages.shareGatewayInfo": "ゲートウェイ情報を共有", + "lib.shared-messages.skipCryptoDescription": "アップリンクペイロードの復号化とダウンリンクペイロードの暗号化をスキップします", + "lib.shared-messages.skipCryptoPlaceholder": "暗号化/復号化無効", + "lib.shared-messages.skipCryptoTitle": "ペイロードの暗号化と復号化をスキップ", + "lib.shared-messages.source": "ソース", + "lib.shared-messages.stable": "安定した", + "lib.shared-messages.state": "状態", + "lib.shared-messages.stateApproved": "承認済み", + "lib.shared-messages.stateDescription": "状態の説明", + "lib.shared-messages.stateFlagged": "フラグ付き", + "lib.shared-messages.stateRejected": "拒絶された", + "lib.shared-messages.stateRequested": "要求された", + "lib.shared-messages.stateSuspended": "中断された", + "lib.shared-messages.status": "ステータス", + "lib.shared-messages.statusDescription": "このゲートウェイの状態を公開することができます", + "lib.shared-messages.statusPage": "ステータスページ", + "lib.shared-messages.statusUnknown": "ステータス不明", + "lib.shared-messages.success": "成功", "lib.shared-messages.suggestions": "", - "lib.shared-messages.supportsClassB": "", - "lib.shared-messages.supportsClassC": "", - "lib.shared-messages.takeMeBack": "", - "lib.shared-messages.technicalContact": "", - "lib.shared-messages.tenantId": "", - "lib.shared-messages.termsAndCondition": "", - "lib.shared-messages.time": "", - "lib.shared-messages.token": "", - "lib.shared-messages.tokenDelete": "", - "lib.shared-messages.tokenDeleted": "", - "lib.shared-messages.tokenSet": "", - "lib.shared-messages.tokenUpdated": "", - "lib.shared-messages.traffic": "", - "lib.shared-messages.troubleshooting": "", - "lib.shared-messages.type": "", - "lib.shared-messages.typeToSearch": "", - "lib.shared-messages.unexposed": "", - "lib.shared-messages.unknown": "", - "lib.shared-messages.unknownError": "", - "lib.shared-messages.unknownHwOption": "", - "lib.shared-messages.updateChannelDescription": "", - "lib.shared-messages.updatedAt": "", - "lib.shared-messages.uplink": "", - "lib.shared-messages.uplinkFrameCount": "", - "lib.shared-messages.uplinkMessage": "", - "lib.shared-messages.uplinkNormalized": "", - "lib.shared-messages.uplinksReceived": "", + "lib.shared-messages.supportsClassB": "クラスBをサポート", + "lib.shared-messages.supportsClassC": "クラスCをサポート", + "lib.shared-messages.takeMeBack": "私を連れ戻して", + "lib.shared-messages.technicalContact": "技術連絡先", + "lib.shared-messages.tenantId": "テナントID", + "lib.shared-messages.termsAndCondition": "ご利用規約", + "lib.shared-messages.time": "時間", + "lib.shared-messages.token": "トークン", + "lib.shared-messages.tokenDelete": "トークン削除", + "lib.shared-messages.tokenDeleted": "削除されたトークン", + "lib.shared-messages.tokenSet": "トークンの設定", + "lib.shared-messages.tokenUpdated": "トークンの更新", + "lib.shared-messages.traffic": "トラフィック", + "lib.shared-messages.troubleshooting": "トラブルシューティング", + "lib.shared-messages.type": "タイプ", + "lib.shared-messages.typeToSearch": "検索に入力する…", + "lib.shared-messages.unexposed": "未公開", + "lib.shared-messages.unknown": "不明", + "lib.shared-messages.unknownError": "DevEUIの生成時に不明なエラーが発生", + "lib.shared-messages.unknownHwOption": "不明ver.", + "lib.shared-messages.updateChannelDescription": "ゲートウェイ自動更新のためのチャンネル", + "lib.shared-messages.updatedAt": "最終更新日", + "lib.shared-messages.uplink": "アップリンク", + "lib.shared-messages.uplinkFrameCount": "アップリンクフレーム数", + "lib.shared-messages.uplinkMessage": "アップリンクメッセージ", + "lib.shared-messages.uplinkNormalized": "正規化されたアップリンク", + "lib.shared-messages.uplinksReceived": "アップリンクを受信", "lib.shared-messages.uploadAnImage": "", - "lib.shared-messages.used": "", - "lib.shared-messages.user": "", - "lib.shared-messages.userAdd": "", - "lib.shared-messages.userDelete": "", + "lib.shared-messages.used": "{currentValue}/{maxValue} 使用", + "lib.shared-messages.useDefaultPolicy": "", + "lib.shared-messages.user": "ユーザー", + "lib.shared-messages.userAdd": "ユーザーを追加", + "lib.shared-messages.userDelete": "ユーザーを削除", "lib.shared-messages.userDescDescription": "", "lib.shared-messages.userDescription": "", - "lib.shared-messages.userEdit": "", - "lib.shared-messages.userId": "", + "lib.shared-messages.userEdit": "ユーザーを編集", + "lib.shared-messages.userId": "ユーザーID", "lib.shared-messages.userIdPlaceholder": "", - "lib.shared-messages.userInvitations": "", - "lib.shared-messages.userManagement": "", + "lib.shared-messages.userInvitations": "ユーザー招待", + "lib.shared-messages.userManagement": "ユーザー管理", "lib.shared-messages.userNamePlaceholder": "", "lib.shared-messages.userOrgId": "", - "lib.shared-messages.username": "", - "lib.shared-messages.users": "", - "lib.shared-messages.validFrom": "", - "lib.shared-messages.validTo": "", - "lib.shared-messages.validateAddressFormat": "", - "lib.shared-messages.validateApiKey": "", - "lib.shared-messages.validateDateInPast": "", - "lib.shared-messages.validateDelayFormat": "", - "lib.shared-messages.validateDigit": "", - "lib.shared-messages.validateEmail": "", - "lib.shared-messages.validateFreqDynamic": "", - "lib.shared-messages.validateFreqNumeric": "", - "lib.shared-messages.validateFreqRequired": "", - "lib.shared-messages.validateHexLength": "", - "lib.shared-messages.validateIdFormat": "", - "lib.shared-messages.validateInt32": "", - "lib.shared-messages.validateJson": "", - "lib.shared-messages.validateLatitude": "", - "lib.shared-messages.validateLength": "", - "lib.shared-messages.validateLongitude": "", - "lib.shared-messages.validateMacAddressEntered": "", - "lib.shared-messages.validateMqttPassword": "", - "lib.shared-messages.validateMqttUrl": "", - "lib.shared-messages.validateNoSpaces": "", - "lib.shared-messages.validateNumberGte": "", - "lib.shared-messages.validateNumberLte": "", - "lib.shared-messages.validatePasswordMatch": "", - "lib.shared-messages.validateRequired": "", - "lib.shared-messages.validateRights": "", - "lib.shared-messages.validateSpecial": "", - "lib.shared-messages.validateTooLong": "", - "lib.shared-messages.validateTooShort": "", - "lib.shared-messages.validateUppercase": "", - "lib.shared-messages.validateUrl": "", - "lib.shared-messages.value": "", - "lib.shared-messages.webhookActivated": "", - "lib.shared-messages.webhookAlreadyExistsModalMessage": "", - "lib.shared-messages.webhookBaseUrl": "", - "lib.shared-messages.webhookDeleted": "", - "lib.shared-messages.webhookFormat": "", - "lib.shared-messages.webhookId": "", + "lib.shared-messages.username": "ユーザー名", + "lib.shared-messages.users": "ユーザー", + "lib.shared-messages.validFrom": "から有効です", + "lib.shared-messages.validTo": "に有効です", + "lib.shared-messages.validateAddressFormat": "{field} は \"host \"または \"host:port \"の形式でなければなりません", + "lib.shared-messages.validateApiKey": "API キーは、\"NNSXS.[...].[.... ]\"の形式に従う必要があります", + "lib.shared-messages.validateDateInPast": "{field}は未来の日付でなければなりません", + "lib.shared-messages.validateDelayFormat": "{field} は正の整数でなければなりません。", + "lib.shared-messages.validateDigit": "{field} は少なくとも{digit} {digit, multiple, one {digit} other {digits}}を持っている必要があります", + "lib.shared-messages.validateEmail": "メールアドレスには、「@」と「.」を1つだけ使用し、特殊文字は使用しないでください", + "lib.shared-messages.validateFreqDynamic": "ダイナミック周波数が100000Hz以上の場合、{field}は0でなければなりません", + "lib.shared-messages.validateFreqNumeric": "周波数の値はすべて正の整数でなければなりません", + "lib.shared-messages.validateFreqRequired": "すべての周波数の値が必要です。空の項目は削除してください", + "lib.shared-messages.validateHexLength": "{field} は完全な16進数の値でなければなりません", + "lib.shared-messages.validateIdFormat": "{field} には小文字、数字、ダッシュ(-)のみを含める必要があります", + "lib.shared-messages.validateInt32": "{field}は整数、負または正でなければなりません", + "lib.shared-messages.validateJson": "{field}は有効なJSONオブジェクトでなければなりません", + "lib.shared-messages.validateLatitude": "緯度は-90から90までの整数または10進数でなければなりません", + "lib.shared-messages.validateLength": "{field} は正確に{length}文字の長さでなければなりません", + "lib.shared-messages.validateLongitude": "経度は-180から180までの整数または小数でなければなりません", + "lib.shared-messages.validateMacAddressEntered": "MACアドレスを入力してください", + "lib.shared-messages.validateMqttPassword": "{field}は空か、少なくとも2文字以上でなければなりません", + "lib.shared-messages.validateMqttUrl": "MQTTのURLは \"mqtt[s]://[username][:password]@host.domain[:port]\"という形式でなければなりません", + "lib.shared-messages.validateNoSpaces": "{field} にはスペースを入れてはいけません", + "lib.shared-messages.validateNumberGte": "{field} は少なくとも{min}以上でなければなりません", + "lib.shared-messages.validateNumberLte": "{field} は{max}以下でなければなりません", + "lib.shared-messages.validatePasswordMatch": "パスワードは一致している必要があります", + "lib.shared-messages.validateRequired": "{field}が必要です", + "lib.shared-messages.validateRights": "少なくとも1つの権利を選択する必要があります。", + "lib.shared-messages.validateSpecial": "{field} は少なくとも{special}special{special,plural, one {character} other {characters}}を持っている必要があります", + "lib.shared-messages.validateTooLong": "{field}の文字数は{max}以下でなければなりません", + "lib.shared-messages.validateTooShort": "{field} には少なくとも{min}文字が必要です", + "lib.shared-messages.validateUppercase": "{field} は、少なくとも {upper} uppercase {upper, plural, one {character} other {characters}}を使用しなければなりません", + "lib.shared-messages.validateUrl": "有効なURL形式でなければならず、スペースや特殊文字は含まれていません", + "lib.shared-messages.value": "値", + "lib.shared-messages.webhookActivated": "Webhookがアクティブになりました", + "lib.shared-messages.webhookAlreadyExistsModalMessage": "ID \"{id}\" を持つwebhookが既に存在します。このウェブフックを置き換えますか?", + "lib.shared-messages.webhookBaseUrl": "ベースURL", + "lib.shared-messages.webhookDeleted": "Webhookが削除されました", + "lib.shared-messages.webhookFormat": "Webhook フォーマット", + "lib.shared-messages.webhookId": "Webhook ID", "lib.shared-messages.webhookUpdated": "", - "lib.shared-messages.webhooks": "", + "lib.shared-messages.webhooks": "Webhooks", "enum:CHANNEL_MASKS": "チャネルマスク", "enum:CID_ADR_PARAM_SETUP": "ADRパラメータ", "enum:CID_BEACON_FREQ": "ビーコン周波数", @@ -1883,6 +1881,10 @@ "error:pkg/config:format": "無効なフォーマット `{input}`", "error:pkg/config:missing_blob_config": "Blobストア設定が見つかりません", "error:pkg/config:unknown_blob_provider": "無効なBlobストアプロバイダ `{provider}`", + "error:pkg/console/internal/events/protocol:message_type": "", + "error:pkg/console/internal/events/subscriptions:already_subscribed": "", + "error:pkg/console/internal/events/subscriptions:no_identifiers": "", + "error:pkg/console/internal/events/subscriptions:not_subscribed": "", "error:pkg/crypto/cryptoservices:no_app_key": "指定されたAppKeyがありません", "error:pkg/crypto/cryptoservices:no_dev_eui": "指定されたDevEUIがありません", "error:pkg/crypto/cryptoservices:no_join_eui": "指定されたJoinEUIがありません", @@ -1980,13 +1982,13 @@ "error:pkg/errors:x509_certificate_invalid": "証明書が無効", "error:pkg/errors:x509_hostname": "証明書の承認された名前が要求された名前と一致しません", "error:pkg/errors:x509_unknown_authority": "不明の証明書発行機関", - "error:pkg/events/grpc:invalid_regexp": "無効な正規表現", "error:pkg/events/grpc:no_identifiers": "識別子がありません", - "error:pkg/events/grpc:no_matching_events": "正規表現`{regexp}`に一致するイベントがありません", "error:pkg/events/grpc:storage_disabled": "イベントストレージが有効になっていません", - "error:pkg/events/grpc:unknown_event_name": "不明なイベント`{name}`", "error:pkg/events/redis:channel_closed": "チャネルが閉じています", "error:pkg/events/redis:unknown_encoding": "不明なエンコーディング", + "error:pkg/events:invalid_regexp": "無効な正規表現", + "error:pkg/events:no_matching_events": "正規表現`{regexp}`に一致するイベントがありません", + "error:pkg/events:unknown_event_name": "不明なイベント`{name}`", "error:pkg/fetch:fetch_file": "ファイル `{filename}` を取得できません", "error:pkg/fetch:file_not_found": "ファイル `{filename}` が見つかりません", "error:pkg/fetch:filename_not_specified": "ファイル名が特定されません", diff --git a/tools/go.mod b/tools/go.mod index b06d50ec53..d450416177 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -248,7 +248,7 @@ require ( google.golang.org/genproto v0.0.0-20230911183012-2d3300fd4832 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230911183012-2d3300fd4832 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230911183012-2d3300fd4832 // indirect - google.golang.org/grpc v1.58.2 // indirect + google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -257,4 +257,5 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.90.1 // indirect mellium.im/sasl v0.3.1 // indirect + nhooyr.io/websocket v1.8.10 // indirect ) diff --git a/tools/go.sum b/tools/go.sum index f621452ef8..7984c18fb3 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -576,7 +576,7 @@ github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOl github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt/v2 v2.5.2 h1:DhGH+nKt+wIkDxM6qnVSKjokq5t59AZV5HRcFW0zJwU= -github.com/nats-io/nats-server/v2 v2.10.1 h1:MIJ614dhOIdo71iSzY8ln78miXwrYvlvXHUyS+XdKZQ= +github.com/nats-io/nats-server/v2 v2.10.2 h1:2o/OOyc/dxeMCQtrF1V/9er0SU0A3LKhDlv/+rqreBM= github.com/nats-io/nats.go v1.30.2 h1:aloM0TGpPorZKQhbAkdCzYDj+ZmsJDyeo3Gkbr72NuY= github.com/nats-io/nats.go v1.30.2/go.mod h1:dcfhUgmQNN4GJEfIb2f9R7Fow+gzBF4emzDHrVBd5qM= github.com/nats-io/nkeys v0.4.5 h1:Zdz2BUlFm4fJlierwvGK+yl20IAKUm7eV6AAZXEhkPk= @@ -1228,8 +1228,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= -google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/grpc/examples v0.0.0-20210424002626-9572fd6faeae/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1290,6 +1290,8 @@ k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= +nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= +nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=