Commit e761e6e1 authored by Mike Greiling's avatar Mike Greiling

Merge branch '5029-support-cluster-metrics-frontend' into 5029-support-cluster-metrics

* 5029-support-cluster-metrics-frontend:
  move ee-specific files into the ee namespace
  prefer classes over IDs
  prefer getElementById
  fix karma tests
  move cluster health monitoring behind an EEU feature flag
  remove prometheus panel styling on cluster monitoring page
  limit the size of the loading and unable-to-connect states for cluster monitoring
  add settings section wrapper to empty state
  lighten axis tick text
  add ability to override graph size measurements
  fix spacing around axis label text in small breakpoints
  remove unnecessary wrapper class
  hide legend without hiding the axis labels
  add option to hide the graph legend
  use proper dependency injection for monitoring dashboard component
  make deployments endpoint optional
  integrate prometheus graphs into cluster page (rough pass)
  add prometheus cluster health monitoring empty state
parents 581f95a2 68bcd4fe
......@@ -117,7 +117,10 @@
</script>
<template>
<section class="settings no-animate expanded">
<section
id="cluster-applications"
class="settings no-animate expanded"
>
<div class="settings-header">
<h4>
{{ s__('ClusterIntegration|Applications') }}
......
......@@ -10,31 +10,79 @@
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
export default {
components: {
Graph,
GraphGroup,
EmptyState,
},
data() {
const metricsData = document.querySelector('#prometheus-graphs').dataset;
const store = new MonitoringStore();
props: {
hasMetrics: {
type: String,
required: true,
},
showLegend: {
type: Boolean,
required: false,
default: true,
},
showPanels: {
type: Boolean,
required: false,
default: true,
},
forceSmallGraph: {
type: Boolean,
required: false,
default: false,
},
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: true,
},
clustersPath: {
type: String,
required: true,
},
tagsPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
metricsEndpoint: {
type: String,
required: true,
},
deploymentEndpoint: {
type: String,
required: false,
default: null,
},
emptyGettingStartedSvgPath: {
type: String,
required: true,
},
emptyLoadingSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
},
},
data() {
return {
store,
store: new MonitoringStore(),
state: 'gettingStarted',
hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
clustersPath: metricsData.clustersPath,
tagsPath: metricsData.tagsPath,
projectPath: metricsData.projectPath,
metricsEndpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint,
emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath,
emptyLoadingSvgPath: metricsData.emptyLoadingSvgPath,
emptyUnableToConnectSvgPath: metricsData.emptyUnableToConnectSvgPath,
showEmptyState: true,
updateAspectRatio: false,
updatedAspectRatios: 0,
......@@ -60,13 +108,14 @@
mounted() {
this.resizeThrottled = _.throttle(this.resize, 600);
if (!this.hasMetrics) {
if (!convertPermissionToBoolean(this.hasMetrics)) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
window.addEventListener('resize', this.resizeThrottled, false);
}
},
methods: {
getGraphsData() {
this.state = 'loading';
......@@ -115,6 +164,7 @@
v-for="(groupData, index) in store.groups"
:key="index"
:name="groupData.group"
:show-panels="showPanels"
>
<graph
v-for="(graphData, index) in groupData.metrics"
......@@ -125,6 +175,8 @@
:deployment-data="store.deploymentData"
:project-path="projectPath"
:tags-path="tagsPath"
:show-legend="showLegend"
:small-graph="forceSmallGraph"
/>
</graph-group>
</div>
......
......@@ -52,6 +52,16 @@
type: String,
required: true,
},
showLegend: {
type: Boolean,
required: false,
default: true,
},
smallGraph: {
type: Boolean,
required: false,
default: false,
},
},
data() {
......@@ -130,7 +140,7 @@
const breakpointSize = bp.getBreakpointSize();
const query = this.graphData.queries[0];
this.margin = measurements.large.margin;
if (breakpointSize === 'xs' || breakpointSize === 'sm') {
if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300;
this.margin = measurements.small.margin;
this.measurements = measurements.small;
......@@ -182,7 +192,9 @@
this.graphHeightOffset,
);
if (this.timeSeries.length > 3) {
if (!this.showLegend) {
this.baseGraphHeight -= 50;
} else if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
......@@ -255,6 +267,7 @@
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
:show-legend-group="showLegend"
/>
<svg
class="graph-data"
......
......@@ -39,6 +39,11 @@
type: Number,
required: true,
},
showLegendGroup: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
......@@ -57,8 +62,9 @@
},
rectTransform() {
const yCoordinate = ((this.graphHeight - this.margin.top) / 2)
+ (this.yLabelWidth / 2) + 10 || 0;
const yCoordinate = (((this.graphHeight - this.margin.top)
+ this.measurements.axisLabelLineOffset) / 2)
+ (this.yLabelWidth / 2) || 0;
return `translate(0, ${yCoordinate}) rotate(-90)`;
},
......@@ -166,6 +172,7 @@
>
Time
</text>
<template v-if="showLegendGroup">
<g
class="legend-group"
v-for="(series, index) in timeSeries"
......@@ -200,5 +207,6 @@
{{ legendTitle }} {{ formatMetricUsage(series) }}
</text>
</g>
</template>
</g>
</template>
......@@ -5,12 +5,20 @@
type: String,
required: true,
},
showPanels: {
type: Boolean,
required: false,
default: true,
},
},
};
</script>
<template>
<div class="panel panel-default prometheus-panel">
<div
v-if="showPanels"
class="panel panel-default prometheus-panel"
>
<div class="panel-heading">
<h4>{{ name }}</h4>
</div>
......@@ -18,4 +26,10 @@
<slot></slot>
</div>
</div>
<div
v-else
class="prometheus-graph-group"
>
<slot></slot>
</div>
</template>
import Vue from 'vue';
import Dashboard from './components/dashboard.vue';
export default () => new Vue({
el: '#prometheus-graphs',
render: createElement => createElement(Dashboard),
});
export default () => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
// eslint-disable-next-line no-new
new Vue({
el,
render(createElement) {
return createElement(Dashboard, {
props: el.dataset,
});
},
});
}
};
......@@ -40,6 +40,9 @@ export default class MonitoringService {
}
getDeploymentData() {
if (!this.deploymentEndpoint) {
return Promise.resolve([]);
}
return backOffRequest(() => axios.get(this.deploymentEndpoint))
.then(resp => resp.data)
.then((response) => {
......
......@@ -529,7 +529,8 @@
}
> text {
font-size: 12px;
fill: $theme-gray-600;
font-size: 10px;
}
}
......@@ -573,3 +574,17 @@
}
}
}
// EE-only
.cluster-health-graphs {
.prometheus-state {
.state-svg img {
max-height: 120px;
}
.state-description,
.state-button {
display: none;
}
}
}
......@@ -22,6 +22,10 @@
.js-cluster-application-notice
.flash-container
-# EE-specific
- if @cluster.project.feature_available?(:cluster_health)
= render 'health'
%section.settings.no-animate.expanded#cluster-integration
= render 'banner'
= render 'integration_form'
......
......@@ -15,7 +15,8 @@
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
"additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json),
"metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json),
"deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json),
"project-path": project_path(@project),
"tags-path": project_tags_path(@project),
"has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } }
"has-metrics": "#{@environment.has_metrics?}" } }
import Vue from 'vue';
import Dashboard from '~/monitoring/components/dashboard.vue';
export default () => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
// eslint-disable-next-line no-new
new Vue({
el,
render(createElement) {
return createElement(Dashboard, {
props: {
...el.dataset,
showLegend: false,
showPanels: false,
forceSmallGraph: true,
},
});
},
});
}
};
import '~/pages/projects/clusters/show';
import initClusterHealth from './cluster_health';
document.addEventListener('DOMContentLoaded', initClusterHealth);
......@@ -59,6 +59,7 @@ class License < ActiveRecord::Base
EEU_FEATURES = EEP_FEATURES + %i[
sast
sast_container
cluster_health
dast
epics
ide
......
%section.settings.no-animate.expanded.cluster-health-graphs#cluster-health
%h4= s_('ClusterIntegration|Kubernetes cluster health')
- if @cluster&.application_prometheus&.installed?
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
"clusters-path": project_clusters_path(@project),
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint": metrics_namespace_project_cluster_path( format: :json ),
"project-path": project_path(@project),
"tags-path": project_tags_path(@project),
"has-metrics": "true" } }
- else
.settings-content
%p= s_("ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data.")
%a.btn{ href: '#cluster-applications' }
= s_('ClusterIntegration|Install Prometheus')
......@@ -8,6 +8,20 @@ describe('Dashboard', () => {
const fixtureName = 'environments/metrics/metrics.html.raw';
let DashboardComponent;
let component;
const propsData = {
hasMetrics: 'false',
documentationPath: '/path/to/docs',
settingsPath: '/path/to/settings',
clustersPath: '/path/to/clusters',
tagsPath: '/path/to/tags',
projectPath: '/path/to/project',
metricsEndpoint: mockApiEndpoint,
deploymentEndpoint: '/endpoint/deployments',
emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
emptyLoadingSvgPath: '/path/to/loading.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
};
preloadFixtures(fixtureName);
beforeEach(() => {
......@@ -19,6 +33,7 @@ describe('Dashboard', () => {
it('shows a getting started empty state when no metrics are present', () => {
component = new DashboardComponent({
el: document.querySelector('#prometheus-graphs'),
propsData,
});
component.$mount();
......@@ -30,7 +45,6 @@ describe('Dashboard', () => {
describe('requests information to the server', () => {
let mock;
beforeEach(() => {
document.querySelector('#prometheus-graphs').setAttribute('data-has-metrics', 'true');
mock = new MockAdapter(axios);
mock.onGet(mockApiEndpoint).reply(200, {
metricsGroupsAPIResponse,
......@@ -44,6 +58,7 @@ describe('Dashboard', () => {
it('shows up a loading state', (done) => {
component = new DashboardComponent({
el: document.querySelector('#prometheus-graphs'),
propsData: { ...propsData, hasMetrics: 'true' },
});
component.$mount();
Vue.nextTick(() => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment