diff --git a/pkg/webui/components/map/widget/index.js b/pkg/webui/components/map/widget/index.js index 418379a4e4..84b5145f0b 100644 --- a/pkg/webui/components/map/widget/index.js +++ b/pkg/webui/components/map/widget/index.js @@ -39,7 +39,7 @@ const Map = ({ id, markers, setupLocationLink }) => { : undefined return ( -
+
{markers.length > 0 ? ( span + display: none + +.status + color: var(--c-text-neutral-semilight) + display: flex + align-items: center + white-space: nowrap diff --git a/pkg/webui/console/containers/application-overview-header/index.js b/pkg/webui/console/containers/application-overview-header/index.js new file mode 100644 index 0000000000..298287a0cf --- /dev/null +++ b/pkg/webui/console/containers/application-overview-header/index.js @@ -0,0 +1,212 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React, { useCallback, useMemo } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { defineMessages } from 'react-intl' +import classnames from 'classnames' + +import Icon, { + IconBroadcast, + IconCalendarMonth, + IconMenu2, + IconStar, + IconStarFilled, + IconCpu, +} from '@ttn-lw/components/icon' +import Button from '@ttn-lw/components/button' +import toast from '@ttn-lw/components/toast' +import DocTooltip from '@ttn-lw/components/tooltip/doc' +import Status from '@ttn-lw/components/status' + +import Message from '@ttn-lw/lib/components/message' +import RequireRequest from '@ttn-lw/lib/components/require-request' + +import LastSeen from '@console/components/last-seen' + +import sharedMessages from '@ttn-lw/lib/shared-messages' +import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' +import { selectFetchingEntry } from '@ttn-lw/lib/store/selectors/fetching' + +import { checkFromState, mayViewApplicationDevices } from '@console/lib/feature-checks' + +import { + ADD_BOOKMARK_BASE, + addBookmark, + DELETE_BOOKMARK_BASE, + deleteBookmark, +} from '@console/store/actions/user-preferences' +import { getApplicationDeviceCount } from '@console/store/actions/applications' + +import { selectUser } from '@console/store/selectors/logout' +import { selectBookmarksList } from '@console/store/selectors/user-preferences' +import { + selectApplicationDerivedLastSeen, + selectApplicationDeviceCount, + selectSelectedApplication, +} from '@console/store/selectors/applications' + +import style from './application-overview-header.styl' + +const m = defineMessages({ + addBookmarkFail: 'There was an error and the application could not be bookmarked', + removeBookmarkFail: 'There was an error and the application could not be removed from bookmarks', + lastSeenAvailableTooltip: + 'The elapsed time since the network registered activity (sent uplinks, confirmed downlinks or (re)join requests) of the end device(s) in this application.', + noActivityTooltip: + 'The network has not recently registered any activity (sent uplinks, confirmed downlinks or (re)join requests) of the end device(s) in this application.', + deviceCount: '{devices} End devices', +}) + +const ApplicationOverviewHeader = () => { + const dispatch = useDispatch() + const { name, created_at, ids } = useSelector(selectSelectedApplication) + const { application_id } = ids + const user = useSelector(selectUser) + const bookmarks = useSelector(selectBookmarksList) + const addBookmarkLoading = useSelector(state => selectFetchingEntry(state, ADD_BOOKMARK_BASE)) + const deleteBookmarkLoading = useSelector(state => + selectFetchingEntry(state, DELETE_BOOKMARK_BASE), + ) + const mayViewDevices = useSelector(state => checkFromState(mayViewApplicationDevices, state)) + const devicesTotalCount = useSelector(state => + selectApplicationDeviceCount(state, application_id), + ) + const lastSeen = useSelector(state => selectApplicationDerivedLastSeen(state, application_id)) + + const showLastSeen = Boolean(lastSeen) + + const isBookmarked = useMemo( + () => + bookmarks + .map(b => b.entity_ids?.application_ids?.application_id) + .some(b => b === application_id), + [bookmarks, application_id], + ) + + const handleAddToBookmark = useCallback(async () => { + try { + if (!isBookmarked) { + await dispatch(attachPromise(addBookmark(user.ids.user_id, { application_ids: ids }))) + return + } + await dispatch( + attachPromise( + deleteBookmark(user.ids.user_id, { + name: 'application', + id: application_id, + }), + ), + ) + } catch (e) { + toast({ + title: application_id, + message: isBookmarked ? m.removeBookmarkFail : m.addBookmarkFail, + type: toast.types.ERROR, + }) + } + }, [application_id, dispatch, ids, isBookmarked, user.ids.user_id]) + + const recentActivity = useMemo(() => { + let node + if (showLastSeen) { + node = ( + } + > + + + ) + } else { + node = ( + } + docPath="/getting-started/console/troubleshooting" + > + + + ) + } + return ( +
+ + {node} +
+ ) + }, [lastSeen, showLastSeen]) + + const menuDropdownItems = <> + + return ( +
+
+
{name || application_id}
+ + + {application_id} + +
+
+ {recentActivity} + {mayViewDevices && ( + +
+ + +
+
+ )} +
+ + +
+
+
+
+
+
+ ) +} + +export default ApplicationOverviewHeader diff --git a/pkg/webui/console/containers/application-title-section/index.js b/pkg/webui/console/containers/application-title-section/index.js deleted file mode 100644 index 0ac08adc0a..0000000000 --- a/pkg/webui/console/containers/application-title-section/index.js +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React, { useCallback } from 'react' -import { defineMessages } from 'react-intl' -import { useSelector } from 'react-redux' - -import applicationIcon from '@assets/misc/application.svg' - -import Icon, { IconHelp, IconApiKeys, IconCollaborators, IconDevice } from '@ttn-lw/components/icon' -import Status from '@ttn-lw/components/status' -import DocTooltip from '@ttn-lw/components/tooltip/doc' - -import Message from '@ttn-lw/lib/components/message' -import RequireRequest from '@ttn-lw/lib/components/require-request' - -import LastSeen from '@console/components/last-seen' -import EntityTitleSection from '@console/components/entity-title-section' - -import sharedMessages from '@ttn-lw/lib/shared-messages' -import { selectCollaboratorsTotalCount } from '@ttn-lw/lib/store/selectors/collaborators' -import { getCollaboratorsList } from '@ttn-lw/lib/store/actions/collaborators' - -import { - checkFromState, - mayViewOrEditApplicationApiKeys, - mayViewOrEditApplicationCollaborators, - mayViewApplicationDevices, -} from '@console/lib/feature-checks' - -import { getApiKeysList } from '@console/store/actions/api-keys' -import { getApplicationDeviceCount } from '@console/store/actions/applications' - -import { - selectApplicationDeviceCount, - selectApplicationDerivedLastSeen, - selectSelectedApplication, - selectSelectedApplicationId, -} from '@console/store/selectors/applications' -import { selectApiKeysTotalCount } from '@console/store/selectors/api-keys' - -const m = defineMessages({ - lastSeenAvailableTooltip: - 'The elapsed time since the network registered activity (sent uplinks, confirmed downlinks or (re)join requests) of the end device(s) in this application.', - noActivityTooltip: - 'The network has not recently registered any activity (sent uplinks, confirmed downlinks or (re)join requests) of the end device(s) in this application.', -}) - -const { Content } = EntityTitleSection - -const ApplicationTitleSection = () => { - const application = useSelector(selectSelectedApplication) - const appId = useSelector(selectSelectedApplicationId) - const apiKeysTotalCount = useSelector(selectApiKeysTotalCount) - const collaboratorsTotalCount = useSelector(state => - selectCollaboratorsTotalCount(state, { id: appId }), - ) - const devicesTotalCount = useSelector(state => selectApplicationDeviceCount(state, appId)) - const lastSeen = useSelector(state => selectApplicationDerivedLastSeen(state, appId)) - const mayViewCollaborators = useSelector(state => - checkFromState(mayViewOrEditApplicationCollaborators, state), - ) - const mayViewApiKeys = useSelector(state => - checkFromState(mayViewOrEditApplicationApiKeys, state), - ) - const mayViewDevices = useSelector(state => checkFromState(mayViewApplicationDevices, state)) - - const showLastSeen = Boolean(lastSeen) - - const bottomBarLeft = showLastSeen ? ( - } - > - - - - - ) : ( - } - docPath="/getting-started/console/troubleshooting" - > - - - - - ) - const bottomBarRight = ( - <> - {mayViewDevices && ( - - )} - {mayViewCollaborators && ( - - )} - {mayViewApiKeys && ( - - )} - - ) - - const loadData = useCallback( - async dispatch => { - if (mayViewCollaborators) { - dispatch(getCollaboratorsList('application', appId)) - } - - if (mayViewApiKeys) { - dispatch(getApiKeysList('application', appId)) - } - - if (mayViewDevices) { - dispatch(getApplicationDeviceCount(appId)) - } - }, - [appId, mayViewApiKeys, mayViewCollaborators, mayViewDevices], - ) - - return ( - - - - - - ) -} - -export default ApplicationTitleSection diff --git a/pkg/webui/console/containers/gateway-overview-header/gateway-overview-header.styl b/pkg/webui/console/containers/gateway-overview-header/gateway-overview-header.styl index 02c124bf69..33da13f8c3 100644 --- a/pkg/webui/console/containers/gateway-overview-header/gateway-overview-header.styl +++ b/pkg/webui/console/containers/gateway-overview-header/gateway-overview-header.styl @@ -15,7 +15,7 @@ .root position: sticky top: 0 - z-index: calc($zi.dropdown - 1) + z-index: $zi.dropdown - 1 display: flex justify-content: space-between gap: $cs.xl diff --git a/pkg/webui/console/containers/gateway-title-section/index.js b/pkg/webui/console/containers/gateway-title-section/index.js deleted file mode 100644 index 8a919478fe..0000000000 --- a/pkg/webui/console/containers/gateway-title-section/index.js +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React, { useCallback } from 'react' -import { useSelector } from 'react-redux' - -import gatewayIcon from '@assets/misc/gateway.svg' - -import { IconApiKeys, IconCollaborators } from '@ttn-lw/components/icon' - -import RequireRequest from '@ttn-lw/lib/components/require-request' - -import EntityTitleSection from '@console/components/entity-title-section' - -import GatewayConnection from '@console/containers/gateway-connection' - -import sharedMessages from '@ttn-lw/lib/shared-messages' -import { selectCollaboratorsTotalCount } from '@ttn-lw/lib/store/selectors/collaborators' -import { getCollaboratorsList } from '@ttn-lw/lib/store/actions/collaborators' - -import { - checkFromState, - mayViewOrEditGatewayApiKeys, - mayViewOrEditGatewayCollaborators, -} from '@console/lib/feature-checks' - -import { getApiKeysList } from '@console/store/actions/api-keys' - -import { selectApiKeysTotalCount } from '@console/store/selectors/api-keys' -import { selectSelectedGateway, selectSelectedGatewayId } from '@console/store/selectors/gateways' - -const { Content } = EntityTitleSection - -const GatewayTitleSection = () => { - const gateway = useSelector(selectSelectedGateway) - const gtwId = useSelector(selectSelectedGatewayId) - const apiKeysTotalCount = useSelector(selectApiKeysTotalCount) - const collaboratorsTotalCount = useSelector(state => - selectCollaboratorsTotalCount(state, { id: gtwId }), - ) - const mayViewCollaborators = useSelector(state => - checkFromState(mayViewOrEditGatewayCollaborators, state), - ) - const mayViewApiKeys = useSelector(state => checkFromState(mayViewOrEditGatewayApiKeys, state)) - - const bottomBarLeft = - const bottomBarRight = ( - <> - {mayViewCollaborators && ( - - )} - {mayViewApiKeys && ( - - )} - - ) - - const loadData = useCallback( - async dispatch => { - if (mayViewCollaborators) { - dispatch(getCollaboratorsList('gateway', gtwId)) - } - - if (mayViewApiKeys) { - dispatch(getApiKeysList('gateway', gtwId)) - } - }, - [gtwId, mayViewApiKeys, mayViewCollaborators], - ) - - return ( - - - - - - ) -} - -export default GatewayTitleSection diff --git a/pkg/webui/console/store/reducers/user-preferences.js b/pkg/webui/console/store/reducers/user-preferences.js index e104f5c41a..b40b017caf 100644 --- a/pkg/webui/console/store/reducers/user-preferences.js +++ b/pkg/webui/console/store/reducers/user-preferences.js @@ -100,7 +100,7 @@ const userPreferences = (state = initialState, { type, payload }) => { bookmarks: { ...state.bookmarks, bookmarks: [...state.bookmarks.bookmarks].filter( - b => b.entity_ids[`${payload.name}_ids`][`${payload.name}_id`] !== payload.id, + b => b.entity_ids?.[`${payload.name}_ids`]?.[`${payload.name}_id`] !== payload.id, ), totalCount: { ...state.bookmarks.totalCount, diff --git a/pkg/webui/console/views/application-overview/index.js b/pkg/webui/console/views/application-overview/index.js index 539d5a706e..06f21b5d16 100644 --- a/pkg/webui/console/views/application-overview/index.js +++ b/pkg/webui/console/views/application-overview/index.js @@ -1,4 +1,4 @@ -// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2024 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. @@ -14,19 +14,13 @@ import React from 'react' import { defineMessages } from 'react-intl' -import { useParams } from 'react-router-dom' import { useSelector } from 'react-redux' -import { PAGE_SIZES } from '@ttn-lw/constants/page-sizes' - -import DataSheet from '@ttn-lw/components/data-sheet' - -import DateTime from '@ttn-lw/lib/components/date-time' import IntlHelmet from '@ttn-lw/lib/components/intl-helmet' -import DevicesTable from '@console/containers/devices-table' -import ApplicationEvents from '@console/containers/application-events' -import ApplicationTitleSection from '@console/containers/application-title-section' +import BlurryNetworkActivityPanel from '@console/components/blurry-network-activity-panel' + +import ApplicationOverviewHeader from '@console/containers/application-overview-header' import Require from '@console/lib/components/require' @@ -38,32 +32,17 @@ import { checkFromState } from '@account/lib/feature-checks' import { selectSelectedApplication } from '@console/store/selectors/applications' -import style from './application-overview.styl' - const m = defineMessages({ failedAccessOtherHostApplication: 'The application you attempted to visit is registered on a different cluster and needs to be accessed using its host Console.', }) const ApplicationOverview = () => { - const { appId } = useParams() const application = useSelector(selectSelectedApplication) const may = useSelector(state => checkFromState(mayViewApplicationInfo, state)) - const { created_at, updated_at } = application const shouldRedirect = isOtherClusterApp(application) const condition = !shouldRedirect && may - const sheetData = [ - { - header: sharedMessages.generalInformation, - items: [ - { key: sharedMessages.appId, value: appId, type: 'code', sensitive: false }, - { key: sharedMessages.createdAt, value: }, - { key: sharedMessages.updatedAt, value: }, - ], - }, - ] - const otherwise = { redirect: '/applications', message: m.failedAccessOtherHostApplication, @@ -71,21 +50,20 @@ const ApplicationOverview = () => { return ( -
-
- - + + +
+
+
-
-
-
- +
+
-
- +
+
-
- +
+
diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 7d1bd11152..1dca6bc962 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -452,14 +452,17 @@ "console.containers.app-status-badge.index.maintenanceScheduledSubtitle": "Maintenance scheduled in less than 1 hour", "console.containers.application-general-settings.index.updateSuccess": "Application updated", "console.containers.application-general-settings.index.deleteSuccess": "Application deleted", + "console.containers.application-overview-header.index.addBookmarkFail": "There was an error and the application could not be bookmarked", + "console.containers.application-overview-header.index.removeBookmarkFail": "There was an error and the application could not be removed from bookmarks", + "console.containers.application-overview-header.index.lastSeenAvailableTooltip": "The elapsed time since the network registered activity (sent uplinks, confirmed downlinks or (re)join requests) of the end device(s) in this application.", + "console.containers.application-overview-header.index.noActivityTooltip": "The network has not recently registered any activity (sent uplinks, confirmed downlinks or (re)join requests) of the end device(s) in this application.", + "console.containers.application-overview-header.index.deviceCount": "{devices} End devices", "console.containers.application-payload-formatters.downlink.title": "Default downlink payload formatter", "console.containers.application-payload-formatters.downlink.infoText": "You can use the \"Payload formatter\" tab of individual end devices to test downlink payload formatters and to define individual payload formatter settings per end device.", "console.containers.application-payload-formatters.downlink.downlinkResetWarning": "You do not have sufficient rights to view the current downlink payload formatter. Only overwriting is allowed.", "console.containers.application-payload-formatters.uplink.title": "Default uplink payload formatter", "console.containers.application-payload-formatters.uplink.infoText": "You can use the \"Payload formatter\" tab of individual end devices to test uplink payload formatters and to define individual payload formatter settings per end device.", "console.containers.application-payload-formatters.uplink.uplinkResetWarning": "You do not have sufficient rights to view the current uplink payload formatter. Only overwriting is allowed.", - "console.containers.application-title-section.index.lastSeenAvailableTooltip": "The elapsed time since the network registered activity (sent uplinks, confirmed downlinks or (re)join requests) of the end device(s) in this application.", - "console.containers.application-title-section.index.noActivityTooltip": "The network has not recently registered any activity (sent uplinks, confirmed downlinks or (re)join requests) of the end device(s) in this application.", "console.containers.applications-form.index.applicationName": "Application name", "console.containers.applications-form.index.appIdPlaceholder": "my-new-application", "console.containers.applications-form.index.appNamePlaceholder": "My new application", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 84faa0a197..e781958bc0 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -452,14 +452,17 @@ "console.containers.app-status-badge.index.maintenanceScheduledSubtitle": "", "console.containers.application-general-settings.index.updateSuccess": "アプリケーションを更新", "console.containers.application-general-settings.index.deleteSuccess": "アプリケーションを削除", + "console.containers.application-overview-header.index.addBookmarkFail": "", + "console.containers.application-overview-header.index.removeBookmarkFail": "", + "console.containers.application-overview-header.index.lastSeenAvailableTooltip": "", + "console.containers.application-overview-header.index.noActivityTooltip": "", + "console.containers.application-overview-header.index.deviceCount": "", "console.containers.application-payload-formatters.downlink.title": "デフォルトのダウンリンクペイロードフォーマッター", "console.containers.application-payload-formatters.downlink.infoText": "個々のエンドデバイスの「Payload formatter」タブを使用して、ダウンリンクのペイロードフォーマッタをテストし、エンドデバイスごとに個別のペイロードフォーマッタ設定を定義することができます", "console.containers.application-payload-formatters.downlink.downlinkResetWarning": "現在のダウンリンクペイロードフォーマッターを表示するのに十分な権限がありません。上書きのみ許可されています", "console.containers.application-payload-formatters.uplink.title": "デフォルトのアップリンクペイロードフォーマッター", "console.containers.application-payload-formatters.uplink.infoText": "これらのペイロードフォーマッタは、このアプリケーション内のすべてのエンドデバイスからのアップリンクメッセージで実行されます。注意:エンドデバイスレベルのペイロードフォーマッタが優先されます", "console.containers.application-payload-formatters.uplink.uplinkResetWarning": "個々のエンドデバイスの「Payload formatter」タブを使用して、アップリンクのペイロードフォーマッタをテストし、エンドデバイスごとに個別のペイロードフォーマッタ設定を定義することができます", - "console.containers.application-title-section.index.lastSeenAvailableTooltip": "本アプリケーションにおいて、エンドデバイスのネットワーク登録アクティビティ(アップリンク送信、ダウンリンク確認、(再)参加要求)からの経過時間", - "console.containers.application-title-section.index.noActivityTooltip": "ネットワークは、このアプリケーションのエンドデバイスのアクティビティ(アップリンクの送信、ダウンリンクの確認、(再)参加要求)を最近登録していません", "console.containers.applications-form.index.applicationName": "アプリケーション名", "console.containers.applications-form.index.appIdPlaceholder": "my-new-application", "console.containers.applications-form.index.appNamePlaceholder": "私の新しいアプリケーション",