diff --git a/ui/src/components/tiles/AvailabilityTile.tsx b/ui/src/components/tiles/AvailabilityTile.tsx new file mode 100644 index 000000000..cf6357039 --- /dev/null +++ b/ui/src/components/tiles/AvailabilityTile.tsx @@ -0,0 +1,89 @@ +import React from 'react' +import {Spinner} from 'react-bootstrap' +import {Objective} from '../../proto/objectives/v1alpha1/objectives_pb' +import {hasObjectiveType, ObjectiveType} from '../../App' + +interface AvailabilityTileProps { + objective: Objective + loading: boolean + success: boolean + errors: number | undefined + total: number | undefined +} + +const AvailabilityTile = ({ + objective, + loading, + success, + errors, + total, +}: AvailabilityTileProps): React.JSX.Element => { + console.log(loading, success, errors, total) + + const headline =
Availability
+ if (loading) { + return ( +
+ {headline} + +
+ ) + } + + if (success) { + if (errors !== undefined && total !== undefined) { + const percentage = 1 - errors / total + + const objectiveType = hasObjectiveType(objective) + const objectiveTypeLatency = + objectiveType === ObjectiveType.Latency || objectiveType === ObjectiveType.LatencyNative + + return ( +
objective.target ? 'good' : 'bad'}> + {headline} +

{(100 * percentage).toFixed(3)}%

+ + + + + + + + + + + +
{objectiveTypeLatency ? 'Slow:' : 'Errors:'}{Math.floor(errors).toLocaleString()}
Total:{Math.floor(total).toLocaleString()}
+
+ ) + } else { + return ( +
+ {headline} +

No data

+
+ ) + } + } + + return ( +
+ <> + {headline} +

Error

+ +
+ ) +} + +export default AvailabilityTile diff --git a/ui/src/components/tiles/ErrorBudgetTile.tsx b/ui/src/components/tiles/ErrorBudgetTile.tsx new file mode 100644 index 000000000..c1f9f992b --- /dev/null +++ b/ui/src/components/tiles/ErrorBudgetTile.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import {Spinner} from 'react-bootstrap' +import {Objective} from '../../proto/objectives/v1alpha1/objectives_pb' + +interface ErrorBudgetTileProps { + objective: Objective + loading: boolean + success: boolean + errors: number | undefined + total: number | undefined +} + +const ErrorBudgetTile = ({objective, loading, success, errors, total}: ErrorBudgetTileProps) => { + const headline =
Error Budget
+ + if (loading) { + return ( +
+ {headline} + +
+ ) + } + if (success) { + if (errors !== undefined && total !== undefined) { + const budget = 1 - objective.target + const unavailability = errors / total + const availableBudget = (budget - unavailability) / budget + + return ( +
0 ? 'good' : 'bad'}> + {headline} +

{(100 * availableBudget).toFixed(3)}%

+
+ ) + } else { + return ( +
+ {headline} +

No data

+
+ ) + } + } + return ( +
+ {headline} +

Error

+
+ ) +} + +export default ErrorBudgetTile diff --git a/ui/src/components/tiles/ObjectiveTile.tsx b/ui/src/components/tiles/ObjectiveTile.tsx new file mode 100644 index 000000000..a001239d5 --- /dev/null +++ b/ui/src/components/tiles/ObjectiveTile.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import {hasObjectiveType, ObjectiveType, renderLatencyTarget} from '../../App' +import {formatDuration} from '../../duration' +import {Objective} from '../../proto/objectives/v1alpha1/objectives_pb' + +interface ObjectiveTileProps { + objective: Objective +} + +const ObjectiveTile = ({objective}: ObjectiveTileProps): React.JSX.Element => { + const objectiveType = hasObjectiveType(objective) + switch (objectiveType) { + case ObjectiveType.Ratio: + return ( +
+
Objective
+

{(100 * objective.target).toFixed(3)}%

+ <>in {formatDuration(Number(objective.window?.seconds) * 1000)} +
+ ) + case ObjectiveType.BoolGauge: + return ( +
+
Objective
+

{(100 * objective.target).toFixed(3)}%

+ <>in {formatDuration(Number(objective.window?.seconds) * 1000)} +
+ ) + case ObjectiveType.Latency: + case ObjectiveType.LatencyNative: + return ( +
+
Objective
+

{(100 * objective.target).toFixed(3)}%

+ <>in {formatDuration(Number(objective.window?.seconds) * 1000)} +
+

faster than {renderLatencyTarget(objective)}

+
+ ) + default: + return
+ } +} + +export default ObjectiveTile diff --git a/ui/src/components/tiles/Tiles.scss b/ui/src/components/tiles/Tiles.scss new file mode 100644 index 000000000..066644859 --- /dev/null +++ b/ui/src/components/tiles/Tiles.scss @@ -0,0 +1,68 @@ +.tiles { + width: 100%; + display: grid; + grid-template-columns: repeat(1, 1fr); + column-gap: 25px; + row-gap: 25px; + justify-items: stretch; + + @media (min-width: 576px) { + grid-template-columns: repeat(2, 1fr); + column-gap: 50px; + row-gap: 50px; + } + @media (min-width: 992px) { + grid-template-columns: repeat(3, 1fr); + column-gap: 75px; + row-gap: 75px; + } + + div { + padding: 35px; + background-color: $gray-300; + color: $gray-900; + border-radius: 8px; + + h2, h6 { + font-family: $font-family-sans-serif; + } + + h2 { + font-weight: 400; + font-size: 40px; + margin-bottom: 0; + } + + h6 { + font-weight: 600; + font-size: 20px; + } + + .headline, .details { + opacity: 0.5; + } + + .metric { + display: inline-block; + margin-right: 0.5rem; + } + + .details { + font-weight: 500; + } + + &.good { + background-color: $green; + color: $green-text; + } + + &.bad { + background-color: $red; + color: $red-text; + } + + h2.error { + color: $red; + } + } +} diff --git a/ui/src/components/tiles/Tiles.tsx b/ui/src/components/tiles/Tiles.tsx new file mode 100644 index 000000000..1b40a0fff --- /dev/null +++ b/ui/src/components/tiles/Tiles.tsx @@ -0,0 +1,9 @@ +interface TilesProps { + children: React.ReactNode +} + +const Tiles = (props: TilesProps) => { + return
{props.children}
+} + +export default Tiles diff --git a/ui/src/index.scss b/ui/src/index.scss index b387a4a26..e17dd8369 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -156,3 +156,4 @@ a.external-prometheus { @import 'components/Navbar'; @import 'components/AlertsTable'; @import 'components/graphs/BurnrateGraph'; +@import "components/tiles/Tiles"; diff --git a/ui/src/pages/Detail.scss b/ui/src/pages/Detail.scss index e86f21386..bb87b7adb 100644 --- a/ui/src/pages/Detail.scss +++ b/ui/src/pages/Detail.scss @@ -9,75 +9,6 @@ } } - .metrics { - width: 100%; - display: grid; - grid-template-columns: repeat(1, 1fr); - column-gap: 25px; - row-gap: 25px; - justify-items: stretch; - - @media (min-width: 576px) { - grid-template-columns: repeat(2, 1fr); - column-gap: 50px; - row-gap: 50px; - } - @media (min-width: 992px) { - grid-template-columns: repeat(3, 1fr); - column-gap: 75px; - row-gap: 75px; - } - - div { - padding: 35px; - background-color: $gray-300; - color: $gray-900; - border-radius: 8px; - - h2, h6 { - font-family: $font-family-sans-serif; - } - - h2 { - font-weight: 400; - font-size: 40px; - margin-bottom: 0; - } - - h6 { - font-weight: 600; - font-size: 20px; - } - - .headline, .details { - opacity: 0.5; - } - - .metric { - display: inline-block; - margin-right: 0.5rem; - } - - .details { - font-weight: 500; - } - - &.good { - background-color: $green; - color: $green-text; - } - - &.bad { - background-color: $red; - color: $red-text; - } - - h2.error { - color: $red; - } - } - } - .timerange { background: linear-gradient(0deg, rgba(0, 0, 0, 0) 45%, $gray-300 50%, rgba(0, 0, 0, 0) 55%); diff --git a/ui/src/pages/Detail.tsx b/ui/src/pages/Detail.tsx index 91a455efc..91fae7a50 100644 --- a/ui/src/pages/Detail.tsx +++ b/ui/src/pages/Detail.tsx @@ -11,13 +11,7 @@ import { Spinner, Tooltip as OverlayTooltip, } from 'react-bootstrap' -import { - API_BASEPATH, - hasObjectiveType, - latencyTarget, - ObjectiveType, - renderLatencyTarget, -} from '../App' +import {API_BASEPATH, hasObjectiveType, latencyTarget, ObjectiveType} from '../App' import Navbar from '../components/Navbar' import {MetricName, parseLabels} from '../labels' import ErrorBudgetGraph from '../components/graphs/ErrorBudgetGraph' @@ -34,6 +28,10 @@ import {replaceInterval, usePrometheusQuery} from '../prometheus' import {useObjectivesList} from '../objectives' import {Objective} from '../proto/objectives/v1alpha1/objectives_pb' import {formatDuration, parseDuration} from '../duration' +import ObjectiveTile from '../components/tiles/ObjectiveTile' +import AvailabilityTile from '../components/tiles/AvailabilityTile' +import ErrorBudgetTile from '../components/tiles/ErrorBudgetTile' +import Tiles from '../components/tiles/Tiles' const Detail = () => { const baseUrl = API_BASEPATH === undefined ? 'http://localhost:9099' : API_BASEPATH @@ -199,181 +197,24 @@ const Detail = () => { const objectiveTypeLatency = objectiveType === ObjectiveType.Latency || objectiveType === ObjectiveType.LatencyNative - const renderObjective = () => { - switch (objectiveType) { - case ObjectiveType.Ratio: - return ( -
-
Objective
-

{(100 * objective.target).toFixed(3)}%

- <>in {formatDuration(Number(objective.window?.seconds) * 1000)} -
- ) - case ObjectiveType.BoolGauge: - return ( -
-
Objective
-

{(100 * objective.target).toFixed(3)}%

- <>in {formatDuration(Number(objective.window?.seconds) * 1000)} -
- ) - case ObjectiveType.Latency: - case ObjectiveType.LatencyNative: - return ( -
-
Objective
-

{(100 * objective.target).toFixed(3)}%

- <>in {formatDuration(Number(objective.window?.seconds) * 1000)} -
-

faster than {renderLatencyTarget(objective)}

-
- ) - default: - return
- } - } - - const renderAvailability = () => { - const headline =
Availability
- if ( - totalStatus === 'loading' || - totalStatus === 'idle' || - errorStatus === 'loading' || - errorStatus === 'idle' - ) { - return ( -
- {headline} - -
- ) - } - - if (totalStatus === 'success' && errorStatus === 'success') { - if (totalResponse?.options.case === 'vector' && errorResponse?.options.case === 'vector') { - let errors = 0 - if (errorResponse.options.value.samples.length > 0) { - errors = errorResponse.options.value.samples[0].value - } - - let total = 1 - if (totalResponse.options.value.samples.length > 0) { - total = totalResponse.options.value.samples[0].value - } - - const percentage = 1 - errors / total - - return ( -
objective.target ? 'good' : 'bad'}> - {headline} -

{(100 * percentage).toFixed(3)}%

- - - - - - - - - - - -
{objectiveTypeLatency ? 'Slow:' : 'Errors:'}{Math.floor(errors).toLocaleString()}
Total:{Math.floor(total).toLocaleString()}
-
- ) - } else { - return ( -
- {headline} -

No data

-
- ) - } - } - - return ( -
- <> - {headline} -

Error

- -
- ) - } + const loading: boolean = + totalStatus === 'loading' || + totalStatus === 'idle' || + errorStatus === 'loading' || + errorStatus === 'idle' - const renderErrorBudget = () => { - const headline =
Error Budget
+ const success: boolean = totalStatus === 'success' && errorStatus === 'success' - if ( - totalStatus === 'loading' || - totalStatus === 'idle' || - errorStatus === 'loading' || - errorStatus === 'idle' - ) { - return ( -
- {headline} - -
- ) + let errors: number = 0 + let total: number = 1 + if (totalResponse?.options.case === 'vector' && errorResponse?.options.case === 'vector') { + if (errorResponse.options.value.samples.length > 0) { + errors = errorResponse.options.value.samples[0].value } - if (totalStatus === 'success' && errorStatus === 'success') { - if (totalResponse?.options.case === 'vector' && errorResponse?.options.case === 'vector') { - let errors = 0 - if (errorResponse.options.value.samples.length > 0) { - errors = errorResponse.options.value.samples[0].value - } - - let total = 1 - if (totalResponse.options.value.samples.length > 0) { - total = totalResponse.options.value.samples[0].value - } - const budget = 1 - objective.target - const unavailability = errors / total - const availableBudget = (budget - unavailability) / budget - - return ( -
0 ? 'good' : 'bad'}> - {headline} -

{(100 * availableBudget).toFixed(3)}%

-
- ) - } else { - return ( -
- {headline} -

No data

-
- ) - } + if (totalResponse.options.value.samples.length > 0) { + total = totalResponse.options.value.samples[0].value } - return ( -
- {headline} -

Error

-
- ) } const labelBadges = Object.entries({...objective.labels, ...groupingLabels}) @@ -421,11 +262,23 @@ const Detail = () => { -
- {renderObjective()} - {renderAvailability()} - {renderErrorBudget()} -
+ + + + +