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 gateway status panel #7104

Merged
merged 28 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
39318be
console: Gateway status panel scaffold
ryaplots Apr 29, 2024
40d0ede
console: Fix title
ryaplots Apr 29, 2024
a6996bc
console: Fix status label
ryaplots May 1, 2024
6ed2042
console: Add gateways status panel logic
ryaplots May 1, 2024
e080f76
console: Add charts and refactor gateway status panel
ryaplots May 16, 2024
f46bcd2
console: Add empty messages and upsell the noc
ryaplots May 22, 2024
c9c8999
console: Update icon and status to accomodate differente styles
ryaplots May 22, 2024
bc32bf0
console: Add fetching and update styling
ryaplots May 22, 2024
8620c67
console: Add messages
ryaplots May 23, 2024
09c6224
console: Fix desconnected
ryaplots May 23, 2024
9141e7d
console: Add no data yet
ryaplots May 23, 2024
0d1f9bf
console: Refactor gtw status panel
ryaplots May 27, 2024
524d99c
console: Fix general styling
ryaplots May 27, 2024
65dd2df
console: Update locales
ryaplots May 27, 2024
dc3d949
console: Fix transmissions
ryaplots May 27, 2024
2084343
console: Fix messages
ryaplots May 27, 2024
5fc7faf
console: Fix tooltips
ryaplots May 28, 2024
78fdc10
console: Fix responsiveness
ryaplots May 28, 2024
7731c41
console: Add established connection
ryaplots May 29, 2024
3fcfc95
console: Fix locales
ryaplots May 30, 2024
05d5557
console: Fix roundtrip times and styling issues
ryaplots May 30, 2024
d0e2003
console: Fix roundtrip times
ryaplots Jun 4, 2024
ee3c02f
console: Remove title from status
ryaplots Jun 4, 2024
b5689eb
console: Remove unnecessary <>
ryaplots Jun 4, 2024
8554084
console: Update locales
ryaplots Jun 4, 2024
7632dea
console: Fix no connection yet and datat flashes
ryaplots Jun 4, 2024
d448dc6
console: Update messages
ryaplots Jun 4, 2024
bee2108
console: Fix tests
ryaplots Jun 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions pkg/webui/components/data-sheet/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import React from 'react'
import classnames from 'classnames'
import { defineMessages } from 'react-intl'

import SafeInspector from '@ttn-lw/components/safe-inspector'

Expand All @@ -25,10 +24,6 @@ import sharedMessages from '@ttn-lw/lib/shared-messages'

import style from './data-sheet.styl'

const m = defineMessages({
noData: 'No data available',
})

const DataSheet = ({ className, data }) => (
<table className={classnames(className, style.table)}>
<tbody>
Expand Down Expand Up @@ -61,7 +56,7 @@ const DataSheet = ({ className, data }) => (
) : (
<tr>
<th colSpan={2}>
<Message content={group.emptyMessage || m.noData} />
<Message content={group.emptyMessage || sharedMessages.noData} />
</th>
</tr>
)}
Expand Down
5 changes: 4 additions & 1 deletion pkg/webui/components/panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const Panel = ({
children,
title,
icon,
iconClassName,
toggleOptions,
activeToggle,
onToggleClick,
Expand All @@ -59,7 +60,7 @@ const Panel = ({
<div className={classnames(styles.panel, className)}>
<div className="d-flex j-between al-center mb-cs-m gap-cs-m">
<div className="d-flex gap-cs-xs al-center overflow-hidden">
{icon && <Icon icon={icon} className={styles.panelHeaderIcon} />}
{icon && <Icon icon={icon} className={classnames(styles.panelHeaderIcon, iconClassName)} />}
<Message content={title} className={styles.panelHeaderTitle} />
{messageDecorators}
</div>
Expand Down Expand Up @@ -90,6 +91,7 @@ Panel.propTypes = {
className: PropTypes.string,
divider: PropTypes.bool,
icon: PropTypes.icon,
iconClassName: PropTypes.string,
messageDecorators: PropTypes.node,
onToggleClick: PropTypes.func,
shortCutLinkDisabled: PropTypes.bool,
Expand All @@ -112,6 +114,7 @@ Panel.defaultProps = {
shortCutLinkPath: undefined,
shortCutLinkTitle: undefined,
shortCutLinkTarget: undefined,
iconClassName: undefined,
}

export { Panel as default, PanelError }
1 change: 1 addition & 0 deletions pkg/webui/components/panel/panel.styl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
&-title
font-weight: $fwv2.bold
font-size: $fsv2.l
line-height: 1.3
white-space: nowrap
text-overflow: ellipsis
overflow: hidden
Expand Down
12 changes: 6 additions & 6 deletions pkg/webui/components/status-label/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@
import React, { useMemo } from 'react'
import classnames from 'classnames'

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

import PropTypes from '@ttn-lw/lib/prop-types'
import from from '@ttn-lw/lib/from'

import Icon, {
IconCircleCheckFilled,
IconAlertTriangleFilled,
IconAlertCircleFilled,
IconInfoCircleFilled,
} from '../icon'
} from '@ttn-lw/components/icon'

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

import PropTypes from '@ttn-lw/lib/prop-types'
import from from '@ttn-lw/lib/from'

import style from './status-label.styl'

Expand Down
36 changes: 8 additions & 28 deletions pkg/webui/components/status/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,15 @@

import React, { useEffect, useState, useCallback, useRef } from 'react'
import classnames from 'classnames'
import { defineMessages, useIntl } from 'react-intl'

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

import PropTypes from '@ttn-lw/lib/prop-types'

import style from './status.styl'

const m = defineMessages({
good: 'good',
bad: 'bad',
mediocre: 'mediocre',
unknown: 'unknown',
})

const Status = React.forwardRef(
(
{ className, status, label, pulse, pulseTrigger, labelValues, children, title, flipped },
ref,
) => {
const intl = useIntl()
({ className, status, label, pulse, pulseTrigger, labelValues, children, flipped, big }, ref) => {
const [animate, setAnimate] = useState(false)
const pulseArmed = useRef(false)
useEffect(() => {
Expand All @@ -50,6 +38,7 @@ const Status = React.forwardRef(
}, [setAnimate])

const cls = classnames(style.status, {
[style.statusGreen]: status === 'green',
[style.statusGood]: status === 'good',
[style.statusBad]: status === 'bad',
[style.statusMediocre]: status === 'mediocre',
Expand All @@ -58,6 +47,7 @@ const Status = React.forwardRef(
[style.flipped]: flipped,
[style[`triggered-${status}-pulse`]]: animate,
[style.dotOnly]: !label && !children,
[style.statusBig]: big,
})

let statusLabel = null
Expand All @@ -74,32 +64,23 @@ const Status = React.forwardRef(
)
}

let translatedTitle

if (title) {
translatedTitle = typeof title === 'string' ? title : intl.formatMessage(title)
} else if (label) {
translatedTitle = typeof label === 'string' ? label : intl.formatMessage(label)
} else {
translatedTitle = intl.formatMessage(m[status])
}

return (
<span
className={classnames(className, style.container)}
onAnimationEnd={handleAnimationEnd}
ref={ref}
>
{flipped && <span className={classnames(cls)} title={translatedTitle} />}
{flipped && <span className={classnames(cls)} />}
{statusLabel}
{children}
{!flipped && <span className={classnames(cls)} title={translatedTitle} />}
{!flipped && <span className={classnames(cls)} />}
</span>
)
},
)

Status.propTypes = {
big: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
flipped: PropTypes.bool,
Expand All @@ -111,8 +92,7 @@ Status.propTypes = {
PropTypes.number,
PropTypes.instanceOf(Date),
]),
status: PropTypes.oneOf(['good', 'bad', 'mediocre', 'unknown']),
title: PropTypes.message,
status: PropTypes.oneOf(['green', 'good', 'bad', 'mediocre', 'unknown']),
}

Status.defaultProps = {
Expand All @@ -124,7 +104,7 @@ Status.defaultProps = {
pulse: undefined,
pulseTrigger: undefined,
status: 'unknown',
title: undefined,
big: false,
}

export default Status
7 changes: 7 additions & 0 deletions pkg/webui/components/status/status.styl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
border-radius: 50%
transition: background-color $ad.m

&-big
width: 8px
height: 8px

&:not(.dot-only)
&:not(.flipped)
margin-left: $cs.xs
Expand All @@ -37,6 +41,9 @@
opacity: .7
animation: pulse 2s infinite

&-green
background-color: var(--c-bg-success-normal)

&-good
background-color: var(--c-text-brand-normal)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
.root
position: sticky
top: 0
z-index: calc($zi.dropdown - 1)
z-index: $zi.dropdown - 1
display: flex
justify-content: space-between
gap: $cs.xl
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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, defineMessages } from 'react-intl'
import classNames from 'classnames'
import ReactApexChart from 'react-apexcharts'

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

import PropTypes from '@ttn-lw/lib/prop-types'

import style from './gateway-status-panel.styl'

const m = defineMessages({
frequencyRange: '{minFreq} - {maxFreq}MHz',
})

const options = {
chart: {
type: 'radialBar',
},
grid: {
padding: {
left: -9,
right: -9,
bottom: -12,
top: -9,
},
},
colors: [
({ value }) => {
if (value < 55) {
return '#1CB041'
} else if (value === 100) {
return '#DB2328'
}

return '#DB7600'
},
],
stroke: {
lineCap: 'round',
},
dataLabels: {
enabled: false,
},
legend: {
show: false,
},
plotOptions: {
radialBar: {
track: {
show: true,
margin: 1.5,
},
dataLabels: {
show: false,
},
},
},
}

const DutyCycleUtilization = ({ index, gatewayStats, band }) => {
const maxFrequency = band.max_frequency / 1e6
const minFrequency = band.min_frequency / 1e6
const utilization = band.downlink_utilization
? (band.downlink_utilization * 100) / band.downlink_utilization_limit
: 0

return (
<div
className={classNames(style.gtwStatusPanelDutyCycle, {
'mb-cs-m': index !== gatewayStats.sub_bands.length - 1,
'mt-cs-l': index === 0,
})}
>
<Message
content={m.frequencyRange}
values={{
minFreq: minFrequency.toFixed(1),
maxFreq: maxFrequency.toFixed(1),
}}
className="fs-s"
/>
<div className="d-flex al-center j-center gap-cs-xs">
<div className="md:d-none">
<ReactApexChart
options={options}
series={[utilization.toFixed(2)]}
type="radialBar"
height={20}
width={20}
/>
</div>
<span
className={classNames('fs-s fw-bold', {
'c-text-success-normal': utilization <= 60,
'c-text-warning-normal': utilization > 60 && utilization < 100,
'c-text-error-normal': utilization === 100,
})}
style={{ minWidth: '39px' }}
>
<FormattedNumber
style="percent"
value={
isNaN(band.downlink_utilization / band.downlink_utilization_limit)
? 0
: band.downlink_utilization / band.downlink_utilization_limit
}
minimumFractionDigits={2}
/>
</span>
</div>
</div>
)
}

DutyCycleUtilization.propTypes = {
band: PropTypes.shape({
downlink_utilization: PropTypes.number,
downlink_utilization_limit: PropTypes.number,
max_frequency: PropTypes.number,
min_frequency: PropTypes.number,
}).isRequired,
gatewayStats: PropTypes.shape({
sub_bands: PropTypes.arrayOf(
PropTypes.shape({
downlink_utilization: PropTypes.number,
downlink_utilization_limit: PropTypes.number,
max_frequency: PropTypes.number,
min_frequency: PropTypes.number,
}),
),
}).isRequired,
index: PropTypes.number.isRequired,
}

export default DutyCycleUtilization
Loading
Loading