Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lastSeen column to top entities panel #7049

Merged
merged 6 commits into from May 13, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion pkg/webui/console/components/last-seen/index.js
Expand Up @@ -35,6 +35,7 @@ const computeDeltaInSeconds = (from, to) => {
const LastSeen = React.forwardRef((props, ref) => {
const {
className,
statusClassName,
lastSeen,
short,
updateIntervalInSeconds,
Expand All @@ -51,7 +52,7 @@ const LastSeen = React.forwardRef((props, ref) => {
pulseTrigger={lastSeen}
flipped={flipped}
ref={ref}
className="d-flex al-center"
className={classnames(statusClassName, 'd-flex al-center')}
>
<div className={classnames(className, 'd-inline-block')}>
{!short && <Message className="mr-cs-xxs" content={message} />}
Expand Down Expand Up @@ -82,6 +83,7 @@ LastSeen.propTypes = {
noTitle: PropTypes.bool,
short: PropTypes.bool,
status: PropTypes.oneOf(['good', 'bad', 'mediocre', 'unknown']),
statusClassName: PropTypes.string,
updateIntervalInSeconds: PropTypes.number,
}

Expand All @@ -94,6 +96,7 @@ LastSeen.defaultProps = {
status: 'good',
message: sharedMessages.lastSeen,
noTitle: false,
statusClassName: undefined,
}

export default LastSeen
Expand Up @@ -17,9 +17,12 @@ import { defineMessages } from 'react-intl'
import { useSelector } from 'react-redux'

import Icon from '@ttn-lw/components/icon'
import Status from '@ttn-lw/components/status'

import Message from '@ttn-lw/lib/components/message'

import LastSeen from '@console/components/last-seen'

import sharedMessages from '@ttn-lw/lib/shared-messages'

import {
Expand All @@ -41,7 +44,7 @@ const AllTopEntitiesList = () => {
{
name: 'type',
displayName: sharedMessages.type,
width: '35px',
width: '2.5rem',
render: icon => <Icon icon={icon} />,
},
{
Expand All @@ -50,6 +53,42 @@ const AllTopEntitiesList = () => {
align: 'left',
render: (name, id) => <Message content={name === '' ? id : name} />,
},
{
name: 'lastSeen',
displayName: sharedMessages.lastSeen,
render: lastSeen => {
if (!lastSeen) {
return (
<Status
status="mediocre"
label={sharedMessages.noRecentActivity}
className="d-flex j-end al-center"
/>
)
}
if (lastSeen && typeof lastSeen === 'string') {
ryaplots marked this conversation as resolved.
Show resolved Hide resolved
return <LastSeen lastSeen={lastSeen} short statusClassName="j-end" />
}

return lastSeen.isDisconnected ? (
<LastSeen
status="bad"
message={sharedMessages.disconnected}
lastSeen={lastSeen.disconnectedAt}
statusClassName="j-end"
short
/>
) : Boolean(lastSeen.gatewayLastSeen) ? (
<LastSeen lastSeen={lastSeen.gatewayLastSeen} statusClassName="j-end" short />
) : (
<Status
status="mediocre"
label={sharedMessages.noRecentActivity}
className="d-flex j-end al-center"
/>
)
},
},
]

return (
Expand Down
Expand Up @@ -12,18 +12,52 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import React from 'react'
import React, { useEffect } from 'react'
import classNames from 'classnames'
import { useDispatch, useSelector } from 'react-redux'

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 { startGatewayStatistics, stopGatewayStatistics } from '@console/store/actions/gateways'

import { selectDeviceLastSeen } from '@console/store/selectors/devices'
import { selectApplicationDerivedLastSeen } from '@console/store/selectors/applications'
import { selectGatewayStatistics } from '@console/store/selectors/gateways'
import { selectGatewayLastSeen } from '@console/store/selectors/gateway-status'

import styles from './top-entities-panel.styl'

const EntitiesItem = ({ bookmark, headers, last }) => {
const dispatch = useDispatch()
const { title, ids, path, icon } = useBookmark(bookmark)
const entityIds = bookmark.entity_ids
const entity = Object.keys(entityIds)[0].replace('_ids', '')
const deviceLastSeen = useSelector(state => selectDeviceLastSeen(state, ids.appId, ids.id))
const appLastSeen = useSelector(state => selectApplicationDerivedLastSeen(state, ids.id))
const statistics = useSelector(selectGatewayStatistics)
const gatewayLastSeen = useSelector(selectGatewayLastSeen)
ryaplots marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
dispatch(startGatewayStatistics(ids.id))
return () => {
dispatch(stopGatewayStatistics())
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
ryaplots marked this conversation as resolved.
Show resolved Hide resolved

const isDisconnected = Boolean(statistics) && Boolean(statistics.disconnected_at)

const status = {
gatewayLastSeen,
isDisconnected,
disconnectedAt: statistics?.disconnected_at,
}

const lastSeen =
entity === 'device' ? deviceLastSeen : entity === 'gateway' ? status : appLastSeen

return (
<Table.Row
Expand All @@ -35,14 +69,19 @@ const EntitiesItem = ({ bookmark, headers, last }) => {
>
{headers.map((header, index) => {
const value =
headers[index].name === 'name' ? title : headers[index].name === 'type' ? icon : ''
headers[index].name === 'name'
? title
: headers[index].name === 'type'
? icon
: headers[index].name === 'lastSeen'
? lastSeen
: ''
const entityID = ids.id
return (
<Table.DataCell
key={index}
align={header.align}
className={classNames(styles.entityCell, {
[styles.entityCellExtended]: index === 1 && headers[index].name === 'name',
[styles.entityCellSmall]: headers[index].name === 'type',
})}
>
Expand All @@ -55,7 +94,9 @@ const EntitiesItem = ({ bookmark, headers, last }) => {
}

EntitiesItem.propTypes = {
bookmark: PropTypes.shape({}).isRequired,
bookmark: PropTypes.shape({
entity_ids: PropTypes.shape({}).isRequired,
}).isRequired,
headers: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
Expand Down
Expand Up @@ -17,9 +17,12 @@ import { FormattedNumber, defineMessages } from 'react-intl'
import { useSelector } from 'react-redux'

import Spinner from '@ttn-lw/components/spinner'
import Status from '@ttn-lw/components/status'

import Message from '@ttn-lw/lib/components/message'

import LastSeen from '@console/components/last-seen'

import sharedMessages from '@ttn-lw/lib/shared-messages'

import {
Expand Down Expand Up @@ -55,6 +58,7 @@ const TopApplicationsList = () => {
{
name: 'deviceCount',
displayName: sharedMessages.devicesShort,
align: 'center',
render: deviceCount =>
typeof deviceCount !== 'number' ? (
<Spinner micro right after={100} className="c-icon" />
Expand All @@ -64,6 +68,22 @@ const TopApplicationsList = () => {
</strong>
),
},
{
name: 'lastSeen',
displayName: sharedMessages.lastSeen,
render: lastSeen => {
const showLastSeen = Boolean(lastSeen)
return showLastSeen ? (
<LastSeen lastSeen={lastSeen} short statusClassName="j-end" />
) : (
<Status
status="mediocre"
label={sharedMessages.noRecentActivity}
className="d-flex j-end al-center"
/>
)
},
},
]

return (
Expand Down
Expand Up @@ -27,7 +27,10 @@ import PropTypes from '@ttn-lw/lib/prop-types'

import { getApplicationDeviceCount } from '@console/store/actions/applications'

import { selectApplicationDeviceCount } from '@console/store/selectors/applications'
import {
selectApplicationDerivedLastSeen,
selectApplicationDeviceCount,
} from '@console/store/selectors/applications'

import styles from '../top-entities-panel.styl'

Expand All @@ -38,6 +41,7 @@ const m = defineMessages({
const TopApplicationsItem = ({ bookmark, headers, last }) => {
const { title, ids, path } = useBookmark(bookmark)
const deviceCount = useSelector(state => selectApplicationDeviceCount(state, ids.id))
const lastSeen = useSelector(state => selectApplicationDerivedLastSeen(state, ids.id))

const loadDeviceCount = useCallback(
async dispatch => {
Expand All @@ -59,15 +63,26 @@ const TopApplicationsItem = ({ bookmark, headers, last }) => {
className={classNames(styles.entityRow, { [styles.lastRow]: last })}
>
{headers.map((header, index) => {
const value = headers[index].name === 'name' ? title : deviceCount
const value =
headers[index].name === 'name'
? title
: headers[index].name === 'lastSeen'
? lastSeen
: deviceCount
const entityID = ids.id
return (
<RequireRequest
key={index}
requestAction={loadDeviceCount}
errorRenderFunction={errorRenderFunction}
>
<Table.DataCell align={header.align} className={styles.entityCell}>
<Table.DataCell
align={header.align}
className={classNames(styles.entityCell, {
[styles.entityCellDivided]:
headers[index].name === 'lastSeen' || headers[index].name === 'name',
})}
>
{headers[index].render(value, entityID)}
</Table.DataCell>
</RequireRequest>
Expand Down
Expand Up @@ -16,8 +16,12 @@ import React from 'react'
import { defineMessages } from 'react-intl'
import { useSelector } from 'react-redux'

import Status from '@ttn-lw/components/status'

import Message from '@ttn-lw/lib/components/message'

import LastSeen from '@console/components/last-seen'

import sharedMessages from '@ttn-lw/lib/shared-messages'

import {
Expand Down Expand Up @@ -48,6 +52,22 @@ const TopDevicesList = () => {
</>
),
},
{
name: 'lastSeen',
displayName: sharedMessages.lastSeen,
render: lastSeen => {
const showLastSeen = Boolean(lastSeen)
return showLastSeen ? (
<LastSeen lastSeen={lastSeen} short statusClassName="j-end" />
) : (
<Status
status="mediocre"
label={sharedMessages.noRecentActivity}
className="d-flex j-end al-center"
/>
)
},
},
]

return (
Expand Down
Expand Up @@ -40,8 +40,8 @@
&.last-row
margin-bottom: 56px
&-cell
width: 33%
&-extended
width: 100%
min-width: 100%
&-divided
width: 33%
&-small
width: 12%
width: 2.5rem
Expand Up @@ -16,8 +16,12 @@ import React from 'react'
import { defineMessages } from 'react-intl'
import { useSelector } from 'react-redux'

import Status from '@ttn-lw/components/status'

import Message from '@ttn-lw/lib/components/message'

import LastSeen from '@console/components/last-seen'

import sharedMessages from '@ttn-lw/lib/shared-messages'

import {
Expand Down Expand Up @@ -49,6 +53,30 @@ const TopGatewaysList = () => {
</>
),
},
{
name: 'lastSeen',
displayName: sharedMessages.lastSeen,
render: ({ gatewayLastSeen, isDisconnected, disconnectedAt }) => {
const showLastSeen = Boolean(gatewayLastSeen)
return isDisconnected ? (
<LastSeen
status="bad"
message={sharedMessages.disconnected}
lastSeen={disconnectedAt}
statusClassName="j-end"
short
/>
) : showLastSeen ? (
<LastSeen lastSeen={gatewayLastSeen} statusClassName="j-end" short />
) : (
<Status
status="mediocre"
label={sharedMessages.noRecentActivity}
className="d-flex j-end al-center"
/>
)
},
},
]

return (
Expand Down
4 changes: 3 additions & 1 deletion pkg/webui/lib/hooks/use-bookmark.js
Expand Up @@ -97,7 +97,9 @@ const useBookmark = bookmark => {
let response
if (entity === 'device') {
response = await dispatch(
attachPromise(entityRequestMap[entity](entityId.appId, entityId.id, 'name')),
attachPromise(
entityRequestMap[entity](entityId.appId, entityId.id, ['name', 'last_seen_at']),
),
)
} else {
response = await dispatch(attachPromise(entityRequestMap[entity](entityId.id, 'name')))
Expand Down