Skip to content

Commit

Permalink
Merge pull request #7104 from TheThingsNetwork/feature/gateway-status…
Browse files Browse the repository at this point in the history
…-panel

Add gateway status panel
  • Loading branch information
ryaplots authored Jun 7, 2024
2 parents 1a81155 + bee2108 commit 2a40ccb
Show file tree
Hide file tree
Showing 20 changed files with 921 additions and 64 deletions.
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

0 comments on commit 2a40ccb

Please sign in to comment.