summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-08 21:09:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-08 21:09:50 +0000
commit76358aee81a471a5e71eaf3e8c2d91b7c9a0a5a9 (patch)
treedf9ba3dcc09eb404de31e0d79cb8f0b77812e655 /app/assets
parent80e9fdc9682cfbcfb9202a2733605a6a6bd23f05 (diff)
downloadgitlab-ce-76358aee81a471a5e71eaf3e8c2d91b7c9a0a5a9.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue96
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue294
-rw-r--r--app/assets/javascripts/custom_metrics/components/delete_custom_metric_modal.vue54
-rw-r--r--app/assets/javascripts/custom_metrics/constants.js12
-rw-r--r--app/assets/javascripts/custom_metrics/index.js47
-rw-r--r--app/assets/javascripts/dropzone_input.js3
-rw-r--r--app/assets/javascripts/gl_form.js2
-rw-r--r--app/assets/javascripts/monitoring/constants.js6
-rw-r--r--app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql4
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js3
-rw-r--r--app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue62
-rw-r--r--app/assets/javascripts/reports/components/issue_body.js3
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss10
-rw-r--r--app/assets/stylesheets/framework/typography.scss10
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss2
15 files changed, 583 insertions, 25 deletions
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
new file mode 100644
index 00000000000..e5c0d1e4970
--- /dev/null
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
@@ -0,0 +1,96 @@
+<script>
+import { GlDeprecatedButton } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+import CustomMetricsFormFields from './custom_metrics_form_fields.vue';
+import DeleteCustomMetricModal from './delete_custom_metric_modal.vue';
+import { formDataValidator } from '../constants';
+
+export default {
+ components: {
+ CustomMetricsFormFields,
+ DeleteCustomMetricModal,
+ GlDeprecatedButton,
+ },
+ props: {
+ customMetricsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ metricPersisted: {
+ type: Boolean,
+ required: true,
+ },
+ editProjectServicePath: {
+ type: String,
+ required: true,
+ },
+ validateQueryPath: {
+ type: String,
+ required: true,
+ },
+ formData: {
+ type: Object,
+ required: true,
+ validator: formDataValidator,
+ },
+ },
+ data() {
+ return {
+ formIsValid: null,
+ errorMessage: '',
+ };
+ },
+ computed: {
+ saveButtonText() {
+ return this.metricPersisted ? __('Save Changes') : s__('Metrics|Create metric');
+ },
+ titleText() {
+ return this.metricPersisted ? s__('Metrics|Edit metric') : s__('Metrics|New metric');
+ },
+ },
+ created() {
+ this.csrf = csrf.token != null ? csrf.token : '';
+ this.formOperation = this.metricPersisted ? 'patch' : 'post';
+ },
+ methods: {
+ formValidation(isValid) {
+ this.formIsValid = isValid;
+ },
+ submit() {
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+<template>
+ <div class="row my-3">
+ <h4 class="prepend-top-0 col-lg-8 offset-lg-2">{{ titleText }}</h4>
+ <form ref="form" class="col-lg-8 offset-lg-2" :action="customMetricsPath" method="post">
+ <custom-metrics-form-fields
+ :form-operation="formOperation"
+ :form-data="formData"
+ :metric-persisted="metricPersisted"
+ :validate-query-path="validateQueryPath"
+ @formValidation="formValidation"
+ />
+ <div class="form-actions">
+ <gl-deprecated-button variant="success" :disabled="!formIsValid" @click="submit">
+ {{ saveButtonText }}
+ </gl-deprecated-button>
+ <gl-deprecated-button
+ variant="secondary"
+ class="float-right"
+ :href="editProjectServicePath"
+ >{{ __('Cancel') }}</gl-deprecated-button
+ >
+ <delete-custom-metric-modal
+ v-if="metricPersisted"
+ :delete-metric-url="customMetricsPath"
+ :csrf-token="csrf"
+ />
+ </div>
+ </form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
new file mode 100644
index 00000000000..f5207b47f69
--- /dev/null
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
@@ -0,0 +1,294 @@
+<script>
+import { GlFormInput, GlLink, GlFormGroup, GlFormRadioGroup, GlLoadingIcon } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { __, s__ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import csrf from '~/lib/utils/csrf';
+import axios from '~/lib/utils/axios_utils';
+import statusCodes from '~/lib/utils/http_status';
+import { backOff } from '~/lib/utils/common_utils';
+import { queryTypes, formDataValidator } from '../constants';
+
+const VALIDATION_REQUEST_TIMEOUT = 10000;
+const axiosCancelToken = axios.CancelToken;
+let cancelTokenSource;
+
+function backOffRequest(makeRequestCallback) {
+ return backOff((next, stop) => {
+ makeRequestCallback()
+ .then(resp => {
+ if (resp.status === statusCodes.OK) {
+ stop(resp);
+ } else {
+ next();
+ }
+ })
+ // If the request is cancelled by axios
+ // then consider it as noop so that its not
+ // caught by subsequent catches
+ .catch(thrown => (axios.isCancel(thrown) ? undefined : stop(thrown)));
+ }, VALIDATION_REQUEST_TIMEOUT);
+}
+
+export default {
+ components: {
+ GlFormInput,
+ GlLink,
+ GlFormGroup,
+ GlFormRadioGroup,
+ GlLoadingIcon,
+ Icon,
+ },
+ props: {
+ formOperation: {
+ type: String,
+ required: true,
+ },
+ formData: {
+ type: Object,
+ required: false,
+ default: () => ({
+ title: '',
+ yLabel: '',
+ query: '',
+ unit: '',
+ group: '',
+ legend: '',
+ }),
+ validator: formDataValidator,
+ },
+ metricPersisted: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ validateQueryPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ const group = this.formData.group.length ? this.formData.group : queryTypes.business;
+
+ return {
+ queryIsValid: null,
+ queryValidateInFlight: false,
+ ...this.formData,
+ group,
+ };
+ },
+ computed: {
+ formIsValid() {
+ return Boolean(
+ this.queryIsValid &&
+ this.title.length &&
+ this.yLabel.length &&
+ this.unit.length &&
+ this.group.length,
+ );
+ },
+ validQueryMsg() {
+ return this.queryIsValid ? s__('Metrics|PromQL query is valid') : '';
+ },
+ invalidQueryMsg() {
+ return !this.queryIsValid ? this.errorMessage : '';
+ },
+ },
+ watch: {
+ formIsValid(value) {
+ this.$emit('formValidation', value);
+ },
+ },
+ beforeMount() {
+ if (this.metricPersisted) {
+ this.validateQuery();
+ }
+ },
+ methods: {
+ requestValidation(query, cancelToken) {
+ return backOffRequest(() =>
+ axios.post(
+ this.validateQueryPath,
+ {
+ query,
+ },
+ {
+ cancelToken,
+ },
+ ),
+ );
+ },
+ setFormState(isValid, inFlight, message) {
+ this.queryIsValid = isValid;
+ this.queryValidateInFlight = inFlight;
+ this.errorMessage = message;
+ },
+ validateQuery() {
+ if (!this.query) {
+ this.setFormState(null, false, '');
+ return;
+ }
+ this.setFormState(null, true, '');
+ // cancel previously dispatched backoff request
+ if (cancelTokenSource) {
+ cancelTokenSource.cancel();
+ }
+ // Creating a new token for each request because
+ // if a single token is used it can cancel existing requests
+ // as well.
+ cancelTokenSource = axiosCancelToken.source();
+ this.requestValidation(this.query, cancelTokenSource.token)
+ .then(res => {
+ const response = res.data;
+ const { valid, error } = response.query;
+ if (response.success) {
+ this.setFormState(valid, false, valid ? '' : error);
+ } else {
+ throw new Error(__('There was an error trying to validate your query'));
+ }
+ })
+ .catch(() => {
+ this.setFormState(
+ false,
+ false,
+ s__('Metrics|There was an error trying to validate your query'),
+ );
+ });
+ },
+ debouncedValidateQuery: debounce(function checkQuery() {
+ this.validateQuery();
+ }, 500),
+ },
+ csrfToken: csrf.token || '',
+ formGroupOptions: [
+ { text: __('Business'), value: queryTypes.business },
+ { text: __('Response'), value: queryTypes.response },
+ { text: __('System'), value: queryTypes.system },
+ ],
+};
+</script>
+
+<template>
+ <div>
+ <input ref="method" type="hidden" name="_method" :value="formOperation" />
+ <input :value="$options.csrfToken" type="hidden" name="authenticity_token" />
+ <gl-form-group :label="__('Name')" label-for="prometheus_metric_title" label-class="label-bold">
+ <gl-form-input
+ id="prometheus_metric_title"
+ v-model="title"
+ name="prometheus_metric[title]"
+ class="form-control"
+ :placeholder="s__('Metrics|e.g. Throughput')"
+ data-qa-selector="custom_metric_prometheus_title_field"
+ required
+ />
+ <span class="form-text text-muted">{{ s__('Metrics|Used as a title for the chart') }}</span>
+ </gl-form-group>
+ <gl-form-group :label="__('Type')" label-for="prometheus_metric_group" label-class="label-bold">
+ <gl-form-radio-group
+ id="metric-group"
+ v-model="group"
+ :options="$options.formGroupOptions"
+ :checked="group"
+ name="prometheus_metric[group]"
+ />
+ <span class="form-text text-muted">{{ s__('Metrics|For grouping similar metrics') }}</span>
+ </gl-form-group>
+ <gl-form-group
+ :label="__('Query')"
+ label-for="prometheus_metric_query"
+ label-class="label-bold"
+ :state="queryIsValid"
+ >
+ <gl-form-input
+ id="prometheus_metric_query"
+ v-model.trim="query"
+ data-qa-selector="custom_metric_prometheus_query_field"
+ name="prometheus_metric[query]"
+ class="form-control"
+ :placeholder="s__('Metrics|e.g. rate(http_requests_total[5m])')"
+ required
+ :state="queryIsValid"
+ @input="debouncedValidateQuery"
+ />
+ <span v-if="queryValidateInFlight" class="form-text text-muted">
+ <gl-loading-icon :inline="true" class="mr-1 align-middle" />
+ {{ s__('Metrics|Validating query') }}
+ </span>
+ <slot v-if="!queryValidateInFlight" name="valid-feedback">
+ <span class="form-text cgreen">
+ {{ validQueryMsg }}
+ </span>
+ </slot>
+ <slot v-if="!queryValidateInFlight" name="invalid-feedback">
+ <span class="form-text cred">
+ {{ invalidQueryMsg }}
+ </span>
+ </slot>
+ <span v-show="query.length === 0" class="form-text text-muted">
+ {{ s__('Metrics|Must be a valid PromQL query.') }}
+ <gl-link href="https://prometheus.io/docs/prometheus/latest/querying/basics/" tabindex="-1">
+ {{ s__('Metrics|Prometheus Query Documentation') }}
+ <icon name="external-link" :size="12" />
+ </gl-link>
+ </span>
+ </gl-form-group>
+ <gl-form-group
+ :label="s__('Metrics|Y-axis label')"
+ label-for="prometheus_metric_y_label"
+ label-class="label-bold"
+ >
+ <gl-form-input
+ id="prometheus_metric_y_label"
+ v-model="yLabel"
+ data-qa-selector="custom_metric_prometheus_y_label_field"
+ name="prometheus_metric[y_label]"
+ class="form-control"
+ :placeholder="s__('Metrics|e.g. Requests/second')"
+ required
+ />
+ <span class="form-text text-muted">
+ {{
+ s__('Metrics|Label of the y-axis (usually the unit). The x-axis always represents time.')
+ }}
+ </span>
+ </gl-form-group>
+ <gl-form-group
+ :label="s__('Metrics|Unit label')"
+ label-for="prometheus_metric_unit"
+ label-class="label-bold"
+ >
+ <gl-form-input
+ id="prometheus_metric_unit"
+ v-model="unit"
+ data-qa-selector="custom_metric_prometheus_unit_label_field"
+ name="prometheus_metric[unit]"
+ class="form-control"
+ :placeholder="s__('Metrics|e.g. req/sec')"
+ required
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="s__('Metrics|Legend label (optional)')"
+ label-for="prometheus_metric_legend"
+ label-class="label-bold"
+ >
+ <gl-form-input
+ id="prometheus_metric_legend"
+ v-model="legend"
+ data-qa-selector="custom_metric_prometheus_legend_label_field"
+ name="prometheus_metric[legend]"
+ class="form-control"
+ :placeholder="s__('Metrics|e.g. HTTP requests')"
+ required
+ />
+ <span class="form-text text-muted">
+ {{
+ s__(
+ 'Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response.',
+ )
+ }}
+ </span>
+ </gl-form-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/custom_metrics/components/delete_custom_metric_modal.vue b/app/assets/javascripts/custom_metrics/components/delete_custom_metric_modal.vue
new file mode 100644
index 00000000000..34e4aeb290f
--- /dev/null
+++ b/app/assets/javascripts/custom_metrics/components/delete_custom_metric_modal.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlModal, GlModalDirective, GlDeprecatedButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlModal,
+ GlDeprecatedButton,
+ },
+ directives: {
+ 'gl-modal': GlModalDirective,
+ },
+ props: {
+ deleteMetricUrl: {
+ type: String,
+ required: true,
+ },
+ csrfToken: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$refs.form.submit();
+ },
+ },
+ descriptionText: s__(
+ `Metrics|You're about to permanently delete this metric. This cannot be undone.`,
+ ),
+ modalId: 'delete-custom-metric-modal',
+};
+</script>
+<template>
+ <div class="d-inline-block float-right mr-3">
+ <gl-deprecated-button v-gl-modal="$options.modalId" variant="danger">
+ {{ __('Delete') }}
+ </gl-deprecated-button>
+ <gl-modal
+ :title="s__('Metrics|Delete metric?')"
+ :ok-title="s__('Metrics|Delete metric')"
+ :modal-id="$options.modalId"
+ ok-variant="danger"
+ @ok="onSubmit"
+ >
+ {{ $options.descriptionText }}
+
+ <form ref="form" :action="deleteMetricUrl" method="post">
+ <input type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ </form>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/custom_metrics/constants.js b/app/assets/javascripts/custom_metrics/constants.js
new file mode 100644
index 00000000000..2526445fdf9
--- /dev/null
+++ b/app/assets/javascripts/custom_metrics/constants.js
@@ -0,0 +1,12 @@
+export const queryTypes = {
+ business: 'business',
+ response: 'response',
+ system: 'system',
+};
+
+export const formDataValidator = val => {
+ const fieldNames = Object.keys(val);
+ const requiredFields = ['title', 'query', 'yLabel', 'unit', 'group', 'legend'];
+
+ return requiredFields.every(name => fieldNames.includes(name));
+};
diff --git a/app/assets/javascripts/custom_metrics/index.js b/app/assets/javascripts/custom_metrics/index.js
new file mode 100644
index 00000000000..4c279daf5f0
--- /dev/null
+++ b/app/assets/javascripts/custom_metrics/index.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import CustomMetricsForm from './components/custom_metrics_form.vue';
+
+export default () => {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: '#js-custom-metrics',
+ components: {
+ CustomMetricsForm,
+ },
+ render(createElement) {
+ const domEl = document.querySelector(this.$options.el);
+ const {
+ customMetricsPath,
+ editProjectServicePath,
+ validateQueryPath,
+ title,
+ query,
+ yLabel,
+ unit,
+ group,
+ legend,
+ } = domEl.dataset;
+ let { metricPersisted } = domEl.dataset;
+
+ metricPersisted = parseBoolean(metricPersisted);
+
+ return createElement('custom-metrics-form', {
+ props: {
+ customMetricsPath,
+ metricPersisted,
+ editProjectServicePath,
+ validateQueryPath,
+ formData: {
+ title,
+ query,
+ yLabel,
+ unit,
+ group,
+ legend,
+ },
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 6c158ad8990..f839e9acf04 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -23,7 +23,7 @@ function getErrorMessage(res) {
return res.message;
}
-export default function dropzoneInput(form) {
+export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
const divHover = '<div class="div-dropzone-hover"></div>';
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
const $attachButton = form.find('.button-attach-file');
@@ -69,6 +69,7 @@ export default function dropzoneInput(form) {
uploadMultiple: false,
headers: csrf.headers,
previewContainer: false,
+ ...config,
processing: () => $('.div-dropzone-alert').alert('close'),
dragover: () => {
$mdArea.addClass('is-dropzone-hover');
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index a66555838ba..1811a942beb 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -45,7 +45,7 @@ export default class GLForm {
);
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
- dropzoneInput(this.form);
+ dropzoneInput(this.form, { parallelUploads: 1 });
autosize(this.textarea);
}
// form and textarea event listeners
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index b2911bcae8f..e092c0ccae0 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -109,3 +109,9 @@ export const initialStateKeys = [...endpointKeys, 'currentEnvironmentName'];
* Constant to indicate if a metric exists in the database
*/
export const NOT_IN_DB_PREFIX = 'NO_DB';
+
+/**
+ * graphQL environments API value for active environments.
+ * Used as a value for the 'states' query filter
+ */
+export const ENVIRONMENT_AVAILABLE_STATE = 'available';
diff --git a/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql b/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql
index fd3a4348509..17cd1b2c342 100644
--- a/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql
+++ b/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql
@@ -1,6 +1,6 @@
-query getEnvironments($projectPath: ID!, $search: String) {
+query getEnvironments($projectPath: ID!, $search: String, $states: [String!]) {
project(fullPath: $projectPath) {
- data: environments(search: $search) {
+ data: environments(search: $search, states: $states) {
environments: nodes {
name
id
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index acc09fa6305..8427a72a68e 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -10,7 +10,7 @@ import statusCodes from '../../lib/utils/http_status';
import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
-import { PROMETHEUS_TIMEOUT } from '../constants';
+import { PROMETHEUS_TIMEOUT, ENVIRONMENT_AVAILABLE_STATE } from '../constants';
function prometheusMetricQueryParams(timeRange) {
const { start, end } = convertToFixedRange(timeRange);
@@ -238,6 +238,7 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
variables: {
projectPath: removeLeadingSlash(state.projectPath),
search: state.environmentsSearchTerm,
+ states: [ENVIRONMENT_AVAILABLE_STATE],
},
})
.then(resp =>
diff --git a/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue b/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue
new file mode 100644
index 00000000000..6aae9195be1
--- /dev/null
+++ b/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+
+export default {
+ name: 'AccessibilityIssueBody',
+ components: {
+ GlLink,
+ },
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ isNew: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ parsedTECHSCode() {
+ /*
+ * In issue code looks like "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
+ * or "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent"
+ *
+ * The TECHS code is the "G18", "G168", "H91", etc. from the code which is used for the documentation.
+ * Here we simply split the string on `.` and get the code in the 5th position
+ */
+ if (this.issue.code === undefined) {
+ return null;
+ }
+
+ return this.issue.code.split('.')[4] || null;
+ },
+ learnMoreUrl() {
+ if (this.parsedTECHSCode === null) {
+ return 'https://www.w3.org/TR/WCAG20-TECHS/Overview.html';
+ }
+
+ return `https://www.w3.org/TR/WCAG20-TECHS/${this.parsedTECHSCode}.html`;
+ },
+ },
+};
+</script>
+<template>
+ <div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
+ <div ref="accessibility-issue-description" class="report-block-list-issue-description-text">
+ <div
+ v-if="isNew"
+ ref="accessibility-issue-is-new-badge"
+ class="badge badge-danger append-right-5"
+ >
+ {{ s__('AccessibilityReport|New') }}
+ </div>
+ {{ issue.name }}
+ <gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{
+ s__('AccessibilityReport|Learn More')
+ }}</gl-link>
+ {{ sprintf(s__('AccessibilityReport|Message: %{message}'), { message: issue.message }) }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js
index 8b5af263d50..e106e60951b 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/reports/components/issue_body.js
@@ -1,9 +1,12 @@
import TestIssueBody from './test_issue_body.vue';
+import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
export const components = {
+ AccessibilityIssueBody,
TestIssueBody,
};
export const componentNames = {
+ AccessibilityIssueBody: AccessibilityIssueBody.name,
TestIssueBody: TestIssueBody.name,
};
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 875de57cfcc..ed5c133950d 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -42,16 +42,6 @@ html [type="button"],
}
h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- color: $gl-text-color;
- font-weight: 600;
-}
-
-h1,
.h1,
h2,
.h2,
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index b2bbc09664c..69aed2fc20a 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -567,16 +567,6 @@ body {
font-weight: $gl-font-weight-bold;
}
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- color: $gl-text-color;
- font-weight: $gl-font-weight-bold;
-}
-
.light-header {
font-weight: $gl-font-weight-bold;
}
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index 7538459c97b..ef75dabbda4 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -35,6 +35,8 @@ $h3-font-size: 14px * 1.75;
$h4-font-size: 14px * 1.5;
$h5-font-size: 14px * 1.25;
$h6-font-size: 14px;
+$headings-color: $gl-text-color;
+$headings-font-weight: $gl-font-weight-bold;
$spacer: $grid-size;
$spacers: (
0: 0,