Commit 9fcaabb8 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-11-30' into 'master'

CE upstream - 2018-11-30 05:21 UTC

See merge request gitlab-org/gitlab-ee!8649
parents d4cb6614 c2e76c56
...@@ -16,6 +16,7 @@ Set the title to: `[Security] Description of the original issue` ...@@ -16,6 +16,7 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add a link to the MR to the [links section](#links) - [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required - [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**. - [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
- [ ] Add a link to this issue on the original security issue.
#### Backports #### Backports
...@@ -37,6 +38,7 @@ Set the title to: `[Security] Description of the original issue` ...@@ -37,6 +38,7 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details) - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details) - [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details) - [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
- [ ] Once your `master` MR is merged, comment on the original security issue with a link to that MR indicating the issue is fixed.
### Summary ### Summary
......
...@@ -105,6 +105,9 @@ export default { ...@@ -105,6 +105,9 @@ export default {
deploymentFlagData() { deploymentFlagData() {
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag); return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
}, },
shouldRenderData() {
return this.graphData.queries.filter(s => s.result.length > 0).length > 0;
},
}, },
watch: { watch: {
hoverData() { hoverData() {
...@@ -120,17 +123,17 @@ export default { ...@@ -120,17 +123,17 @@ export default {
}, },
draw() { draw() {
const breakpointSize = bp.getBreakpointSize(); const breakpointSize = bp.getBreakpointSize();
const query = this.graphData.queries[0];
const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width; const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
this.margin = measurements.large.margin; this.margin = measurements.large.margin;
if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') { if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300; this.graphHeight = 300;
this.margin = measurements.small.margin; this.margin = measurements.small.margin;
this.measurements = measurements.small; this.measurements = measurements.small;
} }
this.unitOfDisplay = query.unit || '';
this.yAxisLabel = this.graphData.y_label || 'Values'; this.yAxisLabel = this.graphData.y_label || 'Values';
this.legendTitle = query.label || 'Average';
this.graphWidth = svgWidth - this.margin.left - this.margin.right; this.graphWidth = svgWidth - this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight - 50; this.baseGraphHeight = this.graphHeight - 50;
...@@ -139,8 +142,15 @@ export default { ...@@ -139,8 +142,15 @@ export default {
// pixel offsets inside the svg and outside are not 1:1 // pixel offsets inside the svg and outside are not 1:1
this.realPixelRatio = svgWidth / this.baseGraphWidth; this.realPixelRatio = svgWidth / this.baseGraphWidth;
// set the legends on the axes
const [query] = this.graphData.queries;
this.legendTitle = query ? query.label : 'Average';
this.unitOfDisplay = query ? query.unit : '';
if (this.shouldRenderData) {
this.renderAxesPaths(); this.renderAxesPaths();
this.formatDeployments(); this.formatDeployments();
}
}, },
handleMouseOverGraph(e) { handleMouseOverGraph(e) {
let point = this.$refs.graphData.createSVGPoint(); let point = this.$refs.graphData.createSVGPoint();
...@@ -266,7 +276,7 @@ export default { ...@@ -266,7 +276,7 @@ export default {
:y-axis-label="yAxisLabel" :y-axis-label="yAxisLabel"
:unit-of-display="unitOfDisplay" :unit-of-display="unitOfDisplay"
/> />
<svg ref="graphData" :viewBox="innerViewBox" class="graph-data"> <svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data">
<slot name="additionalSvgContent" :graphDrawData="graphDrawData" /> <slot name="additionalSvgContent" :graphDrawData="graphDrawData" />
<graph-path <graph-path
v-for="(path, index) in timeSeries" v-for="(path, index) in timeSeries"
...@@ -293,8 +303,14 @@ export default { ...@@ -293,8 +303,14 @@ export default {
@mousemove="handleMouseOverGraph($event);" @mousemove="handleMouseOverGraph($event);"
/> />
</svg> </svg>
<svg v-else :viewBox="innerViewBox" class="js-no-data-to-display">
<text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">
{{ s__('Metrics|No data to display') }}
</text>
</svg>
</svg> </svg>
<graph-flag <graph-flag
v-if="shouldRenderData"
:real-pixel-ratio="realPixelRatio" :real-pixel-ratio="realPixelRatio"
:current-x-coordinate="currentXCoordinate" :current-x-coordinate="currentXCoordinate"
:current-data="currentData" :current-data="currentData"
......
...@@ -7,10 +7,29 @@ function sortMetrics(metrics) { ...@@ -7,10 +7,29 @@ function sortMetrics(metrics) {
.value(); .value();
} }
function checkQueryEmptyData(query) {
return {
...query,
result: query.result.filter(timeSeries => {
const newTimeSeries = timeSeries;
const hasValue = series =>
!Number.isNaN(series.value) && (series.value !== null || series.value !== undefined);
const hasNonNullValue = timeSeries.values.find(hasValue);
newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
return newTimeSeries.values.length > 0;
}),
};
}
function removeTimeSeriesNoData(queries) {
return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []);
}
function normalizeMetrics(metrics) { function normalizeMetrics(metrics) {
return metrics.map(metric => ({ return metrics.map(metric => {
...metric, const queries = metric.queries.map(query => ({
queries: metric.queries.map(query => ({
...query, ...query,
result: query.result.map(result => ({ result: query.result.map(result => ({
...result, ...result,
...@@ -19,8 +38,13 @@ function normalizeMetrics(metrics) { ...@@ -19,8 +38,13 @@ function normalizeMetrics(metrics) {
value: Number(value), value: Number(value),
})), })),
})), })),
})),
})); }));
return {
...metric,
queries: removeTimeSeriesNoData(queries),
};
});
} }
export default class MonitoringStore { export default class MonitoringStore {
......
---
title: Add empty state for graphs with no values
merge_request: 22630
author:
type: fixed
...@@ -92,13 +92,47 @@ To add an existing Kubernetes cluster to your project: ...@@ -92,13 +92,47 @@ To add an existing Kubernetes cluster to your project:
the `ca.crt` contents here. the `ca.crt` contents here.
- **Token** - - **Token** -
GitLab authenticates against Kubernetes using service tokens, which are GitLab authenticates against Kubernetes using service tokens, which are
scoped to a particular `namespace`. If you don't have a service token yet, scoped to a particular `namespace`.
you can follow the **The token used should belong to a service account with
[Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
to create one. You can also view or create service tokens in the privileges.** To create this service account:
[Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/)
(under **Config > Secrets**). **The account that will issue the service token 1. Create a `gitlab` service account in the `default` namespace:
must have admin privileges on the cluster.**
```bash
kubectl create -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitlab
namespace: default
EOF
```
1. Create a cluster role binding to give the `gitlab` service account
`cluster-admin` privileges:
```bash
kubectl create -f - <<EOF
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: gitlab-cluster-admin
subjects:
- kind: ServiceAccount
name: gitlab
namespace: default
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
EOF
```
NOTE: **Note:**
For GKE clusters, you will need the
`container.clusterRoleBindings.create` permission to create a cluster
role binding. You can follow the [Google Cloud
documentation](https://cloud.google.com/iam/docs/granting-changing-revoking-access)
to grant access.
- **Project namespace** (optional) - You don't have to fill it in; by leaving - **Project namespace** (optional) - You don't have to fill it in; by leaving
it blank, GitLab will create one for you. Also: it blank, GitLab will create one for you. Also:
- Each project should have a unique namespace. - Each project should have a unique namespace.
...@@ -143,8 +177,9 @@ Whether ABAC or RBAC is enabled, GitLab will create the necessary ...@@ -143,8 +177,9 @@ Whether ABAC or RBAC is enabled, GitLab will create the necessary
service accounts and privileges in order to install and run service accounts and privileges in order to install and run
[GitLab managed applications](#installing-applications): [GitLab managed applications](#installing-applications):
- A `gitlab` service account with `cluster-admin` privileges will be created in the - If GitLab is creating the cluster, a `gitlab` service account with
`default` namespace, which will be used by GitLab to manage the newly created cluster. `cluster-admin` privileges will be created in the `default` namespace,
which will be used by GitLab to manage the newly created cluster.
- A project service account with [`edit` - A project service account with [`edit`
privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
......
...@@ -5331,6 +5331,9 @@ msgstr "" ...@@ -5331,6 +5331,9 @@ msgstr ""
msgid "Metrics|New metric" msgid "Metrics|New metric"
msgstr "" msgstr ""
msgid "Metrics|No data to display"
msgstr ""
msgid "Metrics|No deployed environments" msgid "Metrics|No deployed environments"
msgstr "" msgstr ""
......
...@@ -70,3 +70,5 @@ Disallow: /*/*/hooks ...@@ -70,3 +70,5 @@ Disallow: /*/*/hooks
Disallow: /*/*/services Disallow: /*/*/services
Disallow: /*/*/protected_branches Disallow: /*/*/protected_branches
Disallow: /*/*/uploads/ Disallow: /*/*/uploads/
Disallow: /*/-/group_members
Disallow: /*/project_members
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
deploymentData, deploymentData,
convertDatesMultipleSeries, convertDatesMultipleSeries,
singleRowMetricsMultipleSeries, singleRowMetricsMultipleSeries,
queryWithoutData,
} from './mock_data'; } from './mock_data';
const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags'; const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags';
...@@ -104,4 +105,23 @@ describe('Graph', () => { ...@@ -104,4 +105,23 @@ describe('Graph', () => {
expect(component.currentData).toBe(component.timeSeries[0].values[10]); expect(component.currentData).toBe(component.timeSeries[0].values[10]);
}); });
describe('Without data to display', () => {
it('shows a "no data to display" empty state on a graph', done => {
const component = createComponent({
graphData: queryWithoutData,
deploymentData,
tagsPath,
projectPath,
});
Vue.nextTick(() => {
expect(
component.$el.querySelector('.js-no-data-to-display text').textContent.trim(),
).toEqual('No data to display');
done();
});
});
});
}); });
...@@ -14,7 +14,7 @@ export const metricsGroupsAPIResponse = { ...@@ -14,7 +14,7 @@ export const metricsGroupsAPIResponse = {
queries: [ queries: [
{ {
query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20', query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20',
y_label: 'Memory', label: 'Memory',
unit: 'MiB', unit: 'MiB',
result: [ result: [
{ {
...@@ -324,12 +324,15 @@ export const metricsGroupsAPIResponse = { ...@@ -324,12 +324,15 @@ export const metricsGroupsAPIResponse = {
], ],
}, },
{ {
id: 6,
title: 'CPU usage', title: 'CPU usage',
weight: 1, weight: 1,
queries: [ queries: [
{ {
query_range: query_range:
'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100', 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100',
label: 'Core Usage',
unit: 'Cores',
result: [ result: [
{ {
metric: {}, metric: {},
...@@ -639,6 +642,39 @@ export const metricsGroupsAPIResponse = { ...@@ -639,6 +642,39 @@ export const metricsGroupsAPIResponse = {
}, },
], ],
}, },
{
group: 'NGINX',
priority: 2,
metrics: [
{
id: 100,
title: 'Http Error Rate',
weight: 100,
queries: [
{
query_range:
'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100',
label: '5xx errors',
unit: '%',
result: [
{
metric: {},
values: [
[1495700554.925, NaN],
[1495700614.925, NaN],
[1495700674.925, NaN],
[1495700734.925, NaN],
[1495700794.925, NaN],
[1495700854.925, NaN],
[1495700914.925, NaN],
],
},
],
},
],
},
],
},
], ],
last_update: '2017-05-25T13:18:34.949Z', last_update: '2017-05-25T13:18:34.949Z',
}; };
...@@ -6526,6 +6562,21 @@ export const singleRowMetricsMultipleSeries = [ ...@@ -6526,6 +6562,21 @@ export const singleRowMetricsMultipleSeries = [
}, },
]; ];
export const queryWithoutData = {
title: 'HTTP Error rate',
weight: 10,
y_label: 'Http Error Rate',
queries: [
{
query_range:
'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100',
label: '5xx errors',
unit: '%',
result: [],
},
],
};
export function convertDatesMultipleSeries(multipleSeries) { export function convertDatesMultipleSeries(multipleSeries) {
const convertedMultiple = multipleSeries; const convertedMultiple = multipleSeries;
multipleSeries.forEach((column, index) => { multipleSeries.forEach((column, index) => {
......
import MonitoringStore from '~/monitoring/stores/monitoring_store'; import MonitoringStore from '~/monitoring/stores/monitoring_store';
import MonitoringMock, { deploymentData, environmentData } from './mock_data'; import MonitoringMock, { deploymentData, environmentData } from './mock_data';
describe('MonitoringStore', function() { describe('MonitoringStore', () => {
this.store = new MonitoringStore(); const store = new MonitoringStore();
this.store.storeMetrics(MonitoringMock.data); store.storeMetrics(MonitoringMock.data);
it('contains one group that contains two queries sorted by priority', () => { it('contains two groups that contains, one of which has two queries sorted by priority', () => {
expect(this.store.groups).toBeDefined(); expect(store.groups).toBeDefined();
expect(this.store.groups.length).toEqual(1); expect(store.groups.length).toEqual(2);
expect(this.store.groups[0].metrics.length).toEqual(2); expect(store.groups[0].metrics.length).toEqual(2);
}); });
it('gets the metrics count for every group', () => { it('gets the metrics count for every group', () => {
expect(this.store.getMetricsCount()).toEqual(2); expect(store.getMetricsCount()).toEqual(3);
}); });
it('contains deployment data', () => { it('contains deployment data', () => {
this.store.storeDeploymentData(deploymentData); store.storeDeploymentData(deploymentData);
expect(this.store.deploymentData).toBeDefined(); expect(store.deploymentData).toBeDefined();
expect(this.store.deploymentData.length).toEqual(3); expect(store.deploymentData.length).toEqual(3);
expect(typeof this.store.deploymentData[0]).toEqual('object'); expect(typeof store.deploymentData[0]).toEqual('object');
}); });
it('only stores environment data that contains deployments', () => { it('only stores environment data that contains deployments', () => {
this.store.storeEnvironmentsData(environmentData); store.storeEnvironmentsData(environmentData);
expect(store.environmentsData.length).toEqual(2);
});
expect(this.store.environmentsData.length).toEqual(2); it('removes the data if all the values from a query are not defined', () => {
expect(store.groups[1].metrics[0].queries[0].result.length).toEqual(0);
}); });
}); });
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