aboutsummaryrefslogtreecommitdiff
path: root/kamon-status-page/src/main/vue/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'kamon-status-page/src/main/vue/src/components')
-rw-r--r--kamon-status-page/src/main/vue/src/components/Card.vue20
-rw-r--r--kamon-status-page/src/main/vue/src/components/EnvironmentCard.vue66
-rw-r--r--kamon-status-page/src/main/vue/src/components/InstrumentationModuleList.vue44
-rw-r--r--kamon-status-page/src/main/vue/src/components/InstrumentationModuleStatusCard.vue40
-rw-r--r--kamon-status-page/src/main/vue/src/components/MetricList.vue129
-rw-r--r--kamon-status-page/src/main/vue/src/components/MetricListItem.vue108
-rw-r--r--kamon-status-page/src/main/vue/src/components/ModuleList.vue86
-rw-r--r--kamon-status-page/src/main/vue/src/components/ModuleStatusCard.vue52
-rw-r--r--kamon-status-page/src/main/vue/src/components/OverviewCard.vue71
-rw-r--r--kamon-status-page/src/main/vue/src/components/StatusCard.vue85
-rw-r--r--kamon-status-page/src/main/vue/src/components/StatusSection.vue19
11 files changed, 720 insertions, 0 deletions
diff --git a/kamon-status-page/src/main/vue/src/components/Card.vue b/kamon-status-page/src/main/vue/src/components/Card.vue
new file mode 100644
index 00000000..d3300336
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/Card.vue
@@ -0,0 +1,20 @@
+<template>
+ <div class="card-wrapper">
+ <slot/>
+ </div>
+</template>
+
+<script lang="ts">
+import { Component, Vue } from 'vue-property-decorator'
+
+@Component
+export default class Card extends Vue {}
+</script>
+
+
+<style lang="scss">
+.card-wrapper {
+ background-color: white;
+ box-shadow: 0 2px 9px 1px rgba(0, 0, 0, 0.1);
+}
+</style>
diff --git a/kamon-status-page/src/main/vue/src/components/EnvironmentCard.vue b/kamon-status-page/src/main/vue/src/components/EnvironmentCard.vue
new file mode 100644
index 00000000..98dc3f9f
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/EnvironmentCard.vue
@@ -0,0 +1,66 @@
+<template>
+ <status-section title="Environment">
+ <card>
+ <div class="row py-2 no-gutters">
+ <div class="col-auto py-2 px-3">
+ <div class="text-uppercase text-label pb-1">Service</div>
+ <h6>{{ service }}</h6>
+ </div>
+ <div class="col-auto py-2 px-3">
+ <div class="text-uppercase text-label pb-1">Host</div>
+ <h6>{{ host }}</h6>
+ </div>
+ <div class="col-auto py-2 px-3">
+ <div class="text-uppercase text-label pb-1">instance</div>
+ <h6>{{instance}}</h6>
+ </div>
+ <div class="col-12 col-md-3 py-2 px-3">
+ <div class="text-uppercase text-label pb-1">tags</div>
+ <div class="tag-container" v-if="Object.keys(environmentTags).length > 0">
+ <span class="tag" v-for="tag in Object.keys(environmentTags)" :key="tag">
+ {{ tag }}=<span class="tag-value">{{ environmentTags[tag] }}</span>
+ </span>
+ </div>
+ <div v-else>
+ <h6>None</h6>
+ </div>
+ </div>
+ </div>
+ </card>
+ </status-section>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import {Environment} from '../api/StatusApi'
+import Card from './Card.vue'
+import StatusSection from '../components/StatusSection.vue'
+import {Option, none} from 'ts-option'
+
+
+@Component({
+ components: {
+ 'card': Card,
+ 'status-section': StatusSection
+ }
+})
+export default class EnvironmentCard extends Vue {
+ @Prop() private environment: Option<Environment> = none
+
+ get instance(): string {
+ return this.environment.map(e => e.instance).getOrElse('Unknown')
+ }
+
+ get host(): string {
+ return this.environment.map(e => e.host).getOrElse('Unknown')
+ }
+
+ get service(): string {
+ return this.environment.map(e => e.service).getOrElse('Unknown')
+ }
+
+ get environmentTags(): { [key: string]: string } {
+ return this.environment.map(e => e.tags).getOrElse({})
+ }
+}
+</script> \ No newline at end of file
diff --git a/kamon-status-page/src/main/vue/src/components/InstrumentationModuleList.vue b/kamon-status-page/src/main/vue/src/components/InstrumentationModuleList.vue
new file mode 100644
index 00000000..224f6716
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/InstrumentationModuleList.vue
@@ -0,0 +1,44 @@
+<template>
+ <div class="row">
+ <div class="col-12 pt-4 pb-2" v-if="modules.length > 0">
+ <h2>Instrumentation Modules</h2>
+ </div>
+ <div class="col-12 py-1" v-for="module in sortedModules" :key="module.name">
+ <instrumentation-module-status-card :module="module"/>
+ </div>
+ </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import {InstrumentationModule} from '../api/StatusApi'
+import InstrumentationModuleStatusCard from './InstrumentationModuleStatusCard.vue'
+
+
+@Component({
+ components: {
+ 'instrumentation-module-status-card': InstrumentationModuleStatusCard
+ }
+})
+export default class ModuleList extends Vue {
+ @Prop() private modules!: InstrumentationModule[]
+
+ get sortedModules(): InstrumentationModule[] {
+ return this.modules.sort((left, right) => {
+ if (left.active === right.active) {
+ return left.name.localeCompare(right.name)
+ } else {
+ return left.active ? -1 : 1
+ }
+ })
+ }
+}
+</script>
+
+<style lang="scss">
+.apm-suggestion {
+ .kind-label {
+ background-color: #d0f3f0;
+ }
+}
+</style>
diff --git a/kamon-status-page/src/main/vue/src/components/InstrumentationModuleStatusCard.vue b/kamon-status-page/src/main/vue/src/components/InstrumentationModuleStatusCard.vue
new file mode 100644
index 00000000..ed74143a
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/InstrumentationModuleStatusCard.vue
@@ -0,0 +1,40 @@
+<template>
+ <status-card :indicator-text="runStatus.message" :indicator-icon="runStatus.icon" :indicator-background-color="runStatus.color">
+ <div slot="default" class="py-3 pl-4">
+ <h5 class="mb-0">{{ module.name }}</h5>
+ <div class="text-label">
+ {{ module.description }}
+ </div>
+ </div>
+ </status-card>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import {InstrumentationModule} from '../api/StatusApi'
+import StatusCard from './StatusCard.vue'
+
+
+@Component({
+ components: {
+ 'status-card': StatusCard
+ }
+})
+export default class InstrumentationModuleStatusCard extends Vue {
+ @Prop() private module!: InstrumentationModule
+
+ get runStatus(): { message: string, color: string, icon: string } {
+ if (!this.module.enabled) {
+ return { message: 'disabled', color: '#ff9898', icon: 'fa-stop-circle' }
+ } else {
+ return this.module.active ?
+ { message: 'active', color: '#7ade94', icon: 'fa-check' } :
+ { message: 'available', color: '#bbbbbb', icon: 'fa-stop-circle' }
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+
+</style>
diff --git a/kamon-status-page/src/main/vue/src/components/MetricList.vue b/kamon-status-page/src/main/vue/src/components/MetricList.vue
new file mode 100644
index 00000000..f0f4637b
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/MetricList.vue
@@ -0,0 +1,129 @@
+<template>
+ <div class="row no-gutters">
+ <div class="col-12">
+ <div class="search-box mb-3">
+ <span class="search-icon"><i class="fas fa-search fa-fw fa-flip-horizontal"></i></span>
+ <input class="w-100" v-model="filterPattern" type="text">
+ <span class="search-stats">{{ searchStats }}</span>
+ </div>
+ </div>
+
+ <div class="col-12" v-if="matchedMetrics.length > 0">
+ <div class="row no-gutters" v-for="(group, index) in groups" :key="group.name">
+ <div class="col-12">
+ <metric-list-item :group="group"/>
+ </div>
+ <hr v-if="index < (groups.length - 1)" class="w-100">
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import {Metric} from '../api/StatusApi'
+import Card from './Card.vue'
+import MetricListItem, {MetricGroup} from './MetricListItem.vue'
+import StatusCard from './StatusCard.vue'
+import _ from 'underscore'
+
+@Component({
+ components: {
+ 'card': Card,
+ 'status-card': StatusCard,
+ 'metric-list-item': MetricListItem
+ }
+})
+export default class MetricList extends Vue {
+ @Prop( { default: [] }) private metrics!: Metric[]
+ private filterPattern: string = ''
+
+ get totalMetrics(): number {
+ return this.metrics.length
+ }
+
+ get groups(): MetricGroup[] {
+ const gropedByName = _.groupBy(this.matchedMetrics, m => m.name)
+ const metricGroups: MetricGroup[] = []
+
+ Object.keys(gropedByName).forEach(metricName => {
+ const metrics = gropedByName[metricName]
+
+ // All metrics with the same name must have the same unit (constrained in Kamon) so
+ // we can safely assume the first unit is the same for all.
+ metricGroups.push({
+ name: metricName,
+ type: metrics[0].type,
+ unitDimension: metrics[0].unitDimension,
+ unitMagnitude: metrics[0].unitMagnitude,
+ metrics
+ })
+ })
+
+ return _.sortBy(metricGroups, mg => mg.metrics.length).reverse()
+ }
+
+ get filterRegex(): RegExp {
+ return new RegExp(this.filterPattern)
+ }
+
+ get searchStats(): string {
+ if (this.filterPattern.length > 0) {
+ return 'showing ' + this.matchedMetrics.length + ' out of ' + this.totalMetrics + ' series'
+ } else {
+ return this.totalMetrics + ' series'
+ }
+ }
+
+ get matchedMetrics(): Metric[] {
+ if (this.filterPattern.length > 0) {
+ return this.metrics.filter(m => m.search.match(this.filterRegex) != null)
+ } else {
+ return this.metrics
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+.search-box {
+ input {
+ color: #676767;
+ caret-color: #676767;
+ height: 3rem;
+ border: none;
+ border-radius: 0.4rem;
+ background-color: #efefef;
+ padding-left: 3.5rem;
+ font-size: 1.1rem;
+ box-shadow: 0 2px 4px 1px rgba(0, 0, 0, 0.1);
+
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ ::placeholder {
+ color: #929292;
+ }
+
+ .search-icon {
+ color: #c0c0c0;
+ line-height: 3rem;
+ font-size: 1.4rem;
+ position: absolute;
+ left: 1rem;
+ }
+
+ .search-stats {
+ color: #a2a2a2;
+ font-size: 1.1rem;
+ position: absolute;
+ line-height: 3rem;
+ right: 0;
+ padding-right: 1rem;
+ }
+}
+
+</style>
diff --git a/kamon-status-page/src/main/vue/src/components/MetricListItem.vue b/kamon-status-page/src/main/vue/src/components/MetricListItem.vue
new file mode 100644
index 00000000..1414bbaf
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/MetricListItem.vue
@@ -0,0 +1,108 @@
+<template>
+ <status-card indicator-background-color="#7ade94" class="metric-list-item c-pointer my-1" :class="{ 'my-4': expanded }">
+
+ <div slot="status-indicator" @click="onCardClick">
+ <div class="metric-count">
+ {{ group.metrics.length }}
+ </div>
+ <div>SERIES</div>
+ </div>
+
+ <div slot="default" @click="onCardClick">
+ <div class="row no-gutters">
+ <div class="col">
+ <div class="py-3 pl-4">
+ <h5>{{ group.name }}</h5>
+ <div class="text-label">
+ <span>{{group.type}}</span>
+ <span v-if="group.unitDimension !== 'none'"> - {{ group.unitDimension }}</span>
+ <span v-if="group.unitMagnitude !== 'none'"> - {{ group.unitMagnitude }}</span>
+ </div>
+ </div>
+ </div>
+ <div class="col-auto expansion-icon px-5">
+ <i class="fas fa-fw" :class="expansionIcon"></i>
+ </div>
+ <div class="col-12 series-container" v-if="expanded">
+ <div v-for="(metric, index) in group.metrics" :key="index">
+ <div class="p-3">
+ <h6>Incarnation #{{ index + 1 }}</h6>
+ <div class="tag-container">
+ <span class="tag" v-for="tag in Object.keys(metric.tags)" :key="tag">
+ {{ tag }}=<span class="tag-value">{{ metric.tags[tag] }}</span>
+ </span>
+ <span v-if="Object.keys(metric.tags).length === 0" class="pl-2">Base Metric - No Tags</span>
+ </div>
+ </div>
+ <hr v-if="index < (group.metrics.length - 1)" class="w-100 incarnation-hr">
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ </status-card>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import {Metric} from '../api/StatusApi'
+import Card from './Card.vue'
+import StatusCard from './StatusCard.vue'
+import _ from 'underscore'
+
+export interface MetricGroup {
+ name: string
+ type: string
+ unitDimension: string
+ unitMagnitude: string
+ metrics: Metric[]
+}
+
+@Component({
+ components: {
+ 'status-card': StatusCard
+ }
+})
+export default class MetricListItem extends Vue {
+ @Prop( { default: [] }) private group!: MetricGroup
+ private expanded: boolean = false
+
+ get expansionIcon(): string {
+ return this.expanded ? 'fa-angle-up' : 'fa-angle-down'
+ }
+
+ private onCardClick() {
+ this.expanded = !this.expanded
+ }
+}
+</script>
+
+<style lang="scss">
+
+.metric-count {
+ font-size: 2rem;
+ font-weight: 700;
+ line-height: 4rem;
+}
+
+.expansion-icon {
+ line-height: 6rem;
+ color: #d0d0d0;
+ font-size: 2rem;
+}
+
+.series-container {
+ background-color: #f7f7f7;
+}
+
+.incarnation-hr {
+ border-color: #e2e2e2;
+}
+
+.metric-list-item {
+ .tag {
+ background-color: #e6e6e6;
+ }
+}
+</style>
diff --git a/kamon-status-page/src/main/vue/src/components/ModuleList.vue b/kamon-status-page/src/main/vue/src/components/ModuleList.vue
new file mode 100644
index 00000000..ac1e7963
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/ModuleList.vue
@@ -0,0 +1,86 @@
+<template>
+ <div class="w-100">
+ <status-section title="Reporters">
+ <div class="row">
+ <div class="col-12 py-1" v-for="reporter in reporterModules" :key="reporter.name">
+ <module-status-card :module="reporter" />
+ </div>
+ <div v-if="!hasApmModule" class="col-12 py-1 apm-suggestion">
+ <a href="https://kamon.io/apm/?utm_source=kamon&utm_medium=status-page&utm_campaign=kamon-status" target="_blank">
+ <module-status-card :is-suggestion="true" :module="apmModuleSuggestion" />
+ </a>
+ </div>
+ </div>
+ </status-section>
+
+ <status-section title="Modules" v-if="plainModules.length > 0">
+ <div class="row">
+ <div class="col-12 py-1" v-for="module in plainModules" :key="module.name">
+ <module-status-card :module="module"/>
+ </div>
+ </div>
+ </status-section>
+ </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import {Module, ModuleKind} from '../api/StatusApi'
+import ModuleStatusCard from './ModuleStatusCard.vue'
+import StatusSection from './StatusSection.vue'
+
+
+@Component({
+ components: {
+ 'status-section': StatusSection,
+ 'module-status-card': ModuleStatusCard
+ }
+})
+export default class ModuleList extends Vue {
+ @Prop() private modules!: Module[]
+ private apmModuleSuggestion: Module = {
+ name: 'Kamon APM',
+ description: 'See your metrics and trace data for free with a Starter account.',
+ kind: ModuleKind.Combined,
+ programmaticallyRegistered: false,
+ enabled: false,
+ started: false,
+ clazz: ''
+ }
+
+ get sortedModules(): Module[] {
+ return this.modules.sort((left, right) => {
+ if (left.started === right.started) {
+ return left.name.localeCompare(right.name)
+ } else {
+ return left.started ? -1 : 1
+ }
+ })
+ }
+
+
+ get reporterModules(): Module[] {
+ return this.sortedModules.filter(this.isReporter)
+ }
+
+ get plainModules(): Module[] {
+ return this.sortedModules.filter(m => !this.isReporter(m))
+ }
+
+ get hasApmModule(): boolean {
+ const knownApmClasses = [
+ 'kamon.kamino.KaminoReporter'
+ ]
+
+ return this.modules.find(m => knownApmClasses.indexOf(m.clazz) > 0) !== undefined
+ }
+
+ private isReporter(module: Module): boolean {
+ return [ModuleKind.Combined, ModuleKind.Span, ModuleKind.Metric].indexOf(module.kind) > 0
+ }
+
+ private isStarted(module: Module): boolean {
+ return module.started
+ }
+}
+</script> \ No newline at end of file
diff --git a/kamon-status-page/src/main/vue/src/components/ModuleStatusCard.vue b/kamon-status-page/src/main/vue/src/components/ModuleStatusCard.vue
new file mode 100644
index 00000000..18e2b038
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/ModuleStatusCard.vue
@@ -0,0 +1,52 @@
+<template>
+ <status-card :indicator-text="runStatus.message" :indicator-icon="runStatus.icon" :indicator-background-color="runStatus.color">
+ <div slot="default" class="py-3 pl-4">
+ <h5 class="mb-0 mr-3 d-inline-block">{{ module.name }}</h5>
+ <div class="tag-container d-inline-block" v-if="!isSuggestion">
+ <span class="tag">{{ module.kind }}</span>
+ <span class="tag">{{ discoveryStatus }}</span>
+ </div>
+
+ <div class="text-label">
+ {{ module.description }}
+ </div>
+ </div>
+ </status-card>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import {Module} from '../api/StatusApi'
+import StatusCard from './StatusCard.vue'
+
+
+@Component({
+ components: {
+ 'status-card': StatusCard
+ }
+})
+export default class ModuleStatusCard extends Vue {
+ @Prop({ default: false }) private isSuggestion!: boolean
+ @Prop() private module!: Module
+
+ get discoveryStatus(): string {
+ return this.module.programmaticallyRegistered ? 'manual' : 'automatic'
+ }
+
+ get runStatus(): { message: string, color: string, icon: string } {
+ if (this.isSuggestion) {
+ return { message: 'suggested', color: '#5fd7cc', icon: 'fa-plug' }
+ } else if (!this.module.enabled) {
+ return { message: 'disabled', color: '#ff9898', icon: 'fa-stop-circle' }
+ } else {
+ return this.module.started ?
+ { message: 'active', color: '#7ade94', icon: 'fa-check' } :
+ { message: 'available', color: '#bbbbbb', icon: 'fa-check' }
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+
+</style>
diff --git a/kamon-status-page/src/main/vue/src/components/OverviewCard.vue b/kamon-status-page/src/main/vue/src/components/OverviewCard.vue
new file mode 100644
index 00000000..6746d761
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/OverviewCard.vue
@@ -0,0 +1,71 @@
+<template>
+ <status-section title="Overview">
+ <card>
+ <div class="row py-2 no-gutters">
+ <div class="col-12 col-md-3 py-2 px-3">
+ <div class="text-uppercase text-label pb-1">Instrumentation</div>
+ <h5>{{instrumentationStatusMessage}}</h5>
+ </div>
+ <div class="col-12 col-md-3 py-2 px-3">
+ <div class="text-uppercase text-label pb-1">Reporters</div>
+ <h5>{{ activeReporters.length }} Started</h5>
+ </div>
+ <div class="col-12 col-md-3 py-2 px-3">
+ <div class="text-uppercase text-label pb-1">Metrics</div>
+ <h5>{{metricsStatusMessage}}</h5>
+ </div>
+ </div>
+ </card>
+ </status-section>
+</template>
+
+
+<script lang="ts">
+import { Component, Vue, Prop } from 'vue-property-decorator'
+import {Option, none, some} from 'ts-option'
+import Card from '../components/Card.vue'
+import StatusSection from '../components/StatusSection.vue'
+import {StatusApi, ModuleRegistry, ModuleKind, MetricRegistry, Module, Metric, Instrumentation} from '../api/StatusApi'
+
+@Component({
+ components: {
+ 'card': Card,
+ 'status-section': StatusSection
+ },
+})
+export default class OverviewCard extends Vue {
+ @Prop() private moduleRegistry: Option<ModuleRegistry> = none
+ @Prop() private metricRegistry: Option<MetricRegistry> = none
+ @Prop() private instrumentation: Option<Instrumentation> = none
+
+ get reporterModules(): Module[] {
+ return this.moduleRegistry
+ .map(moduleRegistry => moduleRegistry.modules.filter(this.isReporter))
+ .getOrElse([])
+ }
+
+ get activeReporters(): Module[] {
+ return this.reporterModules.filter(this.isStarted)
+ }
+
+ get trackedMetrics(): Option<number> {
+ return this.metricRegistry.map(metricRegistry => metricRegistry.metrics.length)
+ }
+
+ get instrumentationStatusMessage(): string {
+ return this.instrumentation.map(i => (i.active ? 'Active' : 'Disabled') as string).getOrElse('Unknown')
+ }
+
+ get metricsStatusMessage(): string {
+ return this.trackedMetrics.map(mc => mc + ' Series').getOrElse('Unknown')
+ }
+
+ private isReporter(module: Module): boolean {
+ return [ModuleKind.Combined, ModuleKind.Span, ModuleKind.Metric].indexOf(module.kind) > 0
+ }
+
+ private isStarted(module: Module): boolean {
+ return module.started
+ }
+}
+</script>
diff --git a/kamon-status-page/src/main/vue/src/components/StatusCard.vue b/kamon-status-page/src/main/vue/src/components/StatusCard.vue
new file mode 100644
index 00000000..c402842f
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/StatusCard.vue
@@ -0,0 +1,85 @@
+<template>
+ <card>
+ <div class="row status-card no-gutters">
+ <div class="col-auto">
+ <div class="status-indicator-wrap text-center text-uppercase" :style="indicatorStyle">
+ <slot name="status-indicator">
+ <div class="status-indicator h-100 pt-3">
+ <i class="fas fa-fw" :class="indicatorIcon"></i>
+ <div>
+ {{ indicatorText }}
+ </div>
+ </div>
+ </slot>
+ </div>
+ </div>
+ <div class="col">
+ <slot name="default">
+
+ </slot>
+ </div>
+ </div>
+ </card>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import Card from './Card.vue'
+
+@Component({
+ components: {
+ card: Card
+ }
+})
+export default class StatusCard extends Vue {
+ @Prop({ default: 'white' }) private indicatorColor!: string
+ @Prop({ default: '#989898' }) private indicatorBackgroundColor!: string
+ @Prop({ default: 'fa-question' }) private indicatorIcon!: string
+ @Prop({ default: 'Unknown' }) private indicatorText!: string
+
+ get indicatorStyle() {
+ return {
+ color: this.indicatorColor,
+ backgroundColor: this.indicatorBackgroundColor
+ }
+ }
+}
+
+</script>
+
+<style lang="scss">
+
+$indicator-size: 6rem;
+.status-card {
+ min-height: $indicator-size;
+
+ .status-indicator-wrap {
+ height: 100%;
+ min-width: $indicator-size;
+ max-width: $indicator-size;
+ min-height: $indicator-size;
+ font-size: 0.9rem;
+ font-weight: 600;
+ }
+
+ .status-indicator {
+ line-height: 2rem;
+
+ i {
+ font-size: 2.5rem;
+ }
+ }
+
+ .critical {
+ background-color: #dadada;
+ }
+
+ .healthy {
+ background-color: #7ade94;
+ }
+
+ .suggested {
+ background-color: #5fd7cc;
+ }
+}
+</style>
diff --git a/kamon-status-page/src/main/vue/src/components/StatusSection.vue b/kamon-status-page/src/main/vue/src/components/StatusSection.vue
new file mode 100644
index 00000000..b94b5bdf
--- /dev/null
+++ b/kamon-status-page/src/main/vue/src/components/StatusSection.vue
@@ -0,0 +1,19 @@
+<template>
+ <div class="row">
+ <div class="col-12 pt-4 pb-2">
+ <h3>{{ title }}</h3>
+ </div>
+ <div class="col-12 py-1">
+ <slot name="default"/>
+ </div>
+ </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component
+export default class StatusSection extends Vue {
+ @Prop() private title!: string
+}
+</script> \ No newline at end of file