summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue3
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue334
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue10
-rw-r--r--app/assets/javascripts/monitoring/constants.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue1
-rw-r--r--changelogs/unreleased/32032-html-code-shown-in-merge-request.yml5
-rw-r--r--changelogs/unreleased/65412-add-support-for-line-charts.yml5
-rw-r--r--config/prometheus/common_metrics.yml8
-rw-r--r--db/migrate/20190814205640_import_common_metrics_line_charts.rb13
-rw-r--r--doc/administration/high_availability/load_balancer.md57
-rw-r--r--doc/administration/monitoring/gitlab_instance_administration_project/index.md2
-rw-r--r--doc/administration/monitoring/performance/request_profiling.md2
-rw-r--r--doc/install/azure/index.md2
-rw-r--r--doc/user/group/saml_sso/index.md19
-rw-r--r--doc/user/profile/account/two_factor_authentication.md1
-rw-r--r--spec/frontend/test_setup.js6
-rw-r--r--spec/javascripts/monitoring/charts/time_series_spec.js335
-rw-r--r--spec/javascripts/monitoring/mock_data.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js18
19 files changed, 792 insertions, 42 deletions
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 90c764587a3..78d97a3c122 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -12,6 +12,9 @@ import { graphDataValidatorForValues } from '../../utils';
let debouncedResize;
+// TODO: Remove this component in favor of the more general time_series.vue
+// Please port all changes here to time_series.vue as well.
+
export default {
components: {
GlAreaChart,
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
new file mode 100644
index 00000000000..2fdc75f63ca
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -0,0 +1,334 @@
+<script>
+import { __ } from '~/locale';
+import { mapState } from 'vuex';
+import { GlLink, GlButton } from '@gitlab/ui';
+import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
+import dateFormat from 'dateformat';
+import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import Icon from '~/vue_shared/components/icon.vue';
+import { chartHeight, graphTypes, lineTypes, symbolSizes, dateFormats } from '../../constants';
+import { makeDataSeries } from '~/helpers/monitor_helper';
+import { graphDataValidatorForValues } from '../../utils';
+
+let debouncedResize;
+
+export default {
+ components: {
+ GlAreaChart,
+ GlLineChart,
+ GlButton,
+ GlChartSeriesLabel,
+ GlLink,
+ Icon,
+ },
+ inheritAttrs: false,
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: graphDataValidatorForValues.bind(null, false),
+ },
+ containerWidth: {
+ type: Number,
+ required: true,
+ },
+ deploymentData: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showBorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ thresholds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ tooltip: {
+ title: '',
+ content: [],
+ commitUrl: '',
+ isDeployment: false,
+ sha: '',
+ },
+ width: 0,
+ height: chartHeight,
+ svgs: {},
+ primaryColor: null,
+ };
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']),
+ chartData() {
+ // Transforms & supplements query data to render appropriate labels & styles
+ // Input: [{ queryAttributes1 }, { queryAttributes2 }]
+ // Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
+ return this.graphData.queries.reduce((acc, query) => {
+ const { appearance } = query;
+ const lineType =
+ appearance && appearance.line && appearance.line.type
+ ? appearance.line.type
+ : lineTypes.default;
+ const lineWidth =
+ appearance && appearance.line && appearance.line.width
+ ? appearance.line.width
+ : undefined;
+ const areaStyle = {
+ opacity:
+ appearance && appearance.area && typeof appearance.area.opacity === 'number'
+ ? appearance.area.opacity
+ : undefined,
+ };
+
+ const series = makeDataSeries(query.result, {
+ name: this.formatLegendLabel(query),
+ lineStyle: {
+ type: lineType,
+ width: lineWidth,
+ },
+ showSymbol: false,
+ areaStyle: this.graphData.type === 'area-chart' ? areaStyle : undefined,
+ });
+
+ return acc.concat(series);
+ }, []);
+ },
+ chartOptions() {
+ return {
+ xAxis: {
+ name: __('Time'),
+ type: 'time',
+ axisLabel: {
+ formatter: date => dateFormat(date, dateFormats.timeOfDay),
+ },
+ axisPointer: {
+ snap: true,
+ },
+ },
+ yAxis: {
+ name: this.yAxisLabel,
+ axisLabel: {
+ formatter: num => roundOffFloat(num, 3).toString(),
+ },
+ },
+ series: this.scatterSeries,
+ dataZoom: this.dataZoomConfig,
+ };
+ },
+ dataZoomConfig() {
+ const handleIcon = this.svgs['scroll-handle'];
+
+ return handleIcon ? { handleIcon } : {};
+ },
+ earliestDatapoint() {
+ return this.chartData.reduce((acc, series) => {
+ const { data } = series;
+ const { length } = data;
+ if (!length) {
+ return acc;
+ }
+
+ const [first] = data[0];
+ const [last] = data[length - 1];
+ const seriesEarliest = first < last ? first : last;
+
+ return seriesEarliest < acc || acc === null ? seriesEarliest : acc;
+ }, null);
+ },
+ glChartComponent() {
+ const chartTypes = {
+ 'area-chart': GlAreaChart,
+ 'line-chart': GlLineChart,
+ };
+ return chartTypes[this.graphData.type] || GlAreaChart;
+ },
+ isMultiSeries() {
+ return this.tooltip.content.length > 1;
+ },
+ recentDeployments() {
+ return this.deploymentData.reduce((acc, deployment) => {
+ if (deployment.created_at >= this.earliestDatapoint) {
+ const { id, created_at, sha, ref, tag } = deployment;
+ acc.push({
+ id,
+ createdAt: created_at,
+ sha,
+ commitUrl: `${this.projectPath}/commit/${sha}`,
+ tag,
+ tagUrl: tag ? `${this.tagsPath}/${ref.name}` : null,
+ ref: ref.name,
+ showDeploymentFlag: false,
+ });
+ }
+
+ return acc;
+ }, []);
+ },
+ scatterSeries() {
+ return {
+ type: graphTypes.deploymentData,
+ data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]),
+ symbol: this.svgs.rocket,
+ symbolSize: symbolSizes.default,
+ itemStyle: {
+ color: this.primaryColor,
+ },
+ };
+ },
+ yAxisLabel() {
+ return `${this.graphData.y_label}`;
+ },
+ csvText() {
+ const chartData = this.chartData[0].data;
+ const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
+ return chartData.reduce((csv, data) => {
+ const row = data.join(',');
+ return `${csv}${row}\r\n`;
+ }, header);
+ },
+ downloadLink() {
+ const data = new Blob([this.csvText], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
+ },
+ watch: {
+ containerWidth: 'onResize',
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', debouncedResize);
+ },
+ created() {
+ debouncedResize = debounceByAnimationFrame(this.onResize);
+ window.addEventListener('resize', debouncedResize);
+ this.setSvg('rocket');
+ this.setSvg('scroll-handle');
+ },
+ methods: {
+ formatLegendLabel(query) {
+ return `${query.label}`;
+ },
+ formatTooltipText(params) {
+ this.tooltip.title = dateFormat(params.value, dateFormats.default);
+ this.tooltip.content = [];
+ params.seriesData.forEach(dataPoint => {
+ const [xVal, yVal] = dataPoint.value;
+ this.tooltip.isDeployment = dataPoint.componentSubType === graphTypes.deploymentData;
+ if (this.tooltip.isDeployment) {
+ const [deploy] = this.recentDeployments.filter(
+ deployment => deployment.createdAt === xVal,
+ );
+ this.tooltip.sha = deploy.sha.substring(0, 8);
+ this.tooltip.commitUrl = deploy.commitUrl;
+ } else {
+ const { seriesName, color } = dataPoint;
+ const value = yVal.toFixed(3);
+ this.tooltip.content.push({
+ name: seriesName,
+ value,
+ color,
+ });
+ }
+ });
+ },
+ setSvg(name) {
+ getSvgIconPathContent(name)
+ .then(path => {
+ if (path) {
+ this.$set(this.svgs, name, `path://${path}`);
+ }
+ })
+ .catch(e => {
+ // eslint-disable-next-line no-console, @gitlab/i18n/no-non-i18n-strings
+ console.error('SVG could not be rendered correctly: ', e);
+ });
+ },
+ onChartUpdated(chart) {
+ [this.primaryColor] = chart.getOption().color;
+ },
+ onResize() {
+ if (!this.$refs.chart) return;
+ const { width } = this.$refs.chart.$el.getBoundingClientRect();
+ this.width = width;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']">
+ <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
+ <div class="prometheus-graph-header">
+ <h5 class="prometheus-graph-title js-graph-title">{{ graphData.title }}</h5>
+ <gl-button
+ v-if="exportMetricsToCsvEnabled"
+ :href="downloadLink"
+ :title="__('Download CSV')"
+ :aria-label="__('Download CSV')"
+ style="margin-left: 200px;"
+ download="chart_metrics.csv"
+ >
+ {{ __('Download CSV') }}
+ </gl-button>
+ <div class="prometheus-graph-widgets js-graph-widgets">
+ <slot></slot>
+ </div>
+ </div>
+
+ <component
+ :is="glChartComponent"
+ ref="chart"
+ v-bind="$attrs"
+ :data="chartData"
+ :option="chartOptions"
+ :format-tooltip-text="formatTooltipText"
+ :thresholds="thresholds"
+ :width="width"
+ :height="height"
+ @updated="onChartUpdated"
+ >
+ <template v-if="tooltip.isDeployment">
+ <template slot="tooltipTitle">
+ {{ __('Deployed') }}
+ </template>
+ <div slot="tooltipContent" class="d-flex align-items-center">
+ <icon name="commit" class="mr-2" />
+ <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
+ </div>
+ </template>
+ <template v-else>
+ <template slot="tooltipTitle">
+ <div class="text-nowrap">
+ {{ tooltip.title }}
+ </div>
+ </template>
+ <template slot="tooltipContent">
+ <div
+ v-for="(content, key) in tooltip.content"
+ :key="key"
+ class="d-flex justify-content-between"
+ >
+ <gl-chart-series-label :color="isMultiSeries ? content.color : ''">
+ {{ content.name }}
+ </gl-chart-series-label>
+ <div class="prepend-left-32">
+ {{ content.value }}
+ </div>
+ </div>
+ </template>
+ </template>
+ </component>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index ebd610af7b6..d330ceb836c 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -15,7 +15,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
-import MonitorAreaChart from './charts/area.vue';
+import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
@@ -26,7 +26,7 @@ let sidebarMutationObserver;
export default {
components: {
- MonitorAreaChart,
+ MonitorTimeSeriesChart,
MonitorSingleStatChart,
PanelType,
GraphGroup,
@@ -465,7 +465,7 @@ export default {
/>
</template>
<template v-else>
- <monitor-area-chart
+ <monitor-time-series-chart
v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="graphIndex"
:graph-data="graphData"
@@ -473,7 +473,7 @@ export default {
:thresholds="getGraphAlertValues(graphData.queries)"
:container-width="elWidth"
:project-path="projectPath"
- group-id="monitor-area-chart"
+ group-id="monitor-time-series-chart"
>
<div class="d-flex align-items-center">
<alert-widget
@@ -515,7 +515,7 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</div>
- </monitor-area-chart>
+ </monitor-time-series-chart>
</template>
</graph-group>
</div>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index d7d89522732..13aba3d9f44 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -8,6 +8,10 @@ export const graphTypes = {
deploymentData: 'scatter',
};
+export const symbolSizes = {
+ default: 14,
+};
+
export const lineTypes = {
default: 'solid',
};
@@ -21,6 +25,11 @@ export const timeWindows = {
oneWeek: __('1 week'),
};
+export const dateFormats = {
+ timeOfDay: 'h:MM TT',
+ default: 'dd mmm yyyy, h:MMTT',
+};
+
export const secondsIn = {
thirtyMinutes: 60 * 30,
threeHours: 60 * 60 * 3,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index c7b064b8506..339e154affc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -50,6 +50,7 @@ export default {
startTag: '<span class="label-branch">',
endTag: '</span>',
},
+ false,
);
},
},
diff --git a/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml b/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml
new file mode 100644
index 00000000000..ffd58067784
--- /dev/null
+++ b/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: Fix HTML rendering for fast-forward rebases in merge request widget
+merge_request: 32032
+author:
+type: fixed
diff --git a/changelogs/unreleased/65412-add-support-for-line-charts.yml b/changelogs/unreleased/65412-add-support-for-line-charts.yml
new file mode 100644
index 00000000000..cb9043596b7
--- /dev/null
+++ b/changelogs/unreleased/65412-add-support-for-line-charts.yml
@@ -0,0 +1,5 @@
+---
+title: Create component to display area and line charts in monitor dashboards
+merge_request: 31639
+author:
+type: added
diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml
index 32475ef8380..08504d6f7d5 100644
--- a/config/prometheus/common_metrics.yml
+++ b/config/prometheus/common_metrics.yml
@@ -166,7 +166,7 @@ panel_groups:
label: Total (cores)
unit: "cores"
- title: "Memory Usage (Pod average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Memory Used per Pod (MB)"
weight: 2
metrics:
@@ -175,7 +175,7 @@ panel_groups:
label: Pod average (MB)
unit: MB
- title: "Canary: Memory Usage (Pod Average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Memory Used per Pod (MB)"
weight: 2
metrics:
@@ -185,7 +185,7 @@ panel_groups:
unit: MB
track: canary
- title: "Core Usage (Pod Average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
@@ -194,7 +194,7 @@ panel_groups:
label: Pod average (cores)
unit: "cores"
- title: "Canary: Core Usage (Pod Average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
diff --git a/db/migrate/20190814205640_import_common_metrics_line_charts.rb b/db/migrate/20190814205640_import_common_metrics_line_charts.rb
new file mode 100644
index 00000000000..1c28d686a42
--- /dev/null
+++ b/db/migrate/20190814205640_import_common_metrics_line_charts.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ImportCommonMetricsLineCharts < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index 9e9f604317a..f11d27487d1 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -60,11 +60,21 @@ for details on managing SSL certificates and configuring Nginx.
### Basic ports
-| LB Port | Backend Port | Protocol |
-| ------- | ------------ | --------------- |
-| 80 | 80 | HTTP [^1] |
-| 443 | 443 | TCP or HTTPS [^1] [^2] |
-| 22 | 22 | TCP |
+| LB Port | Backend Port | Protocol |
+| ------- | ------------ | ------------------------ |
+| 80 | 80 | HTTP (*1*) |
+| 443 | 443 | TCP or HTTPS (*1*) (*2*) |
+| 22 | 22 | TCP |
+
+- (*1*): [Web terminal](../../ci/environments.md#web-terminals) support requires
+ your load balancer to correctly handle WebSocket connections. When using
+ HTTP or HTTPS proxying, this means your load balancer must be configured
+ to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the
+ [web terminal](../integration/terminal.md) integration guide for
+ more details.
+- (*2*): When using HTTPS protocol for port 443, you will need to add an SSL
+ certificate to the load balancers. If you wish to terminate SSL at the
+ GitLab application server instead, use TCP protocol.
### GitLab Pages Ports
@@ -72,12 +82,19 @@ If you're using GitLab Pages with custom domain support you will need some
additional port configurations.
GitLab Pages requires a separate virtual IP address. Configure DNS to point the
`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the
-[GitLab Pages documentation][gitlab-pages] for more information.
+[GitLab Pages documentation](../pages/index.md) for more information.
-| LB Port | Backend Port | Protocol |
-| ------- | ------------ | -------- |
-| 80 | Varies [^3] | HTTP |
-| 443 | Varies [^3] | TCP [^4] |
+| LB Port | Backend Port | Protocol |
+| ------- | ------------- | --------- |
+| 80 | Varies (*1*) | HTTP |
+| 443 | Varies (*1*) | TCP (*2*) |
+
+- (*1*): The backend port for GitLab Pages depends on the
+ `gitlab_pages['external_http']` and `gitlab_pages['external_https']`
+ setting. See [GitLab Pages documentation](../pages/index.md) for more details.
+- (*2*): Port 443 for GitLab Pages should always use the TCP protocol. Users can
+ configure custom domains with custom SSL, which would not be possible
+ if SSL was terminated at the load balancer.
### Alternate SSH Port
@@ -86,7 +103,7 @@ it may be helpful to configure an alternate SSH hostname that allows users
to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address
compared to the other GitLab HTTP configuration above.
-Configure DNS for an alternate SSH hostname such as altssh.gitlab.example.com.
+Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`.
| LB Port | Backend Port | Protocol |
| ------- | ------------ | -------- |
@@ -101,24 +118,6 @@ Read more on high-availability configuration:
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
-[^1]: [Web terminal](../../ci/environments.md#web-terminals) support requires
- your load balancer to correctly handle WebSocket connections. When using
- HTTP or HTTPS proxying, this means your load balancer must be configured
- to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the
- [web terminal](../integration/terminal.md) integration guide for
- more details.
-[^2]: When using HTTPS protocol for port 443, you will need to add an SSL
- certificate to the load balancers. If you wish to terminate SSL at the
- GitLab application server instead, use TCP protocol.
-[^3]: The backend port for GitLab Pages depends on the
- `gitlab_pages['external_http']` and `gitlab_pages['external_https']`
- setting. See [GitLab Pages documentation][gitlab-pages] for more details.
-[^4]: Port 443 for GitLab Pages should always use the TCP protocol. Users can
- configure custom domains with custom SSL, which would not be possible
- if SSL was terminated at the load balancer.
-
-[gitlab-pages]: ../pages/index.md
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/administration/monitoring/gitlab_instance_administration_project/index.md b/doc/administration/monitoring/gitlab_instance_administration_project/index.md
index 8e33cea6217..d445b68721d 100644
--- a/doc/administration/monitoring/gitlab_instance_administration_project/index.md
+++ b/doc/administration/monitoring/gitlab_instance_administration_project/index.md
@@ -27,7 +27,7 @@ If that's not the case or if you have an external Prometheus instance or an HA s
you should
[configure it manually](../../../user/project/integrations/prometheus.md#manual-configuration-of-prometheus).
-## Taking action on Prometheus alerts **[ULTIMATE]**
+## Taking action on Prometheus alerts **(ULTIMATE)**
You can [add a webhook](../../../user/project/integrations/prometheus.md#external-prometheus-instances)
to the Prometheus config in order for GitLab to receive notifications of any alerts.
diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md
index 9f671e0db11..c32edb60f9d 100644
--- a/doc/administration/monitoring/performance/request_profiling.md
+++ b/doc/administration/monitoring/performance/request_profiling.md
@@ -5,7 +5,7 @@
1. Grab the profiling token from **Monitoring > Requests Profiles** admin page
(highlighted in a blue in the image below).
![Profile token](img/request_profiling_token.png)
-1. Pass the header `X-Profile-Token: <token>` and `X-Profile-Mode: <mode>`(where <mode> can be `execution` or `memory`) to the request you want to profile. You can use:
+1. Pass the header `X-Profile-Token: <token>` and `X-Profile-Mode: <mode>`(where `<mode>` can be `execution` or `memory`) to the request you want to profile. You can use:
- Browser extensions. For example, [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension.
- `curl`. For example, `curl --header 'X-Profile-Token: <token>' --header 'X-Profile-Mode: <mode>' https://gitlab.example.com/group/project`.
1. Once request is finished (which will take a little longer than usual), you can
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
index 543a222bd25..c5939dc6856 100644
--- a/doc/install/azure/index.md
+++ b/doc/install/azure/index.md
@@ -407,7 +407,7 @@ on any cloud service you choose.
## Where to next?
-Check out our other [Technical Articles](../../articles/index.md) or browse the [GitLab Documentation][GitLab-Docs](../../README.md) to learn more about GitLab.
+Check out our other [Technical Articles](../../articles/index.md) or browse the [GitLab Documentation](../../README.md) to learn more about GitLab.
### Useful links
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index e3f657af564..bd50367681e 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -39,6 +39,25 @@ However, users will not be prompted to log via SSO on each visit. GitLab will ch
We intend to add a similar SSO requirement for [Git and API activity](https://gitlab.com/gitlab-org/gitlab-ee/issues/9152) in the future.
+#### Group-managed accounts
+
+[Introduced in GitLab 12.1](https://gitlab.com/groups/gitlab-org/-/epics/709).
+
+When SSO is being enforced, groups can enable an additional level of protection by enforcing the creation of dedicated user accounts to access the group.
+
+Without group-managed accounts, users can link their SAML identity with any existing user on the instance. With group-managed accounts enabled, users are required to create a new, dedicated user linked to the group. The notification email address associated with the user is locked to the email address received from the configured identity provider.
+
+When this option is enabled:
+
+- All existing and new users in the group will be required to log in via the SSO URL associated with the group.
+- On successfully authenticating, GitLab will prompt the user to create a new, dedicated account using the email address received from the configured identity provider.
+- After the group managed account has been created, group activity will require the use of this user account.
+
+Since use of the group managed account requires the use of SSO, users of group managed accounts will lose access to these accounts when they are no longer able to authenticate with the connected identity provider. In the case of an offboarded employee who has been removed from your identity provider:
+
+- The user will be unable to access the group (their credentials will no longer work on the identity provider when prompted to SSO).
+- Contributions in the group (e.g. issues, merge requests) will remain intact.
+
### NameID
GitLab.com uses the SAML NameID to identify users. The NameID element:
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index e2b1a20a605..f7ba921aa7d 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -48,6 +48,7 @@ To enable 2FA:
- [andOTP](https://github.com/andOTP/andOTP): feature rich open source app for Android which supports PGP encrypted backups.
- [FreeOTP](https://freeotp.github.io/): open source app for Android.
- [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en): proprietary app for iOS and Android.
+ - [SailOTP](https://openrepos.net/content/seiichiro0185/sailotp): open source app for SailFish OS.
1. In the application, add a new entry in one of two ways:
- Scan the code presented in GitLab with your device's camera to add the
entry automatically.
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index df8a625319b..d52aeb1fe6b 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -93,3 +93,9 @@ Object.assign(global, {
clearTimeout(id);
},
});
+
+// make sure that each test actually tests something
+// see https://jestjs.io/docs/en/expect#expecthasassertions
+beforeEach(() => {
+ expect.hasAssertions();
+});
diff --git a/spec/javascripts/monitoring/charts/time_series_spec.js b/spec/javascripts/monitoring/charts/time_series_spec.js
new file mode 100644
index 00000000000..d145a64e8d0
--- /dev/null
+++ b/spec/javascripts/monitoring/charts/time_series_spec.js
@@ -0,0 +1,335 @@
+import { shallowMount } from '@vue/test-utils';
+import { createStore } from '~/monitoring/stores';
+import { GlLink } from '@gitlab/ui';
+import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
+import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
+import TimeSeries from '~/monitoring/components/charts/time_series.vue';
+import * as types from '~/monitoring/stores/mutation_types';
+import { TEST_HOST } from 'spec/test_constants';
+import MonitoringMock, { deploymentData, mockProjectPath } from '../mock_data';
+
+describe('Time series component', () => {
+ const mockSha = 'mockSha';
+ const mockWidgets = 'mockWidgets';
+ const mockSvgPathContent = 'mockSvgPathContent';
+ const projectPath = `${TEST_HOST}${mockProjectPath}`;
+ const commitUrl = `${projectPath}/commit/${mockSha}`;
+ let mockGraphData;
+ let makeTimeSeriesChart;
+ let spriteSpy;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
+ store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
+ store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true });
+ [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics;
+
+ makeTimeSeriesChart = (graphData, type) =>
+ shallowMount(TimeSeries, {
+ propsData: {
+ graphData: { ...graphData, type },
+ containerWidth: 0,
+ deploymentData: store.state.monitoringDashboard.deploymentData,
+ projectPath,
+ },
+ slots: {
+ default: mockWidgets,
+ },
+ sync: false,
+ store,
+ });
+
+ spriteSpy = spyOnDependency(TimeSeries, 'getSvgIconPathContent').and.callFake(
+ () => new Promise(resolve => resolve(mockSvgPathContent)),
+ );
+ });
+
+ describe('general functions', () => {
+ let timeSeriesChart;
+
+ beforeEach(() => {
+ timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
+ });
+
+ it('renders chart title', () => {
+ expect(timeSeriesChart.find('.js-graph-title').text()).toBe(mockGraphData.title);
+ });
+
+ it('contains graph widgets from slot', () => {
+ expect(timeSeriesChart.find('.js-graph-widgets').text()).toBe(mockWidgets);
+ });
+
+ describe('when exportMetricsToCsvEnabled is disabled', () => {
+ beforeEach(() => {
+ store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false });
+ });
+
+ it('does not render the Download CSV button', done => {
+ timeSeriesChart.vm.$nextTick(() => {
+ expect(timeSeriesChart.contains('glbutton-stub')).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('formatTooltipText', () => {
+ const mockDate = deploymentData[0].created_at;
+ const mockCommitUrl = deploymentData[0].commitUrl;
+ const generateSeriesData = type => ({
+ seriesData: [
+ {
+ seriesName: timeSeriesChart.vm.chartData[0].name,
+ componentSubType: type,
+ value: [mockDate, 5.55555],
+ seriesIndex: 0,
+ },
+ ],
+ value: mockDate,
+ });
+
+ describe('when series is of line type', () => {
+ beforeEach(done => {
+ timeSeriesChart.vm.formatTooltipText(generateSeriesData('line'));
+ timeSeriesChart.vm.$nextTick(done);
+ });
+
+ it('formats tooltip title', () => {
+ expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
+ });
+
+ it('formats tooltip content', () => {
+ const name = 'Core Usage';
+ const value = '5.556';
+ const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
+
+ expect(seriesLabel.vm.color).toBe('');
+ expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
+ expect(timeSeriesChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]);
+ expect(
+ shallowWrapperContainsSlotText(
+ timeSeriesChart.find(GlAreaChart),
+ 'tooltipContent',
+ value,
+ ),
+ ).toBe(true);
+ });
+ });
+
+ describe('when series is of scatter type', () => {
+ beforeEach(() => {
+ timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
+ });
+
+ it('formats tooltip title', () => {
+ expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
+ });
+
+ it('formats tooltip sha', () => {
+ expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9');
+ });
+
+ it('formats tooltip commit url', () => {
+ expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl);
+ });
+ });
+ });
+
+ describe('setSvg', () => {
+ const mockSvgName = 'mockSvgName';
+
+ beforeEach(done => {
+ timeSeriesChart.vm.setSvg(mockSvgName);
+ timeSeriesChart.vm.$nextTick(done);
+ });
+
+ it('gets svg path content', () => {
+ expect(spriteSpy).toHaveBeenCalledWith(mockSvgName);
+ });
+
+ it('sets svg path content', () => {
+ timeSeriesChart.vm.$nextTick(() => {
+ expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`);
+ });
+ });
+ });
+
+ describe('onResize', () => {
+ const mockWidth = 233;
+
+ beforeEach(() => {
+ spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({
+ width: mockWidth,
+ }));
+ timeSeriesChart.vm.onResize();
+ });
+
+ it('sets area chart width', () => {
+ expect(timeSeriesChart.vm.width).toBe(mockWidth);
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe('chartData', () => {
+ let chartData;
+ const seriesData = () => chartData[0];
+
+ beforeEach(() => {
+ ({ chartData } = timeSeriesChart.vm);
+ });
+
+ it('utilizes all data points', () => {
+ const { values } = mockGraphData.queries[0].result[0];
+
+ expect(chartData.length).toBe(1);
+ expect(seriesData().data.length).toBe(values.length);
+ });
+
+ it('creates valid data', () => {
+ const { data } = seriesData();
+
+ expect(
+ data.filter(
+ ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number',
+ ).length,
+ ).toBe(data.length);
+ });
+
+ it('formats line width correctly', () => {
+ expect(chartData[0].lineStyle.width).toBe(2);
+ });
+ });
+
+ describe('chartOptions', () => {
+ describe('yAxis formatter', () => {
+ let format;
+
+ beforeEach(() => {
+ format = timeSeriesChart.vm.chartOptions.yAxis.axisLabel.formatter;
+ });
+
+ it('rounds to 3 decimal places', () => {
+ expect(format(0.88888)).toBe('0.889');
+ });
+ });
+ });
+
+ describe('scatterSeries', () => {
+ it('utilizes deployment data', () => {
+ expect(timeSeriesChart.vm.scatterSeries.data).toEqual([
+ ['2017-05-31T21:23:37.881Z', 0],
+ ['2017-05-30T20:08:04.629Z', 0],
+ ['2017-05-30T17:42:38.409Z', 0],
+ ]);
+
+ expect(timeSeriesChart.vm.scatterSeries.symbolSize).toBe(14);
+ });
+ });
+
+ describe('yAxisLabel', () => {
+ it('constructs a label for the chart y-axis', () => {
+ expect(timeSeriesChart.vm.yAxisLabel).toBe('CPU');
+ });
+ });
+
+ describe('csvText', () => {
+ it('converts data from json to csv', () => {
+ const header = `timestamp,${mockGraphData.y_label}`;
+ const data = mockGraphData.queries[0].result[0].values;
+ const firstRow = `${data[0][0]},${data[0][1]}`;
+
+ expect(timeSeriesChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`);
+ });
+ });
+
+ describe('downloadLink', () => {
+ it('produces a link to download metrics as csv', () => {
+ const link = timeSeriesChart.vm.downloadLink;
+
+ expect(link).toContain('blob:');
+ });
+ });
+ });
+
+ afterEach(() => {
+ timeSeriesChart.destroy();
+ });
+ });
+
+ describe('wrapped components', () => {
+ const glChartComponents = [
+ {
+ chartType: 'area-chart',
+ component: GlAreaChart,
+ },
+ {
+ chartType: 'line-chart',
+ component: GlLineChart,
+ },
+ ];
+
+ glChartComponents.forEach(dynamicComponent => {
+ describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
+ let timeSeriesAreaChart;
+ let glChart;
+
+ beforeEach(done => {
+ timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
+ glChart = timeSeriesAreaChart.find(dynamicComponent.component);
+ timeSeriesAreaChart.vm.$nextTick(done);
+ });
+
+ it('is a Vue instance', () => {
+ expect(glChart.exists()).toBe(true);
+ expect(glChart.isVueInstance()).toBe(true);
+ });
+
+ it('receives data properties needed for proper chart render', () => {
+ const props = glChart.props();
+
+ expect(props.data).toBe(timeSeriesAreaChart.vm.chartData);
+ expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions);
+ expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText);
+ expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds);
+ });
+
+ it('recieves a tooltip title', done => {
+ const mockTitle = 'mockTitle';
+ timeSeriesAreaChart.vm.tooltip.title = mockTitle;
+
+ timeSeriesAreaChart.vm.$nextTick(() => {
+ expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', mockTitle)).toBe(true);
+ done();
+ });
+ });
+
+ describe('when tooltip is showing deployment data', () => {
+ beforeEach(done => {
+ timeSeriesAreaChart.vm.tooltip.isDeployment = true;
+ timeSeriesAreaChart.vm.$nextTick(done);
+ });
+
+ it('uses deployment title', () => {
+ expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', 'Deployed')).toBe(true);
+ });
+
+ it('renders clickable commit sha in tooltip content', done => {
+ timeSeriesAreaChart.vm.tooltip.sha = mockSha;
+ timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl;
+
+ timeSeriesAreaChart.vm.$nextTick(() => {
+ const commitLink = timeSeriesAreaChart.find(GlLink);
+
+ expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
+ expect(commitLink.attributes('href')).toEqual(commitUrl);
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 85e660d3925..17e7314e214 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -1,5 +1,7 @@
export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`;
+export const mockProjectPath = '/frontend-fixtures/environments-project';
+
export const metricsGroupsAPIResponse = {
success: true,
data: [
@@ -902,7 +904,7 @@ export const metricsDashboardResponse = {
},
{
title: 'Memory Usage (Pod average)',
- type: 'area-chart',
+ type: 'line-chart',
y_label: 'Memory Used per Pod',
weight: 2,
metrics: [
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
index 212519743aa..7216ad00cc1 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
@@ -83,6 +83,24 @@ describe('Merge request widget rebase component', () => {
expect(text).toContain('foo');
expect(text.replace(/\s\s+/g, ' ')).toContain('to allow this merge request to be merged.');
});
+
+ it('should render the correct target branch name', () => {
+ const targetBranch = 'fake-branch-to-test-with';
+ vm = mountComponent(Component, {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch,
+ },
+ service: {},
+ });
+
+ const elem = vm.$el.querySelector('.rebase-state-find-class-convention span');
+
+ expect(elem.innerHTML).toContain(
+ `Fast-forward merge is not possible. Rebase the source branch onto <span class="label-branch">${targetBranch}</span> to allow this merge request to be merged.`,
+ );
+ });
});
describe('methods', () => {