From 3c45789a4a0e99e4f7b221140393343713cdb46e Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Apr 2024 10:54:59 +0200 Subject: [PATCH 01/29] console: Fix toggle and spinner --- pkg/webui/components/dropdown/index.js | 2 +- pkg/webui/components/panel/toggle/index.js | 2 +- pkg/webui/components/panel/toggle/toggle.styl | 2 +- pkg/webui/components/spinner/index.js | 4 ++++ pkg/webui/components/spinner/spinner.styl | 4 ++++ 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/webui/components/dropdown/index.js b/pkg/webui/components/dropdown/index.js index 22abd46d69..ed94242db3 100644 --- a/pkg/webui/components/dropdown/index.js +++ b/pkg/webui/components/dropdown/index.js @@ -169,7 +169,7 @@ DropdownItem.propTypes = { active: PropTypes.bool, exact: PropTypes.bool, external: PropTypes.bool, - icon: PropTypes.string, + icon: PropTypes.shape({}), messageClassName: PropTypes.string, path: PropTypes.string, showActive: PropTypes.bool, diff --git a/pkg/webui/components/panel/toggle/index.js b/pkg/webui/components/panel/toggle/index.js index 64c62664ae..1354fe210b 100644 --- a/pkg/webui/components/panel/toggle/index.js +++ b/pkg/webui/components/panel/toggle/index.js @@ -46,7 +46,7 @@ Toggle.propTypes = { onToggleChange: PropTypes.func.isRequired, options: PropTypes.arrayOf( PropTypes.shape({ - label: PropTypes.string.isRequired, + label: PropTypes.message.isRequired, value: PropTypes.string.isRequired, }), ).isRequired, diff --git a/pkg/webui/components/panel/toggle/toggle.styl b/pkg/webui/components/panel/toggle/toggle.styl index 44882c3d2d..1e8d4e0a5c 100644 --- a/pkg/webui/components/panel/toggle/toggle.styl +++ b/pkg/webui/components/panel/toggle/toggle.styl @@ -39,7 +39,7 @@ &:hover:not(.toggle-button-active) background: var(--c-bg-neutral-light) -@container panel (max-width: 395px) +@container panel (max-width: 485px) .toggle width: 100% diff --git a/pkg/webui/components/spinner/index.js b/pkg/webui/components/spinner/index.js index 2ba62cf797..6e8ef56c06 100644 --- a/pkg/webui/components/spinner/index.js +++ b/pkg/webui/components/spinner/index.js @@ -31,6 +31,7 @@ const Spinner = ({ micro = false, small, inline = false, + right = false, }) => { const [visible, setVisible] = useState(false) const visibilityTimeout = setTimeout(() => setVisible(true), after) @@ -52,6 +53,7 @@ const Spinner = ({ faded, visible, inline, + right, }), ) @@ -82,6 +84,7 @@ Spinner.propTypes = { faded: PropTypes.bool, inline: PropTypes.bool, micro: PropTypes.bool, + right: PropTypes.bool, small: PropTypes.bool, } @@ -94,6 +97,7 @@ Spinner.defaultProps = { inline: false, micro: false, small: false, + right: false, } export default Spinner diff --git a/pkg/webui/components/spinner/spinner.styl b/pkg/webui/components/spinner/spinner.styl index 0f7fc1b6a9..0db3f6f5fe 100644 --- a/pkg/webui/components/spinner/spinner.styl +++ b/pkg/webui/components/spinner/spinner.styl @@ -33,6 +33,10 @@ $xs = $cs.m justify-content: flex-start align-items: center + &.right + &:not(.inline) + text-align: right + .spinner width: $l height: $l From 28b8479577a797e4f0d639e28b186d2f61ff9fa0 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Apr 2024 10:55:25 +0200 Subject: [PATCH 02/29] console: Add top entities panel --- .../top-entities-dashboard-panel/index.js | 101 ++++++++++ .../top-entities-dashboard-panel/item.js | 63 +++++++ .../top-entities-dashboard-panel/list.js | 178 ++++++++++++++++++ .../top-entities-panel.styl | 38 ++++ 4 files changed, 380 insertions(+) create mode 100644 pkg/webui/console/containers/top-entities-dashboard-panel/index.js create mode 100644 pkg/webui/console/containers/top-entities-dashboard-panel/item.js create mode 100644 pkg/webui/console/containers/top-entities-dashboard-panel/list.js create mode 100644 pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/index.js new file mode 100644 index 0000000000..f4b3f79c16 --- /dev/null +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/index.js @@ -0,0 +1,101 @@ +// 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, useState } from 'react' +import { useDispatch } from 'react-redux' + +import { IconStar } from '@ttn-lw/components/icon' +import Panel from '@ttn-lw/components/panel' + +import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' +import sharedMessages from '@ttn-lw/lib/shared-messages' + +import { getBookmarksList } from '@console/store/actions/user-preferences' + +import AllTopEntitiesList from './all-top-entities' +import TopApplicationsList from './top-applications' +import TopGatewaysList from './top-gateways' +import TopDevicesList from './top-devices' + +import styles from './top-entities-panel.styl' + +const BATCH_SIZE = 20 + +const indicesToPage = (startIndex, stopIndex, limit) => { + const startPage = Math.floor(startIndex / limit) + 1 + const stopPage = Math.floor(stopIndex / limit) + 1 + return [startPage, stopPage] +} + +const TopEntitiesDashboardPanel = () => { + const [active, setActive] = useState('all') + const [fetching, setFetching] = useState(false) + const dispatch = useDispatch() + + const handleChange = useCallback( + (_, value) => { + setActive(value) + }, + [setActive], + ) + + const options = [ + { label: sharedMessages.all, value: 'all' }, + { label: sharedMessages.applications, value: 'applications' }, + { label: sharedMessages.gateways, value: 'gateways' }, + { label: sharedMessages.devices, value: 'end-devices' }, + ] + + const loadNextPage = useCallback( + async (startIndex, stopIndex) => { + if (fetching) return + setFetching(true) + + // Calculate the number of items to fetch. + const limit = Math.max(BATCH_SIZE, stopIndex - startIndex + 1) + const [startPage, stopPage] = indicesToPage(startIndex, stopIndex, limit) + + // Fetch new notifications with a maximum of 1000 items. + await dispatch( + attachPromise( + getBookmarksList({ + limit: Math.min((stopPage - startPage + 1) * BATCH_SIZE, 1000), + page: startPage, + }), + ), + ) + + setFetching(false) + }, + [fetching, dispatch], + ) + + return ( + + {active === 'all' && } + {active === 'applications' && } + {active === 'gateways' && } + {active === 'end-devices' && } + + ) +} + +export default TopEntitiesDashboardPanel diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/item.js b/pkg/webui/console/containers/top-entities-dashboard-panel/item.js new file mode 100644 index 0000000000..c726a69156 --- /dev/null +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/item.js @@ -0,0 +1,63 @@ +// 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, { useEffect } from 'react' + +import { Table } from '@ttn-lw/components/table' + +import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' +import PropTypes from '@ttn-lw/lib/prop-types' + +import styles from './top-entities-panel.styl' + +const EntitiesItem = ({ bookmark, headers, setIsAtBottom, index, itemsTotalCount }) => { + const { title, ids, path, icon } = useBookmark(bookmark) + + useEffect(() => { + setIsAtBottom(index + 1 === itemsTotalCount) + }, [index, setIsAtBottom, itemsTotalCount]) + + return ( + + {headers.map((header, index) => { + const value = + headers[index].name === 'name' ? title : headers[index].name === 'icon' ? icon : '' + const entityID = headers[index].name === 'name' ? ids.id : undefined + return ( + + {headers[index].render(value, entityID)} + + ) + })} + + ) +} + +EntitiesItem.propTypes = { + bookmark: PropTypes.shape({}).isRequired, + headers: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + displayName: PropTypes.string, + render: PropTypes.func, + getValue: PropTypes.func, + align: PropTypes.string, + }), + ).isRequired, + index: PropTypes.number.isRequired, + itemsTotalCount: PropTypes.number.isRequired, + setIsAtBottom: PropTypes.func.isRequired, +} + +export default EntitiesItem diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/list.js b/pkg/webui/console/containers/top-entities-dashboard-panel/list.js new file mode 100644 index 0000000000..8c8fc79b9c --- /dev/null +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/list.js @@ -0,0 +1,178 @@ +// 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, useState } from 'react' +import { FixedSizeList as List } from 'react-window' +import InfiniteLoader from 'react-window-infinite-loader' +import AutoSizer from 'react-virtualized-auto-sizer' +import { useSelector } from 'react-redux' + +import Spinner from '@ttn-lw/components/spinner' +import { Table } from '@ttn-lw/components/table' +import { IconPlus } from '@ttn-lw/components/icon' +import Button from '@ttn-lw/components/button' + +import Message from '@ttn-lw/lib/components/message' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import EntitiesItem from './item' + +import styles from './top-entities-panel.styl' + +const EntitiesList = ({ + loadNextPage, + itemsCountSelector, + itemsSelector, + headers, + emptyMessage, + emptyDescription, + emptyAction, + emptyPath, + EntitiesItemComponent: EntitiesItemProp, + entity, +}) => { + const items = useSelector(itemsSelector) + const itemsTotalCount = useSelector(state => itemsCountSelector(state, entity)) + const hasNextPage = items.length < itemsTotalCount + const [isAtBottom, setIsAtBottom] = useState(false) + const EntitiesItemComponent = EntitiesItemProp ?? EntitiesItem + + // If the total count is not known, we assume that there are 100 items. + // Otherwise, if totalCount is 0, it means the list is empty and we should not have a total count. + const itemCount = itemsTotalCount >= 0 ? itemsTotalCount : 100 + + const isItemLoaded = useCallback( + index => (items.length > 0 ? !hasNextPage || index < items.length : false), + [hasNextPage, items], + ) + + const Item = ({ index, style }) => + isItemLoaded(index) ? ( +
+ +
+ ) : ( +
+ +
+ ) + + Item.propTypes = { + index: PropTypes.number.isRequired, + style: PropTypes.shape({}).isRequired, + } + + const columns = ( + + {headers.map((header, key) => ( + + ))} + + ) + + const minWidth = `${headers.length * 10 + 5}rem` + + return items.length === 0 && itemsTotalCount === 0 ? ( +
+
+ + +
+ {emptyAction && ( +
+ +
+ )} +
+ ) : ( + + {columns} + + + {({ width }) => ( + + {({ onItemsRendered, ref }) => ( + <> + + {Item} + + {!isAtBottom &&
} + + )} + + )} + + +
+ ) +} + +EntitiesList.propTypes = { + EntitiesItemComponent: PropTypes.func, + emptyAction: PropTypes.message, + emptyDescription: PropTypes.message, + emptyMessage: PropTypes.message, + emptyPath: PropTypes.string, + entity: PropTypes.string, + headers: PropTypes.arrayOf( + PropTypes.shape({ + align: PropTypes.string, + displayName: PropTypes.message, + name: PropTypes.string, + width: PropTypes.string, + className: PropTypes.string, + }), + ).isRequired, + itemsCountSelector: PropTypes.func.isRequired, + itemsSelector: PropTypes.func.isRequired, + loadNextPage: PropTypes.func.isRequired, +} + +EntitiesList.defaultProps = { + emptyDescription: undefined, + emptyMessage: undefined, + emptyAction: undefined, + emptyPath: undefined, + entity: undefined, + EntitiesItemComponent: undefined, +} + +export default EntitiesList diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl b/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl new file mode 100644 index 0000000000..11d937fa70 --- /dev/null +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl @@ -0,0 +1,38 @@ +// 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. + +.top-entities-panel + min-height: 31rem + +.entity + &-list + scrollbar-width: none + &::-webkit-scrollbar + display: none + &-gradient + position:absolute + bottom: 0px + left: 0px + width: 100% + height: 98px + border-radius: 0px 0px 14px 14px + background: linear-gradient(180deg, rgba(255, 255, 255, 0.20) 0%, #FFF 64.58%) + &-body + height: 20rem + &-row + min-width: 100% + display: inline-table + border-bottom: 1px solid var(--c-border-neutral-extralight) + &-cell + width: 33% \ No newline at end of file From 831676e0108cdf166baba2333ec4a454ea79760d Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Apr 2024 10:56:18 +0200 Subject: [PATCH 03/29] console: Add individual lists --- .../all-top-entities/index.js | 59 ++++++++++++++ .../top-applications/index.js | 81 +++++++++++++++++++ .../top-applications/item.js | 81 +++++++++++++++++++ .../top-devices/index.js | 61 ++++++++++++++ .../top-gateways/index.js | 63 +++++++++++++++ 5 files changed, 345 insertions(+) create mode 100644 pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js create mode 100644 pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/index.js create mode 100644 pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js create mode 100644 pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js create mode 100644 pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js new file mode 100644 index 0000000000..0a983371a6 --- /dev/null +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js @@ -0,0 +1,59 @@ +// 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 from 'react' + +import Icon from '@ttn-lw/components/icon' + +import Message from '@ttn-lw/lib/components/message' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import { + selectBookmarksList, + selectBookmarksTotalCount, +} from '@console/store/selectors/user-preferences' + +import EntitiesList from '../list' + +const AllTopEntitiesList = ({ loadNextPage }) => { + const headers = [ + { + name: 'icon', + render: icon => , + }, + { + name: 'name', + displayName: 'Name', + render: (name, id) => , + }, + ] + + return ( + + ) +} + +AllTopEntitiesList.propTypes = { + loadNextPage: PropTypes.func.isRequired, +} + +export default AllTopEntitiesList diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/index.js new file mode 100644 index 0000000000..6f2970aac5 --- /dev/null +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/index.js @@ -0,0 +1,81 @@ +// 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 from 'react' +import { FormattedNumber } from 'react-intl' + +import Spinner from '@ttn-lw/components/spinner' + +import Message from '@ttn-lw/lib/components/message' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import { + selectApplicationBookmarks, + selectPerEntityTotalCount, +} from '@console/store/selectors/user-preferences' + +import EntitiesList from '../list' + +import TopApplicationsItem from './item' + +const TopApplicationsList = ({ loadNextPage }) => { + const headers = [ + { + name: 'name', + displayName: 'Name', + render: (name, id) => ( + <> + + {name && ( + + )} + + ), + }, + { + name: 'deviceCount', + displayName: 'Devices', + render: deviceCount => + typeof deviceCount !== 'number' ? ( + + ) : ( + + + + ), + }, + ] + + return ( + + ) +} + +TopApplicationsList.propTypes = { + loadNextPage: PropTypes.func.isRequired, +} + +export default TopApplicationsList diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js new file mode 100644 index 0000000000..c6c4384776 --- /dev/null +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js @@ -0,0 +1,81 @@ +// 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, useEffect } from 'react' +import { useSelector } from 'react-redux' + +import { Table } from '@ttn-lw/components/table' + +import RequireRequest from '@ttn-lw/lib/components/require-request' + +import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' +import PropTypes from '@ttn-lw/lib/prop-types' + +import { getApplicationDeviceCount } from '@console/store/actions/applications' + +import { selectApplicationDeviceCount } from '@console/store/selectors/applications' + +import styles from '../top-entities-panel.styl' + +const TopApplicationsItem = ({ bookmark, headers, setIsAtBottom, index, itemsTotalCount }) => { + const { title, ids, path } = useBookmark(bookmark) + const deviceCount = useSelector(state => selectApplicationDeviceCount(state, ids.id)) + + useEffect(() => { + setIsAtBottom(index + 1 === itemsTotalCount) + }, [index, setIsAtBottom, itemsTotalCount]) + + const loadDeviceCount = useCallback( + async dispatch => { + if (!deviceCount) { + dispatch(getApplicationDeviceCount(ids.id)) + } + }, + [deviceCount, ids.id], + ) + + return ( + + + {headers.map((header, index) => { + const value = headers[index].name === 'name' ? title : deviceCount + const entityID = headers[index].name === 'name' ? ids.id : undefined + return ( + + {headers[index].render(value, entityID)} + + ) + })} + + + ) +} + +TopApplicationsItem.propTypes = { + bookmark: PropTypes.shape({}).isRequired, + headers: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + displayName: PropTypes.string.isRequired, + render: PropTypes.func, + getValue: PropTypes.func, + align: PropTypes.string, + }), + ).isRequired, + index: PropTypes.number.isRequired, + itemsTotalCount: PropTypes.number.isRequired, + setIsAtBottom: PropTypes.func.isRequired, +} + +export default TopApplicationsItem diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js new file mode 100644 index 0000000000..680b5ad858 --- /dev/null +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js @@ -0,0 +1,61 @@ +// 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 from 'react' + +import Message from '@ttn-lw/lib/components/message' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import { + selectEndDeviceBookmarks, + selectPerEntityTotalCount, +} from '@console/store/selectors/user-preferences' + +import EntitiesList from '../list' + +const TopDevicesList = ({ loadNextPage }) => { + const headers = [ + { + name: 'name', + displayName: 'Name', + render: (name, id) => ( + <> + + {name && ( + + )} + + ), + }, + ] + + return ( + + ) +} + +TopDevicesList.propTypes = { + loadNextPage: PropTypes.func.isRequired, +} + +export default TopDevicesList diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js new file mode 100644 index 0000000000..9a127721b4 --- /dev/null +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js @@ -0,0 +1,63 @@ +// 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 from 'react' + +import Message from '@ttn-lw/lib/components/message' + +import PropTypes from '@ttn-lw/lib/prop-types' + +import { + selectGatewayBookmarks, + selectPerEntityTotalCount, +} from '@console/store/selectors/user-preferences' + +import EntitiesList from '../list' + +const TopGatewaysList = ({ loadNextPage }) => { + const headers = [ + { + name: 'name', + displayName: 'Name', + render: (name, id) => ( + <> + + {name && ( + + )} + + ), + }, + ] + + return ( + + ) +} + +TopGatewaysList.propTypes = { + loadNextPage: PropTypes.func.isRequired, +} + +export default TopGatewaysList From 41a1dacb3a859bd52b41f7a4b6dfb03a68f4f554 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Apr 2024 10:56:45 +0200 Subject: [PATCH 04/29] console: Add per entity bookmark store --- .../middleware/logics/user-preferences.js | 35 +++++++++++++++++-- .../store/reducers/user-preferences.js | 2 +- .../store/selectors/user-preferences.js | 29 ++++++++++++++- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/pkg/webui/console/store/middleware/logics/user-preferences.js b/pkg/webui/console/store/middleware/logics/user-preferences.js index 6c6ef01255..4710b372ca 100644 --- a/pkg/webui/console/store/middleware/logics/user-preferences.js +++ b/pkg/webui/console/store/middleware/logics/user-preferences.js @@ -18,6 +18,31 @@ import createRequestLogic from '@ttn-lw/lib/store/logics/create-request-logic' import * as userPreferences from '@console/store/actions/user-preferences' +const getPerEntityTotalCountThroughPagination = async (totalCount, userId) => { + let page = 1 + const limit = 1000 + const result = {} + + while ((page - 1) * limit < totalCount) { + // Get the next page of notifications. + // eslint-disable-next-line no-await-in-loop + const response = await tts.Users.getBookmarks(userId, { page, limit }) + response.bookmarks.forEach(element => { + const entityIds = element.entity_ids + const entity = Object.keys(entityIds)[0].replace('_ids', '') + if (!result[entity]) { + result[entity] = 1 + } else { + result[entity] += 1 + } + }) + + page += 1 + } + + return result +} + const getBookmarksListLogic = createRequestLogic({ type: userPreferences.GET_BOOKMARKS_LIST, process: async ({ action }) => { @@ -26,8 +51,14 @@ const getBookmarksListLogic = createRequestLogic({ params: { page, limit, order, deleted }, } = action.payload const data = await tts.Users.getBookmarks(userId, { page, limit, order, deleted }) - - return { entities: data.bookmarks, totalCount: data.totalCount } + const perEntityTotalCount = await getPerEntityTotalCountThroughPagination( + data.totalCount, + userId, + ) + return { + entities: data.bookmarks, + totalCount: { totalCount: data.totalCount, perEntityTotalCount }, + } }, }) diff --git a/pkg/webui/console/store/reducers/user-preferences.js b/pkg/webui/console/store/reducers/user-preferences.js index 4ca500aa8d..d19c4268b2 100644 --- a/pkg/webui/console/store/reducers/user-preferences.js +++ b/pkg/webui/console/store/reducers/user-preferences.js @@ -18,7 +18,7 @@ import { GET_USER_ME_SUCCESS } from '@console/store/actions/logout' const initialState = { bookmarks: { bookmarks: [], - totalCount: 0, + totalCount: {}, }, consolePreferences: {}, } diff --git a/pkg/webui/console/store/selectors/user-preferences.js b/pkg/webui/console/store/selectors/user-preferences.js index 008cf112da..ac995b4a02 100644 --- a/pkg/webui/console/store/selectors/user-preferences.js +++ b/pkg/webui/console/store/selectors/user-preferences.js @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { createSelector } from 'reselect' + const selectUserPreferencesStore = state => state.userPreferences export const selectConsolePreferences = state => @@ -19,5 +21,30 @@ export const selectConsolePreferences = state => export const selectBookmarksList = state => selectUserPreferencesStore(state).bookmarks.bookmarks +export const selectApplicationBookmarks = createSelector([selectBookmarksList], bookmarks => + bookmarks.filter(bookmark => + bookmark.entity_ids + ? Object.keys(bookmark.entity_ids)[0].replace('_ids', '') === 'application' + : [], + ), +) + +export const selectGatewayBookmarks = createSelector([selectBookmarksList], bookmarks => + bookmarks.filter(bookmark => + bookmark.entity_ids + ? Object.keys(bookmark.entity_ids)[0].replace('_ids', '') === 'gateway' + : [], + ), +) + +export const selectEndDeviceBookmarks = createSelector([selectBookmarksList], bookmarks => + bookmarks.filter(bookmark => + bookmark.entity_ids ? Object.keys(bookmark.entity_ids)[0].replace('_ids', '') === 'device' : [], + ), +) + export const selectBookmarksTotalCount = state => - selectUserPreferencesStore(state).bookmarks.totalCount + selectUserPreferencesStore(state).bookmarks.totalCount.totalCount + +export const selectPerEntityTotalCount = (state, entity) => + selectUserPreferencesStore(state).bookmarks.totalCount.perEntityTotalCount[entity] || 0 From 1dd83566df28eee2ff37dd82ffd1e47e5dfe317c Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Apr 2024 10:57:26 +0200 Subject: [PATCH 05/29] console: Add top entities panel to overview --- pkg/webui/console/views/overview/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/webui/console/views/overview/index.js b/pkg/webui/console/views/overview/index.js index ee42fe272a..15c5c1e656 100644 --- a/pkg/webui/console/views/overview/index.js +++ b/pkg/webui/console/views/overview/index.js @@ -22,6 +22,7 @@ import RequireRequest from '@ttn-lw/lib/components/require-request' import ShortcutPanel from '@console/containers/shortcut-panel' import NotificationsDashboardPanel from '@console/containers/notifications-dashboard-panel' import DocumentationDashboardPanel from '@console/containers/documentation-dashboard-panel' +import TopEntitiesDashboardPanel from '@console/containers/top-entities-dashboard-panel' import sharedMessages from '@ttn-lw/lib/shared-messages' @@ -34,7 +35,9 @@ const Overview = () => { return (
-
+
+ +
From 9416c8d051039c8cd21654cc1cd0c296b8915077 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Apr 2024 10:57:43 +0200 Subject: [PATCH 06/29] console: Fix usebookmark method --- pkg/webui/lib/hooks/use-bookmark.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/pkg/webui/lib/hooks/use-bookmark.js b/pkg/webui/lib/hooks/use-bookmark.js index 54e8f2878c..40b9c63418 100644 --- a/pkg/webui/lib/hooks/use-bookmark.js +++ b/pkg/webui/lib/hooks/use-bookmark.js @@ -15,6 +15,15 @@ import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' +import { + IconApplication, + IconGateway, + IconOrganization, + IconUser, + IconOauthClients, + IconDevice, +} from '@ttn-lw/components/icon' + import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' import { getApplication } from '@console/store/actions/applications' @@ -32,12 +41,12 @@ import { selectClientById } from '@account/store/selectors/clients' import { selectDeviceByIds } from '@console/store/selectors/devices' const iconMap = { - application: 'application', - gateway: 'gateway', - organization: 'organization', - user: 'user', - client: 'client', - device: 'device', + application: IconApplication, + gateway: IconGateway, + organization: IconOrganization, + user: IconUser, + client: IconOauthClients, + device: IconDevice, } const entityRequestMap = { @@ -93,11 +102,11 @@ const useBookmark = bookmark => { } else { response = await dispatch(attachPromise(entityRequestMap[entity](entityId.id, 'name'))) } - setBookmarkTitle(response.name || entityIds[`${entity}_ids`][`${entity}_id`]) + setBookmarkTitle(response.name || '') } // Only fetch the entity if the name is not already in the store. - if (!bookmarkTitle) { + if (typeof bookmarkTitle !== 'string') { fetchEntity() } }, [entity, entityId, dispatch, entityIds, bookmarkTitle]) @@ -108,9 +117,9 @@ const useBookmark = bookmark => { const path = entity === 'device' ? `/applications/${entityIds.device_ids.application_ids.application_id}/devices/${entityIds.device_ids.device_id}` - : `/${entity}s/${entityIds[`${entity}_ids`][`${entity}_id`]}` + : `/${entity}s/${entityId.id}` - return { title: bookmarkTitle ?? '', path, icon } + return { title: bookmarkTitle ?? '', ids: entityId, path, icon } } export default useBookmark From 0f5b5aa1e44b2fc1de418dc9c60f1e27d506135c Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Apr 2024 11:15:01 +0200 Subject: [PATCH 07/29] console: Add messages --- .../top-entities-dashboard-panel/index.js | 7 ++++++- .../top-entities-dashboard-panel/list.js | 7 ++++++- .../top-applications/index.js | 19 +++++++++++++------ .../top-devices/index.js | 10 ++++++++-- .../top-gateways/index.js | 16 ++++++++++++---- pkg/webui/lib/shared-messages.js | 1 + pkg/webui/locales/en.json | 16 ++++++++++++++-- pkg/webui/locales/ja.json | 14 +++++++++++++- 8 files changed, 73 insertions(+), 17 deletions(-) diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/index.js index f4b3f79c16..3228e1227b 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/index.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/index.js @@ -14,6 +14,7 @@ import React, { useCallback, useState } from 'react' import { useDispatch } from 'react-redux' +import { defineMessages } from 'react-intl' import { IconStar } from '@ttn-lw/components/icon' import Panel from '@ttn-lw/components/panel' @@ -38,6 +39,10 @@ const indicesToPage = (startIndex, stopIndex, limit) => { return [startPage, stopPage] } +const m = defineMessages({ + title: 'Your top entities', +}) + const TopEntitiesDashboardPanel = () => { const [active, setActive] = useState('all') const [fetching, setFetching] = useState(false) @@ -83,7 +88,7 @@ const TopEntitiesDashboardPanel = () => { return ( {columns} - + {({ width }) => ( { const headers = [ { name: 'name', - displayName: 'Name', + displayName: sharedMessages.name, render: (name, id) => ( <> @@ -46,7 +53,7 @@ const TopApplicationsList = ({ loadNextPage }) => { }, { name: 'deviceCount', - displayName: 'Devices', + displayName: sharedMessages.devicesShort, render: deviceCount => typeof deviceCount !== 'number' ? ( @@ -65,9 +72,9 @@ const TopApplicationsList = ({ loadNextPage }) => { itemsSelector={selectApplicationBookmarks} headers={headers} EntitiesItemComponent={TopApplicationsItem} - emptyMessage={'No top Application yet'} - emptyDescription={'Your most visited, and bookmarked Applications will be listed here.'} - emptyAction={'Create Application'} + emptyMessage={m.emptyMessage} + emptyDescription={m.emptyDescription} + emptyAction={m.emptyAction} emptyPath={'/applications/add'} entity={'application'} /> diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js index 680b5ad858..38cbbedb20 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js @@ -13,6 +13,7 @@ // limitations under the License. import React from 'react' +import { defineMessages } from 'react-intl' import Message from '@ttn-lw/lib/components/message' @@ -25,6 +26,11 @@ import { import EntitiesList from '../list' +const m = defineMessages({ + emptyMessage: 'No top Device yet', + emptyDescription: 'Your most visited, and bookmarked Devices will be listed here.', +}) + const TopDevicesList = ({ loadNextPage }) => { const headers = [ { @@ -47,8 +53,8 @@ const TopDevicesList = ({ loadNextPage }) => { itemsCountSelector={selectPerEntityTotalCount} itemsSelector={selectEndDeviceBookmarks} headers={headers} - emptyMessage={'No top Device yet'} - emptyDescription={'Your most visited, and bookmarked Devices will be listed here.'} + emptyMessage={m.emptyMessage} + emptyDescription={m.emptyDescription} entity={'device'} /> ) diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js index 9a127721b4..9e691eef1a 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js @@ -13,10 +13,12 @@ // limitations under the License. import React from 'react' +import { defineMessages } from 'react-intl' import Message from '@ttn-lw/lib/components/message' import PropTypes from '@ttn-lw/lib/prop-types' +import sharedMessages from '@ttn-lw/lib/shared-messages' import { selectGatewayBookmarks, @@ -25,11 +27,17 @@ import { import EntitiesList from '../list' +const m = defineMessages({ + emptyMessage: 'No top Gateway yet', + emptyDescription: 'Your most visited, and bookmarked Gateways will be listed here.', + emptyAction: 'Create Gateway', +}) + const TopGatewaysList = ({ loadNextPage }) => { const headers = [ { name: 'name', - displayName: 'Name', + displayName: sharedMessages.displayName, render: (name, id) => ( <> @@ -47,9 +55,9 @@ const TopGatewaysList = ({ loadNextPage }) => { itemsCountSelector={selectPerEntityTotalCount} itemsSelector={selectGatewayBookmarks} headers={headers} - emptyMessage={'No top Gateway yet'} - emptyDescription={'Your most visited, and bookmarked Gateways will be listed here.'} - emptyAction={'Create Gateway'} + emptyMessage={m.emptyMessage} + emptyDescription={m.emptyDescription} + emptyAction={m.emptyAction} emptyPath={'/gateways/add'} entity={'gateway'} /> diff --git a/pkg/webui/lib/shared-messages.js b/pkg/webui/lib/shared-messages.js index e9d37d573a..6a2003882a 100644 --- a/pkg/webui/lib/shared-messages.js +++ b/pkg/webui/lib/shared-messages.js @@ -186,6 +186,7 @@ export default defineMessages({ deviceNamePlaceholder: 'My new end device', deviceSimulationDisabledWarning: 'Simulation is disabled for devices that skip payload crypto', devices: 'End devices', + devicesShort: 'Devices', disabled: 'Disabled', disconnected: 'Disconnected', documentation: 'Documentation', diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 48eaa3c238..253547e2e9 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -568,8 +568,9 @@ "console.containers.gateways-table.index.restoreFail": "There was an error and the gateway could not be restored", "console.containers.gateways-table.index.purgeSuccess": "Gateway purged", "console.containers.gateways-table.index.purgeFail": "There was an error and the gateway could not be purged", - "console.containers.header.bookmarks-dropdown.noBookmarks": "No bookmark yet.", - "console.containers.header.bookmarks-dropdown.noBookmarksDescription": "Your bookmarked entities will be listed here.", + "console.containers.header.bookmarks-dropdown.noBookmarks": "No bookmarks yet", + "console.containers.header.bookmarks-dropdown.noBookmarksDescription": "Your bookmarked entities will be listed here", + "console.containers.header.bookmarks-dropdown.threshold": "Only showing latest 15 bookmarks", "console.containers.header.index.addApplication": "Add new application", "console.containers.header.index.addGateway": "Add new gateway", "console.containers.header.index.addOrganization": "Add new organization", @@ -641,6 +642,16 @@ "console.containers.shortcut-panel.index.registerDevice": "Register a device", "console.containers.sidebar.navigation.app-side-navigation.buttonMessage": "Back to Applications list", "console.containers.sidebar.navigation.gtw-side-navigation.buttonMessage": "Back to Gateways list", + "console.containers.top-entities-dashboard-panel.index.title": "Your top entities", + "console.containers.top-entities-dashboard-panel.list.empty": "No entities yet", + "console.containers.top-entities-dashboard-panel.top-applications.index.emptyMessage": "No top Application yet", + "console.containers.top-entities-dashboard-panel.top-applications.index.emptyDescription": "Your most visited, and bookmarked Applications will be listed here.", + "console.containers.top-entities-dashboard-panel.top-applications.index.emptyAction": "Create Application", + "console.containers.top-entities-dashboard-panel.top-devices.index.emptyMessage": "No top Device yet", + "console.containers.top-entities-dashboard-panel.top-devices.index.emptyDescription": "Your most visited, and bookmarked Devices will be listed here.", + "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyMessage": "No top Gateway yet", + "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyDescription": "Your most visited, and bookmarked Gateways will be listed here.", + "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyAction": "Create Gateway", "console.containers.user-data-form.edit.deleteWarning": "This will PERMANENTLY DELETE THIS ACCOUNT and LOCK THE USER ID AND EMAIL FOR RE-REGISTRATION. Associated entities (e.g. gateways, applications and end devices) owned by this user that do not have any other collaborators will become UNACCESSIBLE and it will NOT BE POSSIBLE TO REGISTER ENTITIES WITH THE SAME ID OR EUI's AGAIN. Make sure you assign new collaborators to such entities if you plan to continue using them.", "console.containers.user-data-form.edit.purgeWarning": "This will PERMANENTLY DELETE THIS ACCOUNT. Associated entities (e.g. gateways, applications and end devices) owned by this user that do not have any other collaborators will become UNACCESSIBLE and it will NOT BE POSSIBLE TO REGISTER ENTITIES WITH THE SAME ID OR EUI's AGAIN. Make sure you assign new collaborators to such entities if you plan to continue using them.", "console.containers.user-data-form.edit.deleteConfirmMessage": "Please type in this user's user ID to confirm.", @@ -1171,6 +1182,7 @@ "lib.shared-messages.deviceNamePlaceholder": "My new end device", "lib.shared-messages.deviceSimulationDisabledWarning": "Simulation is disabled for devices that skip payload crypto", "lib.shared-messages.devices": "End devices", + "lib.shared-messages.devicesShort": "Devices", "lib.shared-messages.disabled": "Disabled", "lib.shared-messages.disconnected": "Disconnected", "lib.shared-messages.documentation": "Documentation", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 19453d256e..34b863a272 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -145,7 +145,7 @@ "components.key-value-map.index.addEntry": "エントリー追加", "components.link.index.glossaryTitle": "用語集「{term}」を参照してください", "components.link.index.defaultGlossaryTitle": "用語集を見る", - "components.mobile-menu.index.loggedInAs": "{userId}でログインしました", + "components.mobile-menu.index.loggedInAs": "", "components.notification.details.index.showDetails": "詳細を表示", "components.notification.details.index.details": "詳細", "components.notification.details.index.errorDetails": "エラー詳細", @@ -570,6 +570,7 @@ "console.containers.gateways-table.index.purgeFail": "エラーが発生したため、ゲートウェイをパージすることができませんでした", "console.containers.header.bookmarks-dropdown.noBookmarks": "", "console.containers.header.bookmarks-dropdown.noBookmarksDescription": "", + "console.containers.header.bookmarks-dropdown.threshold": "", "console.containers.header.index.addApplication": "", "console.containers.header.index.addGateway": "", "console.containers.header.index.addOrganization": "", @@ -641,6 +642,16 @@ "console.containers.shortcut-panel.index.registerDevice": "", "console.containers.sidebar.navigation.app-side-navigation.buttonMessage": "", "console.containers.sidebar.navigation.gtw-side-navigation.buttonMessage": "", + "console.containers.top-entities-dashboard-panel.index.title": "", + "console.containers.top-entities-dashboard-panel.list.empty": "", + "console.containers.top-entities-dashboard-panel.top-applications.index.emptyMessage": "", + "console.containers.top-entities-dashboard-panel.top-applications.index.emptyDescription": "", + "console.containers.top-entities-dashboard-panel.top-applications.index.emptyAction": "", + "console.containers.top-entities-dashboard-panel.top-devices.index.emptyMessage": "", + "console.containers.top-entities-dashboard-panel.top-devices.index.emptyDescription": "", + "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyMessage": "", + "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyDescription": "", + "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyAction": "", "console.containers.user-data-form.edit.deleteWarning": "", "console.containers.user-data-form.edit.purgeWarning": "", "console.containers.user-data-form.edit.deleteConfirmMessage": "", @@ -1171,6 +1182,7 @@ "lib.shared-messages.deviceNamePlaceholder": "私の新しいエンドデバイス", "lib.shared-messages.deviceSimulationDisabledWarning": "ペイロード暗号をスキップする機器では、シミュレーションは無効になります", "lib.shared-messages.devices": "エンドデバイス", + "lib.shared-messages.devicesShort": "", "lib.shared-messages.disabled": "無効化", "lib.shared-messages.disconnected": "切断されます", "lib.shared-messages.documentation": "ドキュメンテーション", From 90a0298cba7a40ece95033d8e10dc796d3f55551 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Apr 2024 12:31:29 +0200 Subject: [PATCH 08/29] console: Fix gradient --- .../top-entities-dashboard-panel/item.js | 25 +++++++++++-------- .../top-entities-dashboard-panel/list.js | 13 +++------- .../top-applications/item.js | 25 +++++++++++-------- .../top-entities-panel.styl | 2 ++ 4 files changed, 36 insertions(+), 29 deletions(-) diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/item.js b/pkg/webui/console/containers/top-entities-dashboard-panel/item.js index c726a69156..ab3501c115 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/item.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/item.js @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useEffect } from 'react' +import React from 'react' +import classNames from 'classnames' import { Table } from '@ttn-lw/components/table' @@ -21,15 +22,17 @@ import PropTypes from '@ttn-lw/lib/prop-types' import styles from './top-entities-panel.styl' -const EntitiesItem = ({ bookmark, headers, setIsAtBottom, index, itemsTotalCount }) => { +const EntitiesItem = ({ bookmark, headers, last }) => { const { title, ids, path, icon } = useBookmark(bookmark) - useEffect(() => { - setIsAtBottom(index + 1 === itemsTotalCount) - }, [index, setIsAtBottom, itemsTotalCount]) - return ( - + {headers.map((header, index) => { const value = headers[index].name === 'name' ? title : headers[index].name === 'icon' ? icon : '' @@ -55,9 +58,11 @@ EntitiesItem.propTypes = { align: PropTypes.string, }), ).isRequired, - index: PropTypes.number.isRequired, - itemsTotalCount: PropTypes.number.isRequired, - setIsAtBottom: PropTypes.func.isRequired, + last: PropTypes.bool, +} + +EntitiesItem.defaultProps = { + last: false, } export default EntitiesItem diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/list.js b/pkg/webui/console/containers/top-entities-dashboard-panel/list.js index 81ff796da3..bebc54ed48 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/list.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/list.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useCallback, useState } from 'react' +import React, { useCallback } from 'react' import { FixedSizeList as List } from 'react-window' import InfiniteLoader from 'react-window-infinite-loader' import AutoSizer from 'react-virtualized-auto-sizer' @@ -51,12 +51,9 @@ const EntitiesList = ({ const items = useSelector(itemsSelector) const itemsTotalCount = useSelector(state => itemsCountSelector(state, entity)) const hasNextPage = items.length < itemsTotalCount - const [isAtBottom, setIsAtBottom] = useState(false) const EntitiesItemComponent = EntitiesItemProp ?? EntitiesItem - // If the total count is not known, we assume that there are 100 items. - // Otherwise, if totalCount is 0, it means the list is empty and we should not have a total count. - const itemCount = itemsTotalCount >= 0 ? itemsTotalCount : 100 + const itemCount = itemsTotalCount const isItemLoaded = useCallback( index => (items.length > 0 ? !hasNextPage || index < items.length : false), @@ -69,9 +66,7 @@ const EntitiesList = ({
) : ( @@ -139,7 +134,7 @@ const EntitiesList = ({ > {Item} - {!isAtBottom &&
} +
)} diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js index c6c4384776..e72955ee65 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useCallback, useEffect } from 'react' +import React, { useCallback } from 'react' import { useSelector } from 'react-redux' +import classNames from 'classnames' import { Table } from '@ttn-lw/components/table' @@ -28,14 +29,10 @@ import { selectApplicationDeviceCount } from '@console/store/selectors/applicati import styles from '../top-entities-panel.styl' -const TopApplicationsItem = ({ bookmark, headers, setIsAtBottom, index, itemsTotalCount }) => { +const TopApplicationsItem = ({ bookmark, headers, last }) => { const { title, ids, path } = useBookmark(bookmark) const deviceCount = useSelector(state => selectApplicationDeviceCount(state, ids.id)) - useEffect(() => { - setIsAtBottom(index + 1 === itemsTotalCount) - }, [index, setIsAtBottom, itemsTotalCount]) - const loadDeviceCount = useCallback( async dispatch => { if (!deviceCount) { @@ -47,7 +44,13 @@ const TopApplicationsItem = ({ bookmark, headers, setIsAtBottom, index, itemsTot return ( - + {headers.map((header, index) => { const value = headers[index].name === 'name' ? title : deviceCount const entityID = headers[index].name === 'name' ? ids.id : undefined @@ -73,9 +76,11 @@ TopApplicationsItem.propTypes = { align: PropTypes.string, }), ).isRequired, - index: PropTypes.number.isRequired, - itemsTotalCount: PropTypes.number.isRequired, - setIsAtBottom: PropTypes.func.isRequired, + last: PropTypes.bool, +} + +TopApplicationsItem.defaultProps = { + last: false, } export default TopApplicationsItem diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl b/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl index 11d937fa70..c29d28696c 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl @@ -34,5 +34,7 @@ min-width: 100% display: inline-table border-bottom: 1px solid var(--c-border-neutral-extralight) + &.last-row + margin-bottom: 56px &-cell width: 33% \ No newline at end of file From 103f31fddbd870ea638eaa5d10136a250a314895 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Apr 2024 13:33:37 +0200 Subject: [PATCH 09/29] console: Fix panel height and overlay render --- .../containers/top-entities-dashboard-panel/index.js | 11 +++++++++-- .../containers/top-entities-dashboard-panel/list.js | 4 ++-- .../top-entities-panel.styl | 2 ++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/index.js index 3228e1227b..724a0dce57 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/index.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/index.js @@ -13,8 +13,9 @@ // limitations under the License. import React, { useCallback, useState } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { defineMessages } from 'react-intl' +import classNames from 'classnames' import { IconStar } from '@ttn-lw/components/icon' import Panel from '@ttn-lw/components/panel' @@ -24,6 +25,8 @@ import sharedMessages from '@ttn-lw/lib/shared-messages' import { getBookmarksList } from '@console/store/actions/user-preferences' +import { selectBookmarksList } from '@console/store/selectors/user-preferences' + import AllTopEntitiesList from './all-top-entities' import TopApplicationsList from './top-applications' import TopGatewaysList from './top-gateways' @@ -46,6 +49,8 @@ const m = defineMessages({ const TopEntitiesDashboardPanel = () => { const [active, setActive] = useState('all') const [fetching, setFetching] = useState(false) + const bookmarks = useSelector(state => selectBookmarksList(state)) + const hasEntities = bookmarks.length > 0 const dispatch = useDispatch() const handleChange = useCallback( @@ -93,7 +98,9 @@ const TopEntitiesDashboardPanel = () => { toggleOptions={options} activeToggle={active} onToggleClick={handleChange} - className={styles.topEntitiesPanel} + className={classNames(styles.topEntitiesPanel, { + [styles.hasEntities]: hasEntities, + })} > {active === 'all' && } {active === 'applications' && } diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/list.js b/pkg/webui/console/containers/top-entities-dashboard-panel/list.js index bebc54ed48..bab0b9709c 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/list.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/list.js @@ -66,7 +66,7 @@ const EntitiesList = ({ 5} />
) : ( @@ -134,7 +134,7 @@ const EntitiesList = ({ > {Item} -
+ {itemsTotalCount > 5 &&
} )} diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl b/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl index c29d28696c..f54ab489a0 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-entities-panel.styl @@ -14,6 +14,8 @@ .top-entities-panel min-height: 31rem + &.has-entities + min-height: 34.5rem .entity &-list From 61d1f0072b77200f93179adc765793f5f1af2a61 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Tue, 2 Apr 2024 13:33:52 +0200 Subject: [PATCH 10/29] console: Fix bookmarks dropdown --- .../console/containers/header/bookmarks-dropdown.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/webui/console/containers/header/bookmarks-dropdown.js b/pkg/webui/console/containers/header/bookmarks-dropdown.js index 96b034013b..cf7c165941 100644 --- a/pkg/webui/console/containers/header/bookmarks-dropdown.js +++ b/pkg/webui/console/containers/header/bookmarks-dropdown.js @@ -34,9 +34,16 @@ const m = defineMessages({ }) const Bookmark = ({ bookmark }) => { - const { title, path, icon } = useBookmark(bookmark) + const { title, ids, path, icon } = useBookmark(bookmark) - return + return ( + + ) } Bookmark.propTypes = { From 67a1a37091ebc552c3fc54d41b671ff646691951 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Thu, 4 Apr 2024 09:13:20 +0200 Subject: [PATCH 11/29] console: Add bookmarks in the sidebar --- .../components/sidebar/section-label/index.js | 2 +- .../navigation/app-list-side-navigation.js | 42 ++++++++++++++++-- .../navigation/general-side-navigation.js | 43 ++++++++++++++++--- .../navigation/gtw-list-side-navigation.js | 43 +++++++++++++++++-- pkg/webui/lib/shared-messages.js | 2 + pkg/webui/locales/en.json | 2 + pkg/webui/locales/ja.json | 2 + 7 files changed, 121 insertions(+), 15 deletions(-) diff --git a/pkg/webui/components/sidebar/section-label/index.js b/pkg/webui/components/sidebar/section-label/index.js index 6fc2bdf659..0eb3547d5d 100644 --- a/pkg/webui/components/sidebar/section-label/index.js +++ b/pkg/webui/components/sidebar/section-label/index.js @@ -36,7 +36,7 @@ const SectionLabel = ({ 'j-between', 'al-center', 'c-text-neutral-light', - 'ml-cs-s', + 'ml-cs-xs', 'fs-s', )} data-test-id={dataTestId} diff --git a/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js b/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js index 6f3ad3d85c..7ce8c7a774 100644 --- a/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js +++ b/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js @@ -12,18 +12,40 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useContext } from 'react' +import React, { useCallback, useContext, useState } from 'react' +import { useSelector } from 'react-redux' import { IconPlus } from '@ttn-lw/components/icon' import SectionLabel from '@ttn-lw/components/sidebar/section-label' import SideNavigation from '@ttn-lw/components/sidebar/side-menu' +import Button from '@ttn-lw/components/button' import sharedMessages from '@ttn-lw/lib/shared-messages' +import PropTypes from '@ttn-lw/lib/prop-types' +import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' + +import { selectApplicationBookmarks } from '@console/store/selectors/user-preferences' import SidebarContext from '../context' +const Bookmark = ({ bookmark }) => { + const { title, ids, path, icon } = useBookmark(bookmark) + + return +} + +Bookmark.propTypes = { + bookmark: PropTypes.shape({}).isRequired, +} + const AppListSideNavigation = () => { - const { topEntities, isMinimized } = useContext(SidebarContext) + const [showMore, setShowMore] = useState(false) + const topEntities = useSelector(state => selectApplicationBookmarks(state)) + const { isMinimized } = useContext(SidebarContext) + + const handleShowMore = useCallback(async () => { + setShowMore(showMore => !showMore) + }, []) if (isMinimized || topEntities.length === 0) { // Rendering an empty div to prevent the shadow of the search bar @@ -36,9 +58,21 @@ const AppListSideNavigation = () => {
null} /> - {topEntities.map(({ path, entity, title }) => ( - + {topEntities.slice(0, 6).map(bookmark => ( + ))} + {showMore && + topEntities.length > 6 && + topEntities + .slice(6, topEntities.length) + .map(bookmark => )} + {topEntities.length > 6 && ( +
) diff --git a/pkg/webui/console/containers/sidebar/navigation/general-side-navigation.js b/pkg/webui/console/containers/sidebar/navigation/general-side-navigation.js index 19e9d9b504..4e1a6b87bc 100644 --- a/pkg/webui/console/containers/sidebar/navigation/general-side-navigation.js +++ b/pkg/webui/console/containers/sidebar/navigation/general-side-navigation.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useContext } from 'react' +import React, { useCallback, useContext, useState } from 'react' import { useSelector } from 'react-redux' import { @@ -25,8 +25,11 @@ import { } from '@ttn-lw/components/icon' import SideNavigation from '@ttn-lw/components/sidebar/side-menu' import SectionLabel from '@ttn-lw/components/sidebar/section-label' +import Button from '@ttn-lw/components/button' import sharedMessages from '@ttn-lw/lib/shared-messages' +import PropTypes from '@ttn-lw/lib/prop-types' +import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' import { checkFromState, @@ -35,12 +38,24 @@ import { } from '@console/lib/feature-checks' import { selectUser, selectUserIsAdmin } from '@console/store/selectors/logout' +import { selectBookmarksList } from '@console/store/selectors/user-preferences' import SidebarContext from '../context' -const GeneralSideNavigation = () => { - const { topEntities, isMinimized } = useContext(SidebarContext) +const Bookmark = ({ bookmark }) => { + const { title, ids, path, icon } = useBookmark(bookmark) + + return +} +Bookmark.propTypes = { + bookmark: PropTypes.shape({}).isRequired, +} + +const GeneralSideNavigation = () => { + const { isMinimized } = useContext(SidebarContext) + const [showMore, setShowMore] = useState(false) + const topEntities = useSelector(state => selectBookmarksList(state)) const isUserAdmin = useSelector(selectUserIsAdmin) const user = useSelector(selectUser) const mayViewOrgs = useSelector(state => @@ -50,6 +65,10 @@ const GeneralSideNavigation = () => { user ? checkFromState(mayViewOrEditApiKeys, state) : false, ) + const handleShowMore = useCallback(async () => { + setShowMore(showMore => !showMore) + }, []) + return ( <> @@ -86,12 +105,24 @@ const GeneralSideNavigation = () => { /> )} - {!isMinimized && ( + {!isMinimized && topEntities.length > 0 && ( null} /> - {topEntities.map(({ path, title, entity }) => ( - + {topEntities.slice(0, 6).map(bookmark => ( + ))} + {showMore && + topEntities.length > 6 && + topEntities + .slice(6, topEntities.length) + .map(bookmark => )} + {topEntities.length > 6 && ( +
) diff --git a/pkg/webui/lib/shared-messages.js b/pkg/webui/lib/shared-messages.js index 6a2003882a..4dd986e826 100644 --- a/pkg/webui/lib/shared-messages.js +++ b/pkg/webui/lib/shared-messages.js @@ -477,6 +477,8 @@ export default defineMessages({ settings: 'Settings', setup: 'Setup', shareGatewayInfo: 'Share gateway information', + showMore: 'Show more', + showLess: 'Show less', skipCryptoDescription: 'Skip decryption of uplink payloads and encryption of downlink payloads', skipCryptoPlaceholder: 'Encryption/decryption disabled', skipCryptoTitle: 'Skip payload encryption and decryption', diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 253547e2e9..34d128d325 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -1455,6 +1455,8 @@ "lib.shared-messages.settings": "Settings", "lib.shared-messages.setup": "Setup", "lib.shared-messages.shareGatewayInfo": "Share gateway information", + "lib.shared-messages.showMore": "Show more", + "lib.shared-messages.showLess": "Show less", "lib.shared-messages.skipCryptoDescription": "Skip decryption of uplink payloads and encryption of downlink payloads", "lib.shared-messages.skipCryptoPlaceholder": "Encryption/decryption disabled", "lib.shared-messages.skipCryptoTitle": "Skip payload encryption and decryption", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 34b863a272..3e1a9275bb 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -1455,6 +1455,8 @@ "lib.shared-messages.settings": "設定", "lib.shared-messages.setup": "", "lib.shared-messages.shareGatewayInfo": "ゲートウェイ情報を共有", + "lib.shared-messages.showMore": "", + "lib.shared-messages.showLess": "", "lib.shared-messages.skipCryptoDescription": "アップリンクペイロードの復号化とダウンリンクペイロードの暗号化をスキップします", "lib.shared-messages.skipCryptoPlaceholder": "暗号化/復号化無効", "lib.shared-messages.skipCryptoTitle": "ペイロードの暗号化と復号化をスキップ", From 1a760877b97e1ad28beaf577090370fab1bd6f02 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Thu, 4 Apr 2024 09:14:34 +0200 Subject: [PATCH 12/29] console: Add through pagination request --- .../console/store/actions/user-preferences.js | 10 ++++ .../console/store/middleware/logics/init.js | 4 +- .../middleware/logics/user-preferences.js | 53 +++++++++++++------ .../store/reducers/user-preferences.js | 6 ++- 4 files changed, 55 insertions(+), 18 deletions(-) diff --git a/pkg/webui/console/store/actions/user-preferences.js b/pkg/webui/console/store/actions/user-preferences.js index ef172fa152..1d832c0f4c 100644 --- a/pkg/webui/console/store/actions/user-preferences.js +++ b/pkg/webui/console/store/actions/user-preferences.js @@ -28,6 +28,16 @@ export const [ { request: getBookmarksList, success: getBookmarksListSuccess, failure: getBookmarksListFailure }, ] = createPaginationByIdRequestActions('BOOKMARKS') +export const GET_ALL_BOOKMARKS_BASE = 'GET_ALL_BOOKMARKS' +export const [ + { + request: GET_ALL_BOOKMARKS, + success: GET_ALL_BOOKMARKS_SUCCESS, + failure: GET_ALL_BOOKMARKS_FAILURE, + }, + { request: getAllBookmarks, success: getAllBookmarksSuccess, failure: getAllBookmarksFailure }, +] = createRequestActions(GET_ALL_BOOKMARKS_BASE, id => ({ id })) + export const ADD_BOOKMARK_BASE = 'ADD_BOOKMARK' export const [ { request: ADD_BOOKMARK, success: ADD_BOOKMARK_SUCCESS, failure: ADD_BOOKMARK_FAILURE }, diff --git a/pkg/webui/console/store/middleware/logics/init.js b/pkg/webui/console/store/middleware/logics/init.js index 4eb37668ae..038f5d64ce 100644 --- a/pkg/webui/console/store/middleware/logics/init.js +++ b/pkg/webui/console/store/middleware/logics/init.js @@ -25,7 +25,7 @@ import { getInboxNotifications, getUnseenNotificationsPeriodically, } from '@console/store/actions/notifications' -import { getBookmarksList } from '@console/store/actions/user-preferences' +import { getAllBookmarks } from '@console/store/actions/user-preferences' const consoleAppLogic = createRequestLogic({ type: init.INITIALIZE, @@ -72,7 +72,7 @@ const consoleAppLogic = createRequestLogic({ userResult.isAdmin = info.is_admin || false dispatch(user.getUserMeSuccess(userResult)) dispatch(getInboxNotifications({ page: 1, limit: 3 })) - dispatch(getBookmarksList(userId, { page: 1, limit: 20 })) + dispatch(getAllBookmarks(userId)) dispatch(getUnseenNotificationsPeriodically()) } catch (error) { dispatch(user.getUserMeFailure(error)) diff --git a/pkg/webui/console/store/middleware/logics/user-preferences.js b/pkg/webui/console/store/middleware/logics/user-preferences.js index 4710b372ca..ad403be9f7 100644 --- a/pkg/webui/console/store/middleware/logics/user-preferences.js +++ b/pkg/webui/console/store/middleware/logics/user-preferences.js @@ -18,25 +18,40 @@ import createRequestLogic from '@ttn-lw/lib/store/logics/create-request-logic' import * as userPreferences from '@console/store/actions/user-preferences' -const getPerEntityTotalCountThroughPagination = async (totalCount, userId) => { +const getBookmarksThroughPagination = async userId => { let page = 1 - const limit = 1000 - const result = {} + const limit = 100 + let totalCount = Infinity + let result = { + bookmarks: [], + totalCount: { + totalCount: 0, + perEntityTotalCount: {}, + }, + } while ((page - 1) * limit < totalCount) { - // Get the next page of notifications. + // Get the next page of bookmarks. // eslint-disable-next-line no-await-in-loop const response = await tts.Users.getBookmarks(userId, { page, limit }) - response.bookmarks.forEach(element => { + result = { + bookmarks: [...result.bookmarks, ...response.bookmarks], + totalCount: { + ...result.totalCount, + totalCount: response.totalCount, + perEntityTotalCount: { ...result.totalCount.perEntityTotalCount }, + }, + } + totalCount = response.totalCount + result.bookmarks.forEach(element => { const entityIds = element.entity_ids const entity = Object.keys(entityIds)[0].replace('_ids', '') - if (!result[entity]) { - result[entity] = 1 + if (!result.totalCount.perEntityTotalCount[entity]) { + result.totalCount.perEntityTotalCount[entity] = 1 } else { - result[entity] += 1 + result.totalCount.perEntityTotalCount[entity] += 1 } }) - page += 1 } @@ -51,13 +66,21 @@ const getBookmarksListLogic = createRequestLogic({ params: { page, limit, order, deleted }, } = action.payload const data = await tts.Users.getBookmarks(userId, { page, limit, order, deleted }) - const perEntityTotalCount = await getPerEntityTotalCountThroughPagination( - data.totalCount, - userId, - ) return { entities: data.bookmarks, - totalCount: { totalCount: data.totalCount, perEntityTotalCount }, + totalCount: data.totalCount, + } + }, +}) + +const getAllBookmarksLogic = createRequestLogic({ + type: userPreferences.GET_ALL_BOOKMARKS, + process: async ({ action }) => { + const { id: userId } = action.payload + const data = await getBookmarksThroughPagination(userId) + return { + entities: data.bookmarks, + totalCount: data.totalCount, } }, }) @@ -84,4 +107,4 @@ const deleteBookmarkLogic = createRequestLogic({ }, }) -export default [getBookmarksListLogic, addBookmarkLogic, deleteBookmarkLogic] +export default [getBookmarksListLogic, getAllBookmarksLogic, addBookmarkLogic, deleteBookmarkLogic] diff --git a/pkg/webui/console/store/reducers/user-preferences.js b/pkg/webui/console/store/reducers/user-preferences.js index d19c4268b2..f99814caa5 100644 --- a/pkg/webui/console/store/reducers/user-preferences.js +++ b/pkg/webui/console/store/reducers/user-preferences.js @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { GET_BOOKMARKS_LIST_SUCCESS } from '@console/store/actions/user-preferences' +import { + GET_ALL_BOOKMARKS_SUCCESS, + GET_BOOKMARKS_LIST_SUCCESS, +} from '@console/store/actions/user-preferences' import { GET_USER_ME_SUCCESS } from '@console/store/actions/logout' const initialState = { @@ -25,6 +28,7 @@ const initialState = { const userPreferences = (state = initialState, { type, payload }) => { switch (type) { + case GET_ALL_BOOKMARKS_SUCCESS: case GET_BOOKMARKS_LIST_SUCCESS: return { ...state, From b0872a5f9d1967331b73c19023288b6d4961ed52 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Thu, 4 Apr 2024 16:06:37 +0200 Subject: [PATCH 13/29] console: Fix dropdown --- pkg/webui/components/header/header.styl | 1 + pkg/webui/components/table/row/index.js | 2 +- pkg/webui/console/containers/header/bookmarks-dropdown.js | 8 +++++--- pkg/webui/console/containers/header/header.styl | 4 ++++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/webui/components/header/header.styl b/pkg/webui/components/header/header.styl index be5f834d93..35ca7aa716 100644 --- a/pkg/webui/components/header/header.styl +++ b/pkg/webui/components/header/header.styl @@ -32,6 +32,7 @@ .bookmarks-dropdown max-width: 19.3rem width: 19.3rem // Avoids flash of small container when dropdown is opened + padding: 0 .logo height: $cs.l diff --git a/pkg/webui/components/table/row/index.js b/pkg/webui/components/table/row/index.js index 11e5b235b6..e71de95d44 100644 --- a/pkg/webui/components/table/row/index.js +++ b/pkg/webui/components/table/row/index.js @@ -96,7 +96,7 @@ Row.propTypes = { /** A flag indicating whether the row is wrapping the head of a table. */ head: PropTypes.bool, /** The identifier of the row. */ - id: PropTypes.number, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), /** The href to be passed as `to` prop to the `` component that wraps the row. */ linkTo: PropTypes.string, /** diff --git a/pkg/webui/console/containers/header/bookmarks-dropdown.js b/pkg/webui/console/containers/header/bookmarks-dropdown.js index cf7c165941..e971cbfd38 100644 --- a/pkg/webui/console/containers/header/bookmarks-dropdown.js +++ b/pkg/webui/console/containers/header/bookmarks-dropdown.js @@ -68,9 +68,11 @@ const BookmarksDropdown = () => {
) : ( <> - {dropdownItems.slice(0, 15).map(bookmark => ( - - ))} +
+ {dropdownItems.slice(0, 15).map((bookmark, index) => ( + + ))} +
{dropdownItems.length > 15 && (
diff --git a/pkg/webui/console/containers/header/header.styl b/pkg/webui/console/containers/header/header.styl index c4e9cd12a7..b57fd82fbd 100644 --- a/pkg/webui/console/containers/header/header.styl +++ b/pkg/webui/console/containers/header/header.styl @@ -63,3 +63,7 @@ .bookmark overflow: hidden text-overflow: ellipsis + + &-container + padding: $cs.xs + border-bottom: 1px solid var(--c-border-neutral-light) From b38329da44eb0a43044eea042c0a9ab9c90d36c1 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Thu, 4 Apr 2024 16:10:44 +0200 Subject: [PATCH 14/29] console: Add order to bookmarks request --- .../top-entities-dashboard-panel/list.js | 7 +-- .../console/store/middleware/logics/init.js | 2 - .../middleware/logics/user-preferences.js | 18 ++++++-- .../store/reducers/user-preferences.js | 46 ++++++++++++++++++- pkg/webui/lib/hooks/use-bookmark.js | 7 +-- 5 files changed, 68 insertions(+), 12 deletions(-) diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/list.js b/pkg/webui/console/containers/top-entities-dashboard-panel/list.js index bab0b9709c..a7543684ea 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/list.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/list.js @@ -50,10 +50,11 @@ const EntitiesList = ({ }) => { const items = useSelector(itemsSelector) const itemsTotalCount = useSelector(state => itemsCountSelector(state, entity)) + const showScrollIndicator = itemsTotalCount > 5 const hasNextPage = items.length < itemsTotalCount const EntitiesItemComponent = EntitiesItemProp ?? EntitiesItem - const itemCount = itemsTotalCount + const itemCount = itemsTotalCount >= 0 ? itemsTotalCount : 100 const isItemLoaded = useCallback( index => (items.length > 0 ? !hasNextPage || index < items.length : false), @@ -66,7 +67,7 @@ const EntitiesList = ({ 5} + last={index === itemsTotalCount - 1 && showScrollIndicator} />
) : ( @@ -134,7 +135,7 @@ const EntitiesList = ({ > {Item} - {itemsTotalCount > 5 &&
} + {showScrollIndicator &&
} )} diff --git a/pkg/webui/console/store/middleware/logics/init.js b/pkg/webui/console/store/middleware/logics/init.js index 038f5d64ce..6d0f6608cc 100644 --- a/pkg/webui/console/store/middleware/logics/init.js +++ b/pkg/webui/console/store/middleware/logics/init.js @@ -25,7 +25,6 @@ import { getInboxNotifications, getUnseenNotificationsPeriodically, } from '@console/store/actions/notifications' -import { getAllBookmarks } from '@console/store/actions/user-preferences' const consoleAppLogic = createRequestLogic({ type: init.INITIALIZE, @@ -72,7 +71,6 @@ const consoleAppLogic = createRequestLogic({ userResult.isAdmin = info.is_admin || false dispatch(user.getUserMeSuccess(userResult)) dispatch(getInboxNotifications({ page: 1, limit: 3 })) - dispatch(getAllBookmarks(userId)) dispatch(getUnseenNotificationsPeriodically()) } catch (error) { dispatch(user.getUserMeFailure(error)) diff --git a/pkg/webui/console/store/middleware/logics/user-preferences.js b/pkg/webui/console/store/middleware/logics/user-preferences.js index ad403be9f7..f370ee7024 100644 --- a/pkg/webui/console/store/middleware/logics/user-preferences.js +++ b/pkg/webui/console/store/middleware/logics/user-preferences.js @@ -33,7 +33,11 @@ const getBookmarksThroughPagination = async userId => { while ((page - 1) * limit < totalCount) { // Get the next page of bookmarks. // eslint-disable-next-line no-await-in-loop - const response = await tts.Users.getBookmarks(userId, { page, limit }) + const response = await tts.Users.getBookmarks( + userId, + { page, limit, order: '-created_at' }, + 'created_at', + ) result = { bookmarks: [...result.bookmarks, ...response.bookmarks], totalCount: { @@ -63,12 +67,20 @@ const getBookmarksListLogic = createRequestLogic({ process: async ({ action }) => { const { id: userId, - params: { page, limit, order, deleted }, + params: { page, limit, deleted }, } = action.payload - const data = await tts.Users.getBookmarks(userId, { page, limit, order, deleted }) + + const data = await tts.Users.getBookmarks(userId, { + page, + limit, + order: '-created_at', + deleted, + }) return { entities: data.bookmarks, totalCount: data.totalCount, + page, + limit, } }, }) diff --git a/pkg/webui/console/store/reducers/user-preferences.js b/pkg/webui/console/store/reducers/user-preferences.js index f99814caa5..12d10c4def 100644 --- a/pkg/webui/console/store/reducers/user-preferences.js +++ b/pkg/webui/console/store/reducers/user-preferences.js @@ -13,11 +13,28 @@ // limitations under the License. import { + ADD_BOOKMARK_SUCCESS, GET_ALL_BOOKMARKS_SUCCESS, GET_BOOKMARKS_LIST_SUCCESS, } from '@console/store/actions/user-preferences' import { GET_USER_ME_SUCCESS } from '@console/store/actions/logout' +// Update a range of values in an array by using another array and a start index. +const fillIntoArray = (array, start, values, totalCount) => { + const newArray = [...array] + const end = Math.min(start + values.length, totalCount) + for (let i = start; i < end; i++) { + newArray[i] = values[i - start] + } + return newArray +} + +const pageToIndices = (page, limit) => { + const startIndex = (page - 1) * limit + const stopIndex = page * limit - 1 + return [startIndex, stopIndex] +} + const initialState = { bookmarks: { bookmarks: [], @@ -29,7 +46,6 @@ const initialState = { const userPreferences = (state = initialState, { type, payload }) => { switch (type) { case GET_ALL_BOOKMARKS_SUCCESS: - case GET_BOOKMARKS_LIST_SUCCESS: return { ...state, bookmarks: { @@ -38,6 +54,34 @@ const userPreferences = (state = initialState, { type, payload }) => { totalCount: payload.totalCount, }, } + case GET_BOOKMARKS_LIST_SUCCESS: + return { + ...state, + bookmarks: { + ...state.bookmarks, + bookmarks: fillIntoArray( + state.bookmarks.bookmarks, + pageToIndices(payload.page, payload.limit)[0], + payload.entities, + payload.totalCount.totalCount, + ), + totalCount: { + ...state.bookmarks.totalCount, + totalCount: payload.totalCount, + }, + }, + } + case ADD_BOOKMARK_SUCCESS: + return { + ...state, + bookmarks: { + ...state.bookmarks, + totalCount: { + ...state.bookmarks.totalCount, + totalCount: state.bookmarks.totalCount.totalCount + 1, + }, + }, + } case GET_USER_ME_SUCCESS: return { ...state, diff --git a/pkg/webui/lib/hooks/use-bookmark.js b/pkg/webui/lib/hooks/use-bookmark.js index 40b9c63418..345917b26e 100644 --- a/pkg/webui/lib/hooks/use-bookmark.js +++ b/pkg/webui/lib/hooks/use-bookmark.js @@ -106,10 +106,11 @@ const useBookmark = bookmark => { } // Only fetch the entity if the name is not already in the store. - if (typeof bookmarkTitle !== 'string') { + if (!bookmarkTitle) { fetchEntity() } - }, [entity, entityId, dispatch, entityIds, bookmarkTitle]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) // Get the icon corresponding to the entity. const icon = iconMap[entity] @@ -119,7 +120,7 @@ const useBookmark = bookmark => { ? `/applications/${entityIds.device_ids.application_ids.application_id}/devices/${entityIds.device_ids.device_id}` : `/${entity}s/${entityId.id}` - return { title: bookmarkTitle ?? '', ids: entityId, path, icon } + return { title: bookmarkTitle ?? 'Fetching bookmark...', ids: entityId, path, icon } } export default useBookmark From b779ccdb17038cfccbcf6db142b868c68e60ba11 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Thu, 4 Apr 2024 16:11:22 +0200 Subject: [PATCH 15/29] console: Fetch bookmarks in the sidebar --- .../navigation/app-list-side-navigation.js | 12 +++-- .../navigation/general-side-navigation.js | 42 +++++++++-------- .../navigation/gtw-list-side-navigation.js | 12 +++-- .../top-entities-dashboard-panel/index.js | 45 ++++++------------- 4 files changed, 55 insertions(+), 56 deletions(-) diff --git a/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js b/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js index 7ce8c7a774..0d4b6c66b0 100644 --- a/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js +++ b/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js @@ -20,11 +20,16 @@ import SectionLabel from '@ttn-lw/components/sidebar/section-label' import SideNavigation from '@ttn-lw/components/sidebar/side-menu' import Button from '@ttn-lw/components/button' +import RequireRequest from '@ttn-lw/lib/components/require-request' + import sharedMessages from '@ttn-lw/lib/shared-messages' import PropTypes from '@ttn-lw/lib/prop-types' import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' +import { getAllBookmarks } from '@console/store/actions/user-preferences' + import { selectApplicationBookmarks } from '@console/store/selectors/user-preferences' +import { selectUserId } from '@console/store/selectors/logout' import SidebarContext from '../context' @@ -42,6 +47,7 @@ const AppListSideNavigation = () => { const [showMore, setShowMore] = useState(false) const topEntities = useSelector(state => selectApplicationBookmarks(state)) const { isMinimized } = useContext(SidebarContext) + const userId = useSelector(selectUserId) const handleShowMore = useCallback(async () => { setShowMore(showMore => !showMore) @@ -55,9 +61,9 @@ const AppListSideNavigation = () => { } return ( -
- null} /> + + null} /> {topEntities.slice(0, 6).map(bookmark => ( ))} @@ -74,7 +80,7 @@ const AppListSideNavigation = () => { /> )} -
+ ) } diff --git a/pkg/webui/console/containers/sidebar/navigation/general-side-navigation.js b/pkg/webui/console/containers/sidebar/navigation/general-side-navigation.js index 4e1a6b87bc..ac7f1857ee 100644 --- a/pkg/webui/console/containers/sidebar/navigation/general-side-navigation.js +++ b/pkg/webui/console/containers/sidebar/navigation/general-side-navigation.js @@ -27,6 +27,8 @@ import SideNavigation from '@ttn-lw/components/sidebar/side-menu' import SectionLabel from '@ttn-lw/components/sidebar/section-label' import Button from '@ttn-lw/components/button' +import RequireRequest from '@ttn-lw/lib/components/require-request' + import sharedMessages from '@ttn-lw/lib/shared-messages' import PropTypes from '@ttn-lw/lib/prop-types' import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' @@ -37,6 +39,8 @@ import { mayViewOrganizationsOfUser, } from '@console/lib/feature-checks' +import { getAllBookmarks } from '@console/store/actions/user-preferences' + import { selectUser, selectUserIsAdmin } from '@console/store/selectors/logout' import { selectBookmarksList } from '@console/store/selectors/user-preferences' @@ -106,24 +110,26 @@ const GeneralSideNavigation = () => { )} {!isMinimized && topEntities.length > 0 && ( - - null} /> - {topEntities.slice(0, 6).map(bookmark => ( - - ))} - {showMore && - topEntities.length > 6 && - topEntities - .slice(6, topEntities.length) - .map(bookmark => )} - {topEntities.length > 6 && ( -
-) +}) => { + const user = useSelector(selectUser) + const mayViewApps = useSelector(state => + user ? checkFromState(mayViewApplications, state) : false, + ) + const mayViewGtws = useSelector(state => (user ? checkFromState(mayViewGateways, state) : false)) + const mayViewOrgs = useSelector(state => + user ? checkFromState(mayViewOrganizationsOfUser, state) : false, + ) + + const plusDropdownItems = ( + <> + {mayViewApps && ( + + )} + {mayViewGtws && ( + + )} + {mayViewOrgs && ( + + )} + + + + ) + + return ( +
+ +
+ ) +} SectionLabel.propTypes = { buttonDisabled: PropTypes.bool, diff --git a/pkg/webui/console/containers/header/index.js b/pkg/webui/console/containers/header/index.js index c486c5f29d..4e3fd513a3 100644 --- a/pkg/webui/console/containers/header/index.js +++ b/pkg/webui/console/containers/header/index.js @@ -14,7 +14,6 @@ import React, { useCallback } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { defineMessages } from 'react-intl' import { IconLogout, @@ -52,12 +51,6 @@ import Logo from '../logo' import NotificationsDropdown from './notifications-dropdown' import BookmarksDropdown from './bookmarks-dropdown' -const m = defineMessages({ - addApplication: 'Add new application', - addGateway: 'Add new gateway', - addOrganization: 'Add new organization', -}) - const accountUrl = selectAccountUrl() const Header = ({ onMenuClick }) => { @@ -78,14 +71,18 @@ const Header = ({ onMenuClick }) => { const plusDropdownItems = ( <> {mayViewApps && ( - + )} {mayViewGtws && ( - + )} {mayViewOrgs && ( diff --git a/pkg/webui/console/containers/shortcut-panel/index.js b/pkg/webui/console/containers/shortcut-panel/index.js index 133e06de3c..8fc6a1e8de 100644 --- a/pkg/webui/console/containers/shortcut-panel/index.js +++ b/pkg/webui/console/containers/shortcut-panel/index.js @@ -32,7 +32,7 @@ const m = defineMessages({ shortcuts: 'Quick actions', addApplication: 'New Application', addGateway: 'New Gateway', - addOrganization: 'New Organization', + addNewOrganization: 'New Organization', addPersonalApiKey: 'New personal API key', registerDevice: 'Register a device', }) @@ -54,7 +54,7 @@ const ShortcutPanel = () => ( /> diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js index 0a983371a6..6e06f50667 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js @@ -13,6 +13,7 @@ // limitations under the License. import React from 'react' +import { defineMessages } from 'react-intl' import Icon from '@ttn-lw/components/icon' @@ -27,15 +28,23 @@ import { import EntitiesList from '../list' +const m = defineMessages({ + noTopEntities: 'No top entities yet', + noTopEntitiesDescription: 'Your most visited, and bookmarked entities will be listed here.', +}) + const AllTopEntitiesList = ({ loadNextPage }) => { const headers = [ { - name: 'icon', + name: 'type', + displayName: 'Type', + width: 7, render: icon => , }, { name: 'name', displayName: 'Name', + align: 'left', render: (name, id) => , }, ] @@ -46,8 +55,8 @@ const AllTopEntitiesList = ({ loadNextPage }) => { itemsCountSelector={selectBookmarksTotalCount} itemsSelector={selectBookmarksList} headers={headers} - emptyMessage={'No top entities yet'} - emptyDescription={'Your most visited, and bookmarked entities will be listed here.'} + emptyMessage={m.noTopEntities} + emptyDescription={m.noTopEntitiesDescription} /> ) } diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js index bdf38003f6..839257f172 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js @@ -43,25 +43,25 @@ const TopApplicationsItem = ({ bookmark, headers, last }) => { ) return ( - - - {headers.map((header, index) => { - const value = headers[index].name === 'name' ? title : deviceCount - const entityID = ids.id - return ( - + + {headers.map((header, index) => { + const value = headers[index].name === 'name' ? title : deviceCount + const entityID = ids.id + return ( + + {headers[index].render(value, entityID)} - ) - })} - - + + ) + })} + ) } From b9a23ccea0dd15a7b7c6a222081972dc9ad4d218 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Mon, 15 Apr 2024 12:49:24 +0200 Subject: [PATCH 20/29] console: Avoid repetition --- .../navigation/app-list-side-navigation.js | 53 +------------ .../navigation/general-side-navigation.js | 47 +----------- .../navigation/gtw-list-side-navigation.js | 40 +--------- .../navigation/top-entities-section.js | 76 +++++++++++++++++++ .../top-entities-dashboard-panel/item.js | 11 ++- .../top-entities-panel.styl | 6 +- 6 files changed, 100 insertions(+), 133 deletions(-) create mode 100644 pkg/webui/console/containers/sidebar/navigation/top-entities-section.js diff --git a/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js b/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js index 0d4b6c66b0..c764b5a700 100644 --- a/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js +++ b/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js @@ -12,47 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useCallback, useContext, useState } from 'react' +import React, { useContext } from 'react' import { useSelector } from 'react-redux' -import { IconPlus } from '@ttn-lw/components/icon' -import SectionLabel from '@ttn-lw/components/sidebar/section-label' -import SideNavigation from '@ttn-lw/components/sidebar/side-menu' -import Button from '@ttn-lw/components/button' - -import RequireRequest from '@ttn-lw/lib/components/require-request' - -import sharedMessages from '@ttn-lw/lib/shared-messages' -import PropTypes from '@ttn-lw/lib/prop-types' -import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' - -import { getAllBookmarks } from '@console/store/actions/user-preferences' - import { selectApplicationBookmarks } from '@console/store/selectors/user-preferences' import { selectUserId } from '@console/store/selectors/logout' import SidebarContext from '../context' -const Bookmark = ({ bookmark }) => { - const { title, ids, path, icon } = useBookmark(bookmark) - - return -} - -Bookmark.propTypes = { - bookmark: PropTypes.shape({}).isRequired, -} +import TopEntitiesSection from './top-entities-section' const AppListSideNavigation = () => { - const [showMore, setShowMore] = useState(false) const topEntities = useSelector(state => selectApplicationBookmarks(state)) const { isMinimized } = useContext(SidebarContext) const userId = useSelector(selectUserId) - const handleShowMore = useCallback(async () => { - setShowMore(showMore => !showMore) - }, []) - if (isMinimized || topEntities.length === 0) { // Rendering an empty div to prevent the shadow of the search bar // from being cut off. There will be a default element rendering @@ -60,28 +34,7 @@ const AppListSideNavigation = () => { return
} - return ( - - - null} /> - {topEntities.slice(0, 6).map(bookmark => ( - - ))} - {showMore && - topEntities.length > 6 && - topEntities - .slice(6, topEntities.length) - .map(bookmark => )} - {topEntities.length > 6 && ( -
) diff --git a/pkg/webui/lib/shared-messages.js b/pkg/webui/lib/shared-messages.js index 4dd986e826..f6edc05a8d 100644 --- a/pkg/webui/lib/shared-messages.js +++ b/pkg/webui/lib/shared-messages.js @@ -29,6 +29,9 @@ export default defineMessages({ actions: 'Actions', activationMode: 'Activation mode', add: 'Add', + addApplication: 'Add application', + addGateway: 'Add new gateway', + addOrganization: 'Add new organization', addApiKey: 'Add API key', addAttributes: 'Add attributes', addCollaborator: 'Add collaborator', diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index a99cfa3f84..9397b5ecfd 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -573,9 +573,6 @@ "console.containers.header.bookmarks-dropdown.noBookmarks": "No bookmarks yet", "console.containers.header.bookmarks-dropdown.noBookmarksDescription": "Your bookmarked entities will be listed here", "console.containers.header.bookmarks-dropdown.threshold": "Only showing latest 15 bookmarks", - "console.containers.header.index.addApplication": "Add new application", - "console.containers.header.index.addGateway": "Add new gateway", - "console.containers.header.index.addOrganization": "Add new organization", "console.containers.header.notifications-dropdown.description": "Showing last 3 of {totalNotifications} notifications", "console.containers.header.notifications-dropdown.noNotifications": "All caught up!", "console.containers.header.notifications-dropdown.noNotificationsDescription": "You don’t have any notifications currently", @@ -639,11 +636,13 @@ "console.containers.shortcut-panel.index.shortcuts": "Quick actions", "console.containers.shortcut-panel.index.addApplication": "New Application", "console.containers.shortcut-panel.index.addGateway": "New Gateway", - "console.containers.shortcut-panel.index.addOrganization": "New Organization", + "console.containers.shortcut-panel.index.addNewOrganization": "New Organization", "console.containers.shortcut-panel.index.addPersonalApiKey": "New personal API key", "console.containers.shortcut-panel.index.registerDevice": "Register a device", "console.containers.sidebar.navigation.app-side-navigation.buttonMessage": "Back to Applications list", "console.containers.sidebar.navigation.gtw-side-navigation.buttonMessage": "Back to Gateways list", + "console.containers.top-entities-dashboard-panel.all-top-entities.index.noTopEntities": "No top entities yet", + "console.containers.top-entities-dashboard-panel.all-top-entities.index.noTopEntitiesDescription": "Your most visited, and bookmarked entities will be listed here.", "console.containers.top-entities-dashboard-panel.index.title": "Your top entities", "console.containers.top-entities-dashboard-panel.list.empty": "No entities yet", "console.containers.top-entities-dashboard-panel.top-applications.index.emptyMessage": "No top Application yet", @@ -1043,6 +1042,9 @@ "lib.shared-messages.actions": "Actions", "lib.shared-messages.activationMode": "Activation mode", "lib.shared-messages.add": "Add", + "lib.shared-messages.addApplication": "Add application", + "lib.shared-messages.addGateway": "Add new gateway", + "lib.shared-messages.addOrganization": "Add new organization", "lib.shared-messages.addApiKey": "Add API key", "lib.shared-messages.addAttributes": "Add attributes", "lib.shared-messages.addCollaborator": "Add collaborator", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index afde9c7419..1c37b8e01c 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -573,9 +573,6 @@ "console.containers.header.bookmarks-dropdown.noBookmarks": "", "console.containers.header.bookmarks-dropdown.noBookmarksDescription": "", "console.containers.header.bookmarks-dropdown.threshold": "", - "console.containers.header.index.addApplication": "", - "console.containers.header.index.addGateway": "", - "console.containers.header.index.addOrganization": "", "console.containers.header.notifications-dropdown.description": "", "console.containers.header.notifications-dropdown.noNotifications": "", "console.containers.header.notifications-dropdown.noNotificationsDescription": "", @@ -639,11 +636,13 @@ "console.containers.shortcut-panel.index.shortcuts": "", "console.containers.shortcut-panel.index.addApplication": "", "console.containers.shortcut-panel.index.addGateway": "", - "console.containers.shortcut-panel.index.addOrganization": "", + "console.containers.shortcut-panel.index.addNewOrganization": "", "console.containers.shortcut-panel.index.addPersonalApiKey": "", "console.containers.shortcut-panel.index.registerDevice": "", "console.containers.sidebar.navigation.app-side-navigation.buttonMessage": "", "console.containers.sidebar.navigation.gtw-side-navigation.buttonMessage": "", + "console.containers.top-entities-dashboard-panel.all-top-entities.index.noTopEntities": "", + "console.containers.top-entities-dashboard-panel.all-top-entities.index.noTopEntitiesDescription": "", "console.containers.top-entities-dashboard-panel.index.title": "", "console.containers.top-entities-dashboard-panel.list.empty": "", "console.containers.top-entities-dashboard-panel.top-applications.index.emptyMessage": "", @@ -1043,6 +1042,9 @@ "lib.shared-messages.actions": "アクション", "lib.shared-messages.activationMode": "アクティベーションモード", "lib.shared-messages.add": "追加", + "lib.shared-messages.addApplication": "", + "lib.shared-messages.addGateway": "", + "lib.shared-messages.addOrganization": "", "lib.shared-messages.addApiKey": "APIキーの追加", "lib.shared-messages.addAttributes": "属性の追加", "lib.shared-messages.addCollaborator": "コラボレータ―の追加", From 0bbf861becc8eb1e2d831ee3980d7ee6454bca0c Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Mon, 15 Apr 2024 12:59:43 +0200 Subject: [PATCH 22/29] console: Fix header --- .../containers/sidebar/navigation/top-entities-section.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js b/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js index 2b1a9d059d..09debf0ad1 100644 --- a/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js +++ b/pkg/webui/console/containers/sidebar/navigation/top-entities-section.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. From f13b5b7c7febd3555fab597909c1ebd7389305aa Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Mon, 15 Apr 2024 13:07:16 +0200 Subject: [PATCH 23/29] console: Fix messages --- .../top-entities-dashboard-panel/all-top-entities/index.js | 5 +++-- .../console/containers/top-entities-dashboard-panel/item.js | 2 +- .../top-entities-dashboard-panel/top-devices/index.js | 3 ++- .../top-entities-dashboard-panel/top-gateways/index.js | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js index 6e06f50667..63baf55b86 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/all-top-entities/index.js @@ -20,6 +20,7 @@ import Icon from '@ttn-lw/components/icon' import Message from '@ttn-lw/lib/components/message' import PropTypes from '@ttn-lw/lib/prop-types' +import sharedMessages from '@ttn-lw/lib/shared-messages' import { selectBookmarksList, @@ -37,13 +38,13 @@ const AllTopEntitiesList = ({ loadNextPage }) => { const headers = [ { name: 'type', - displayName: 'Type', + displayName: sharedMessages.type, width: 7, render: icon => , }, { name: 'name', - displayName: 'Name', + displayName: sharedMessages.name, align: 'left', render: (name, id) => , }, diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/item.js b/pkg/webui/console/containers/top-entities-dashboard-panel/item.js index 9c35dcef6d..d49a3fec88 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/item.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/item.js @@ -36,7 +36,7 @@ const EntitiesItem = ({ bookmark, headers, last }) => { {headers.map((header, index) => { const value = headers[index].name === 'name' ? title : headers[index].name === 'type' ? icon : '' - const entityID = headers[index].name === 'name' ? ids.id : undefined + const entityID = ids.id return ( { const headers = [ { name: 'name', - displayName: 'Name', + displayName: sharedMessages.name, render: (name, id) => ( <> diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js index 9e691eef1a..cc91ec749c 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js @@ -37,7 +37,7 @@ const TopGatewaysList = ({ loadNextPage }) => { const headers = [ { name: 'name', - displayName: sharedMessages.displayName, + displayName: sharedMessages.name, render: (name, id) => ( <> From 27b25d72c498492e5e975873f0f096a452b4244c Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Wed, 17 Apr 2024 14:46:38 +0200 Subject: [PATCH 24/29] console: Fix messages and add error handling --- .../containers/shortcut-panel/index.js | 8 +++--- .../navigation/top-entities-section.js | 10 +++++++- .../top-applications/index.js | 7 +++--- .../top-applications/item.js | 14 ++++++++++- .../top-devices/index.js | 4 +-- .../top-gateways/index.js | 6 ++--- pkg/webui/locales/en.json | 25 ++++++++++--------- pkg/webui/locales/ja.json | 3 ++- 8 files changed, 49 insertions(+), 28 deletions(-) diff --git a/pkg/webui/console/containers/shortcut-panel/index.js b/pkg/webui/console/containers/shortcut-panel/index.js index 8fc6a1e8de..44e2fa69ea 100644 --- a/pkg/webui/console/containers/shortcut-panel/index.js +++ b/pkg/webui/console/containers/shortcut-panel/index.js @@ -30,11 +30,11 @@ import ShortcutItem from './shortcut-item' const m = defineMessages({ shortcuts: 'Quick actions', - addApplication: 'New Application', - addGateway: 'New Gateway', - addNewOrganization: 'New Organization', + addApplication: 'New application', + addGateway: 'New gateway', + addNewOrganization: 'New organization', addPersonalApiKey: 'New personal API key', - registerDevice: 'Register a device', + registerDevice: 'Register an end device', }) const ShortcutPanel = () => ( diff --git a/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js b/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js index 09debf0ad1..f67b3ab1fe 100644 --- a/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js +++ b/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js @@ -13,6 +13,7 @@ // limitations under the License. import React, { useCallback, useState } from 'react' +import { defineMessages } from 'react-intl' import { IconPlus } from '@ttn-lw/components/icon' import Button from '@ttn-lw/components/button' @@ -20,6 +21,7 @@ import SideNavigation from '@ttn-lw/components/sidebar/side-menu' import SectionLabel from '@ttn-lw/components/sidebar/section-label' import RequireRequest from '@ttn-lw/lib/components/require-request' +import Message from '@ttn-lw/lib/components/message' import sharedMessages from '@ttn-lw/lib/shared-messages' import PropTypes from '@ttn-lw/lib/prop-types' @@ -27,6 +29,10 @@ import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' import { getAllBookmarks } from '@console/store/actions/user-preferences' +const m = defineMessages({ + errorMessage: 'Top entities cannot be provided', +}) + const Bookmark = ({ bookmark }) => { const { title, ids, path, icon } = useBookmark(bookmark) @@ -44,8 +50,10 @@ const TopEntitiesSection = ({ topEntities, userId }) => { setShowMore(showMore => !showMore) }, []) + const errorFunction = () => + return ( - + null} /> {topEntities.slice(0, 6).map(bookmark => ( diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/index.js index f0d06dc0dd..c81e757fd7 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/index.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/index.js @@ -32,9 +32,8 @@ import EntitiesList from '../list' import TopApplicationsItem from './item' const m = defineMessages({ - emptyMessage: 'No top Application yet', - emptyDescription: 'Your most visited, and bookmarked Applications will be listed here.', - emptyAction: 'Create Application', + emptyMessage: 'No top application yet', + emptyDescription: 'Your most visited, and bookmarked applications will be listed here', }) const TopApplicationsList = ({ loadNextPage }) => { @@ -74,7 +73,7 @@ const TopApplicationsList = ({ loadNextPage }) => { EntitiesItemComponent={TopApplicationsItem} emptyMessage={m.emptyMessage} emptyDescription={m.emptyDescription} - emptyAction={m.emptyAction} + emptyAction={sharedMessages.createApplication} emptyPath={'/applications/add'} entity={'application'} /> diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js index 839257f172..81a337c330 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-applications/item.js @@ -15,10 +15,12 @@ import React, { useCallback } from 'react' import { useSelector } from 'react-redux' import classNames from 'classnames' +import { defineMessages } from 'react-intl' import { Table } from '@ttn-lw/components/table' import RequireRequest from '@ttn-lw/lib/components/require-request' +import Message from '@ttn-lw/lib/components/message' import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' import PropTypes from '@ttn-lw/lib/prop-types' @@ -29,6 +31,10 @@ import { selectApplicationDeviceCount } from '@console/store/selectors/applicati import styles from '../top-entities-panel.styl' +const m = defineMessages({ + errorMessage: 'Not available', +}) + const TopApplicationsItem = ({ bookmark, headers, last }) => { const { title, ids, path } = useBookmark(bookmark) const deviceCount = useSelector(state => selectApplicationDeviceCount(state, ids.id)) @@ -42,6 +48,8 @@ const TopApplicationsItem = ({ bookmark, headers, last }) => { [deviceCount, ids.id], ) + const errorRenderFunction = () => + return ( { const value = headers[index].name === 'name' ? title : deviceCount const entityID = ids.id return ( - + {headers[index].render(value, entityID)} diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js index 8fdeb20aa6..a95af510f5 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-devices/index.js @@ -28,8 +28,8 @@ import { import EntitiesList from '../list' const m = defineMessages({ - emptyMessage: 'No top Device yet', - emptyDescription: 'Your most visited, and bookmarked Devices will be listed here.', + emptyMessage: 'No top device yet', + emptyDescription: 'Your most visited, and bookmarked end devices will be listed here', }) const TopDevicesList = ({ loadNextPage }) => { diff --git a/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js b/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js index cc91ec749c..9421fa88c4 100644 --- a/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js +++ b/pkg/webui/console/containers/top-entities-dashboard-panel/top-gateways/index.js @@ -28,9 +28,9 @@ import { import EntitiesList from '../list' const m = defineMessages({ - emptyMessage: 'No top Gateway yet', - emptyDescription: 'Your most visited, and bookmarked Gateways will be listed here.', - emptyAction: 'Create Gateway', + emptyMessage: 'No top gateway yet', + emptyDescription: 'Your most visited, and bookmarked gateways will be listed here', + emptyAction: 'Create gateway', }) const TopGatewaysList = ({ loadNextPage }) => { diff --git a/pkg/webui/locales/en.json b/pkg/webui/locales/en.json index 9397b5ecfd..b8910c6019 100644 --- a/pkg/webui/locales/en.json +++ b/pkg/webui/locales/en.json @@ -634,25 +634,26 @@ "console.containers.pubsub-formats-select.index.warning": "Pub/Sub formats unavailable", "console.containers.pubsubs-table.index.host": "Server host", "console.containers.shortcut-panel.index.shortcuts": "Quick actions", - "console.containers.shortcut-panel.index.addApplication": "New Application", - "console.containers.shortcut-panel.index.addGateway": "New Gateway", - "console.containers.shortcut-panel.index.addNewOrganization": "New Organization", + "console.containers.shortcut-panel.index.addApplication": "New application", + "console.containers.shortcut-panel.index.addGateway": "New gateway", + "console.containers.shortcut-panel.index.addNewOrganization": "New organization", "console.containers.shortcut-panel.index.addPersonalApiKey": "New personal API key", - "console.containers.shortcut-panel.index.registerDevice": "Register a device", + "console.containers.shortcut-panel.index.registerDevice": "Register an end device", "console.containers.sidebar.navigation.app-side-navigation.buttonMessage": "Back to Applications list", "console.containers.sidebar.navigation.gtw-side-navigation.buttonMessage": "Back to Gateways list", + "console.containers.sidebar.navigation.top-entities-section.errorMessage": "Top entities cannot be provided", "console.containers.top-entities-dashboard-panel.all-top-entities.index.noTopEntities": "No top entities yet", "console.containers.top-entities-dashboard-panel.all-top-entities.index.noTopEntitiesDescription": "Your most visited, and bookmarked entities will be listed here.", "console.containers.top-entities-dashboard-panel.index.title": "Your top entities", "console.containers.top-entities-dashboard-panel.list.empty": "No entities yet", - "console.containers.top-entities-dashboard-panel.top-applications.index.emptyMessage": "No top Application yet", - "console.containers.top-entities-dashboard-panel.top-applications.index.emptyDescription": "Your most visited, and bookmarked Applications will be listed here.", - "console.containers.top-entities-dashboard-panel.top-applications.index.emptyAction": "Create Application", - "console.containers.top-entities-dashboard-panel.top-devices.index.emptyMessage": "No top Device yet", - "console.containers.top-entities-dashboard-panel.top-devices.index.emptyDescription": "Your most visited, and bookmarked Devices will be listed here.", - "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyMessage": "No top Gateway yet", - "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyDescription": "Your most visited, and bookmarked Gateways will be listed here.", - "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyAction": "Create Gateway", + "console.containers.top-entities-dashboard-panel.top-applications.index.emptyMessage": "No top application yet", + "console.containers.top-entities-dashboard-panel.top-applications.index.emptyDescription": "Your most visited, and bookmarked applications will be listed here", + "console.containers.top-entities-dashboard-panel.top-applications.item.errorMessage": "Not available", + "console.containers.top-entities-dashboard-panel.top-devices.index.emptyMessage": "No top device yet", + "console.containers.top-entities-dashboard-panel.top-devices.index.emptyDescription": "Your most visited, and bookmarked end devices will be listed here", + "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyMessage": "No top gateway yet", + "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyDescription": "Your most visited, and bookmarked gateways will be listed here", + "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyAction": "Create gateway", "console.containers.user-data-form.edit.deleteWarning": "This will PERMANENTLY DELETE THIS ACCOUNT and LOCK THE USER ID AND EMAIL FOR RE-REGISTRATION. Associated entities (e.g. gateways, applications and end devices) owned by this user that do not have any other collaborators will become UNACCESSIBLE and it will NOT BE POSSIBLE TO REGISTER ENTITIES WITH THE SAME ID OR EUI's AGAIN. Make sure you assign new collaborators to such entities if you plan to continue using them.", "console.containers.user-data-form.edit.purgeWarning": "This will PERMANENTLY DELETE THIS ACCOUNT. Associated entities (e.g. gateways, applications and end devices) owned by this user that do not have any other collaborators will become UNACCESSIBLE and it will NOT BE POSSIBLE TO REGISTER ENTITIES WITH THE SAME ID OR EUI's AGAIN. Make sure you assign new collaborators to such entities if you plan to continue using them.", "console.containers.user-data-form.edit.deleteConfirmMessage": "Please type in this user's user ID to confirm.", diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index 1c37b8e01c..0a0e405925 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -641,13 +641,14 @@ "console.containers.shortcut-panel.index.registerDevice": "", "console.containers.sidebar.navigation.app-side-navigation.buttonMessage": "", "console.containers.sidebar.navigation.gtw-side-navigation.buttonMessage": "", + "console.containers.sidebar.navigation.top-entities-section.errorMessage": "", "console.containers.top-entities-dashboard-panel.all-top-entities.index.noTopEntities": "", "console.containers.top-entities-dashboard-panel.all-top-entities.index.noTopEntitiesDescription": "", "console.containers.top-entities-dashboard-panel.index.title": "", "console.containers.top-entities-dashboard-panel.list.empty": "", "console.containers.top-entities-dashboard-panel.top-applications.index.emptyMessage": "", "console.containers.top-entities-dashboard-panel.top-applications.index.emptyDescription": "", - "console.containers.top-entities-dashboard-panel.top-applications.index.emptyAction": "", + "console.containers.top-entities-dashboard-panel.top-applications.item.errorMessage": "", "console.containers.top-entities-dashboard-panel.top-devices.index.emptyMessage": "", "console.containers.top-entities-dashboard-panel.top-devices.index.emptyDescription": "", "console.containers.top-entities-dashboard-panel.top-gateways.index.emptyMessage": "", From f73fe050bd6ed2a6832361fb12e34127b4272d90 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Fri, 19 Apr 2024 11:35:21 +0200 Subject: [PATCH 25/29] console: Fix breadcrumbs --- pkg/webui/components/breadcrumbs/context.js | 2 +- pkg/webui/console/views/application/index.js | 28 ++++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/pkg/webui/components/breadcrumbs/context.js b/pkg/webui/components/breadcrumbs/context.js index ba6e6334a2..9c82336786 100644 --- a/pkg/webui/components/breadcrumbs/context.js +++ b/pkg/webui/components/breadcrumbs/context.js @@ -60,7 +60,7 @@ const useBreadcrumbs = (id, element) => { context.remove(id) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [id]) } export { Consumer as BreadcrumbsConsumer, BreadcrumbsProvider, BreadcrumbsContext, useBreadcrumbs } diff --git a/pkg/webui/console/views/application/index.js b/pkg/webui/console/views/application/index.js index e873db883d..8426d0f157 100644 --- a/pkg/webui/console/views/application/index.js +++ b/pkg/webui/console/views/application/index.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useEffect } from 'react' +import React, { useEffect, useMemo } from 'react' import { Routes, Route, useParams } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' @@ -56,14 +56,17 @@ import { const Application = () => { const { appId } = useParams() - const actions = [ - getApplication( - appId, - 'name,description,attributes,dev_eui_counter,network_server_address,application_server_address,join_server_address,administrative_contact,technical_contact', - ), - getApplicationsRightsList(appId), - getAsConfiguration(), - ] + const actions = useMemo( + () => [ + getApplication( + appId, + 'name,description,attributes,dev_eui_counter,network_server_address,application_server_address,join_server_address,administrative_contact,technical_contact', + ), + getApplicationsRightsList(appId), + getAsConfiguration(), + ], + [appId], + ) // Check whether application still exists after it has been possibly deleted. const application = useSelector(selectSelectedApplication) @@ -71,7 +74,7 @@ const Application = () => { return ( - + {hasApplication && } @@ -90,7 +93,10 @@ const ApplicationInner = () => { const stopStream = React.useCallback(id => dispatch(stopApplicationEventsStream(id)), [dispatch]) useEffect(() => () => stopStream(appId), [appId, stopStream]) - useBreadcrumbs('apps.single', ) + useBreadcrumbs( + `apps.single#${appId}`, + , + ) return ( <> From 37de2eacd1d5b5c8d6314e62e04a0293d64c5db6 Mon Sep 17 00:00:00 2001 From: Darya Plotnytska Date: Sun, 21 Apr 2024 16:28:33 +0200 Subject: [PATCH 26/29] console: Make only one fetch and fill in array of items from store --- .../navigation/app-list-side-navigation.js | 6 +- .../navigation/gtw-list-side-navigation.js | 6 +- .../navigation/top-entities-section.js | 62 +++++------ .../all-top-entities/index.js | 15 ++- .../top-entities-dashboard-panel/index.js | 28 +---- .../top-entities-dashboard-panel/item.js | 2 +- .../top-entities-dashboard-panel/list.js | 32 ++++-- .../top-applications/index.js | 15 ++- .../top-applications/item.js | 2 +- .../top-devices/index.js | 15 ++- .../top-gateways/index.js | 15 ++- .../console/store/middleware/logics/init.js | 2 + .../middleware/logics/user-preferences.js | 101 +++++++++++++++--- .../store/reducers/user-preferences.js | 28 ++++- .../store/selectors/user-preferences.js | 25 +---- pkg/webui/console/store/utils.js | 2 + pkg/webui/containers/fetch-table/index.js | 2 +- pkg/webui/locales/en.json | 1 - pkg/webui/locales/ja.json | 1 - 19 files changed, 209 insertions(+), 151 deletions(-) diff --git a/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js b/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js index c764b5a700..59d9ce5549 100644 --- a/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js +++ b/pkg/webui/console/containers/sidebar/navigation/app-list-side-navigation.js @@ -15,15 +15,15 @@ import React, { useContext } from 'react' import { useSelector } from 'react-redux' -import { selectApplicationBookmarks } from '@console/store/selectors/user-preferences' import { selectUserId } from '@console/store/selectors/logout' +import { selectPerEntityBookmarks } from '@console/store/selectors/user-preferences' import SidebarContext from '../context' import TopEntitiesSection from './top-entities-section' const AppListSideNavigation = () => { - const topEntities = useSelector(state => selectApplicationBookmarks(state)) + const topEntities = useSelector(state => selectPerEntityBookmarks(state, 'application')) const { isMinimized } = useContext(SidebarContext) const userId = useSelector(selectUserId) @@ -34,7 +34,7 @@ const AppListSideNavigation = () => { return
} - return + return } export default AppListSideNavigation diff --git a/pkg/webui/console/containers/sidebar/navigation/gtw-list-side-navigation.js b/pkg/webui/console/containers/sidebar/navigation/gtw-list-side-navigation.js index 866372cbc5..ea69bf246f 100644 --- a/pkg/webui/console/containers/sidebar/navigation/gtw-list-side-navigation.js +++ b/pkg/webui/console/containers/sidebar/navigation/gtw-list-side-navigation.js @@ -20,8 +20,8 @@ import SideNavigation from '@ttn-lw/components/sidebar/side-menu' import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' import PropTypes from '@ttn-lw/lib/prop-types' -import { selectGatewayBookmarks } from '@console/store/selectors/user-preferences' import { selectUserId } from '@console/store/selectors/logout' +import { selectPerEntityBookmarks } from '@console/store/selectors/user-preferences' import SidebarContext from '../context' @@ -38,7 +38,7 @@ Bookmark.propTypes = { } const GtwListSideNavigation = () => { - const topEntities = useSelector(state => selectGatewayBookmarks(state)) + const topEntities = useSelector(state => selectPerEntityBookmarks(state, 'gateway')) const { isMinimized } = useContext(SidebarContext) const userId = useSelector(selectUserId) @@ -46,7 +46,7 @@ const GtwListSideNavigation = () => { return
} - return + return } export default GtwListSideNavigation diff --git a/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js b/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js index f67b3ab1fe..01d5b3ff89 100644 --- a/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js +++ b/pkg/webui/console/containers/sidebar/navigation/top-entities-section.js @@ -13,26 +13,16 @@ // limitations under the License. import React, { useCallback, useState } from 'react' -import { defineMessages } from 'react-intl' import { IconPlus } from '@ttn-lw/components/icon' import Button from '@ttn-lw/components/button' import SideNavigation from '@ttn-lw/components/sidebar/side-menu' import SectionLabel from '@ttn-lw/components/sidebar/section-label' -import RequireRequest from '@ttn-lw/lib/components/require-request' -import Message from '@ttn-lw/lib/components/message' - import sharedMessages from '@ttn-lw/lib/shared-messages' import PropTypes from '@ttn-lw/lib/prop-types' import useBookmark from '@ttn-lw/lib/hooks/use-bookmark' -import { getAllBookmarks } from '@console/store/actions/user-preferences' - -const m = defineMessages({ - errorMessage: 'Top entities cannot be provided', -}) - const Bookmark = ({ bookmark }) => { const { title, ids, path, icon } = useBookmark(bookmark) @@ -43,42 +33,48 @@ Bookmark.propTypes = { bookmark: PropTypes.shape({}).isRequired, } -const TopEntitiesSection = ({ topEntities, userId }) => { +const TopEntitiesSection = ({ topEntities, entity }) => { const [showMore, setShowMore] = useState(false) const handleShowMore = useCallback(async () => { setShowMore(showMore => !showMore) }, []) - const errorFunction = () => + const label = entity + ? entity === 'gateway' + ? sharedMessages.topGateways + : sharedMessages.topApplications + : sharedMessages.topEntities return ( - - - null} /> - {topEntities.slice(0, 6).map(bookmark => ( - - ))} - {showMore && - topEntities.length > 6 && - topEntities - .slice(6, topEntities.length) - .map(bookmark => )} - {topEntities.length > 6 && ( -