diff options
Diffstat (limited to 'kamon-status-page/src/main/vue/src/components')
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 |