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()}
-
+
+
+
+
+