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 all 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,43 @@ 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 (typeof lastSeen === 'string') {
return <LastSeen lastSeen={lastSeen} short statusClassName="j-end" />
}

let indicator = 'unknown'
let label = sharedMessages.unknown

if (lastSeen.status === 'connected') {
indicator = 'good'
label = sharedMessages.connected
} else if (lastSeen.status === 'disconnected') {
indicator = 'bad'
label = sharedMessages.disconnected
} else if (lastSeen.status === 'other-cluster') {
indicator = 'unknown'
label = sharedMessages.otherCluster
} else if (lastSeen.status === 'unknown') {
indicator = 'mediocre'
label = sharedMessages.unknown
}

return <Status status={indicator} label={label} />
},
},
]

return (
Expand Down
Expand Up @@ -14,16 +14,36 @@

import React from 'react'
import classNames from 'classnames'
import { 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 { selectDeviceLastSeen } from '@console/store/selectors/devices'
import { selectApplicationDerivedLastSeen } from '@console/store/selectors/applications'
import { selectGatewayById } from '@console/store/selectors/gateways'

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

const EntitiesItem = ({ bookmark, headers, last }) => {
const { title, ids, path, icon } = useBookmark(bookmark)
const entityIds = bookmark.entity_ids
const entity = Object.keys(entityIds)[0].replace('_ids', '')

let lastSeenSelector
if (entity === 'application') {
lastSeenSelector = state => selectApplicationDerivedLastSeen(state, ids.id)
} else if (entity === 'gateway') {
lastSeenSelector = state => selectGatewayById(state, ids.id)
} else if (entity === 'device') {
lastSeenSelector = state => selectDeviceLastSeen(state, ids.appId, ids.id)
}

const lastSeenSelected = useSelector(lastSeenSelector)

const lastSeen = entity === 'gateway' ? { status: lastSeenSelected?.status } : lastSeenSelected

return (
<Table.Row
Expand All @@ -35,14 +55,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 +80,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,6 +16,8 @@ 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 sharedMessages from '@ttn-lw/lib/shared-messages'
Expand Down Expand Up @@ -49,6 +51,30 @@ const TopGatewaysList = () => {
</>
),
},
{
name: 'lastSeen',
displayName: sharedMessages.status,
render: lastSeen => {
let indicator = 'unknown'
let label = sharedMessages.unknown

if (lastSeen.status === 'connected') {
indicator = 'good'
label = sharedMessages.connected
} else if (lastSeen.status === 'disconnected') {
indicator = 'bad'
label = sharedMessages.disconnected
} else if (lastSeen.status === 'other-cluster') {
indicator = 'unknown'
label = sharedMessages.otherCluster
} else if (lastSeen.status === 'unknown') {
indicator = 'mediocre'
label = sharedMessages.unknown
}

return <Status status={indicator} label={label} />
},
},
]

return (
Expand Down
9 changes: 8 additions & 1 deletion pkg/webui/console/views/overview/index.js
Expand Up @@ -33,7 +33,14 @@ const Overview = () => {
useBreadcrumbs('overview', <Breadcrumb path="/" content={sharedMessages.overview} />)

return (
<RequireRequest requestAction={[getApplicationsList(), getGatewaysList()]}>
<RequireRequest
requestAction={[
getApplicationsList(),
getGatewaysList(undefined, ['name', 'gateway_server_address'], {
withStatus: true,
}),
]}
>
<div className="container container--xl grid p-ls-s gap-ls-s">
<div className="item-12 md:item-12 lg:item-6 sm:item-6">
<TopEntitiesDashboardPanel />
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
12 changes: 8 additions & 4 deletions sdk/js/src/service/users.js
Expand Up @@ -218,10 +218,14 @@ class Users {
}

async addBookmark(userId, entity) {
const response = await this._api.UserBookmarkRegistry.Create(undefined, {
user_ids: { user_id: userId },
entity_ids: entity,
})
const response = await this._api.UserBookmarkRegistry.Create(
{
routeParams: { 'user_ids.user_id': userId },
},
{
entity_ids: entity,
},
)

return Marshaler.unwrapBookmark(response)
}
Expand Down