Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ae30db7b18
commit
6b2c5f542b
14 changed files with 370 additions and 36 deletions
62
app/assets/javascripts/api/alert_management_alerts_api.js
Normal file
62
app/assets/javascripts/api/alert_management_alerts_api.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
import { buildApiUrl } from '~/api/api_utils';
|
||||||
|
import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
|
||||||
|
|
||||||
|
const ALERT_METRIC_IMAGES_PATH =
|
||||||
|
'/api/:version/projects/:id/alert_management_alerts/:alert_iid/metric_images';
|
||||||
|
const ALERT_SINGLE_METRIC_IMAGE_PATH =
|
||||||
|
'/api/:version/projects/:id/alert_management_alerts/:alert_iid/metric_images/:image_id';
|
||||||
|
|
||||||
|
export function fetchAlertMetricImages({ alertIid, id }) {
|
||||||
|
const metricImagesUrl = buildApiUrl(ALERT_METRIC_IMAGES_PATH)
|
||||||
|
.replace(':id', encodeURIComponent(id))
|
||||||
|
.replace(':alert_iid', encodeURIComponent(alertIid));
|
||||||
|
|
||||||
|
return axios.get(metricImagesUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadAlertMetricImage({ alertIid, id, file, url = null, urlText = null }) {
|
||||||
|
const options = { headers: { ...ContentTypeMultipartFormData } };
|
||||||
|
const metricImagesUrl = buildApiUrl(ALERT_METRIC_IMAGES_PATH)
|
||||||
|
.replace(':id', encodeURIComponent(id))
|
||||||
|
.replace(':alert_iid', encodeURIComponent(alertIid));
|
||||||
|
|
||||||
|
// Construct multipart form data
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
if (url) {
|
||||||
|
formData.append('url', url);
|
||||||
|
}
|
||||||
|
if (urlText) {
|
||||||
|
formData.append('url_text', urlText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.post(metricImagesUrl, formData, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAlertMetricImage({ alertIid, id, imageId, url = null, urlText = null }) {
|
||||||
|
const metricImagesUrl = buildApiUrl(ALERT_SINGLE_METRIC_IMAGE_PATH)
|
||||||
|
.replace(':id', encodeURIComponent(id))
|
||||||
|
.replace(':alert_iid', encodeURIComponent(alertIid))
|
||||||
|
.replace(':image_id', encodeURIComponent(imageId));
|
||||||
|
|
||||||
|
// Construct multipart form data
|
||||||
|
const formData = new FormData();
|
||||||
|
if (url != null) {
|
||||||
|
formData.append('url', url);
|
||||||
|
}
|
||||||
|
if (urlText != null) {
|
||||||
|
formData.append('url_text', urlText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios.put(metricImagesUrl, formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAlertMetricImage({ alertIid, id, imageId }) {
|
||||||
|
const individualMetricImageUrl = buildApiUrl(ALERT_SINGLE_METRIC_IMAGE_PATH)
|
||||||
|
.replace(':id', encodeURIComponent(id))
|
||||||
|
.replace(':alert_iid', encodeURIComponent(alertIid))
|
||||||
|
.replace(':image_id', encodeURIComponent(imageId));
|
||||||
|
|
||||||
|
return axios.delete(individualMetricImageUrl);
|
||||||
|
}
|
|
@ -2,6 +2,8 @@ import { DEFAULT_PER_PAGE } from '~/api';
|
||||||
import axios from '../lib/utils/axios_utils';
|
import axios from '../lib/utils/axios_utils';
|
||||||
import { buildApiUrl } from './api_utils';
|
import { buildApiUrl } from './api_utils';
|
||||||
|
|
||||||
|
export * from './alert_management_alerts_api';
|
||||||
|
|
||||||
const PROJECTS_PATH = '/api/:version/projects.json';
|
const PROJECTS_PATH = '/api/:version/projects.json';
|
||||||
const PROJECT_IMPORT_MEMBERS_PATH = '/api/:version/projects/:id/import_project_members/:project_id';
|
const PROJECT_IMPORT_MEMBERS_PATH = '/api/:version/projects/:id/import_project_members/:project_id';
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,12 @@ import Tracking from '~/tracking';
|
||||||
import initUserPopovers from '~/user_popovers';
|
import initUserPopovers from '~/user_popovers';
|
||||||
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
|
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
|
||||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
|
import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue';
|
||||||
import { PAGE_CONFIG, SEVERITY_LEVELS } from '../constants';
|
import { PAGE_CONFIG, SEVERITY_LEVELS } from '../constants';
|
||||||
import createIssueMutation from '../graphql/mutations/alert_issue_create.mutation.graphql';
|
import createIssueMutation from '../graphql/mutations/alert_issue_create.mutation.graphql';
|
||||||
import toggleSidebarStatusMutation from '../graphql/mutations/alert_sidebar_status.mutation.graphql';
|
import toggleSidebarStatusMutation from '../graphql/mutations/alert_sidebar_status.mutation.graphql';
|
||||||
import alertQuery from '../graphql/queries/alert_sidebar_details.query.graphql';
|
import alertQuery from '../graphql/queries/alert_sidebar_details.query.graphql';
|
||||||
import sidebarStatusQuery from '../graphql/queries/alert_sidebar_status.query.graphql';
|
import sidebarStatusQuery from '../graphql/queries/alert_sidebar_status.query.graphql';
|
||||||
import AlertMetrics from './alert_metrics.vue';
|
|
||||||
import AlertSidebar from './alert_sidebar.vue';
|
import AlertSidebar from './alert_sidebar.vue';
|
||||||
import AlertSummaryRow from './alert_summary_row.vue';
|
import AlertSummaryRow from './alert_summary_row.vue';
|
||||||
import SystemNote from './system_notes/system_note.vue';
|
import SystemNote from './system_notes/system_note.vue';
|
||||||
|
@ -74,7 +74,7 @@ export default {
|
||||||
TimeAgoTooltip,
|
TimeAgoTooltip,
|
||||||
AlertSidebar,
|
AlertSidebar,
|
||||||
SystemNote,
|
SystemNote,
|
||||||
AlertMetrics,
|
MetricImagesTab,
|
||||||
},
|
},
|
||||||
inject: {
|
inject: {
|
||||||
projectPath: {
|
projectPath: {
|
||||||
|
@ -372,13 +372,12 @@ export default {
|
||||||
</alert-summary-row>
|
</alert-summary-row>
|
||||||
<alert-details-table :alert="alert" :loading="loading" :statuses="statuses" />
|
<alert-details-table :alert="alert" :loading="loading" :statuses="statuses" />
|
||||||
</gl-tab>
|
</gl-tab>
|
||||||
<gl-tab
|
|
||||||
|
<metric-images-tab
|
||||||
v-if="!isThreatMonitoringPage"
|
v-if="!isThreatMonitoringPage"
|
||||||
:data-testid="$options.tabsConfig[1].id"
|
:data-testid="$options.tabsConfig[1].id"
|
||||||
:title="$options.tabsConfig[1].title"
|
:title="$options.tabsConfig[1].title"
|
||||||
>
|
/>
|
||||||
<alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
|
|
||||||
</gl-tab>
|
|
||||||
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
|
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
|
||||||
<div v-if="alert.notes.nodes.length > 0" class="issuable-discussion">
|
<div v-if="alert.notes.nodes.length > 0" class="issuable-discussion">
|
||||||
<ul class="notes main-notes-list timeline">
|
<ul class="notes main-notes-list timeline">
|
||||||
|
|
|
@ -3,6 +3,9 @@ import produce from 'immer';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import createDefaultClient from '~/lib/graphql';
|
import createDefaultClient from '~/lib/graphql';
|
||||||
|
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||||
|
import createStore from '~/vue_shared/components/metric_images/store';
|
||||||
|
import service from './service';
|
||||||
import AlertDetails from './components/alert_details.vue';
|
import AlertDetails from './components/alert_details.vue';
|
||||||
import { PAGE_CONFIG } from './constants';
|
import { PAGE_CONFIG } from './constants';
|
||||||
import sidebarStatusQuery from './graphql/queries/alert_sidebar_status.query.graphql';
|
import sidebarStatusQuery from './graphql/queries/alert_sidebar_status.query.graphql';
|
||||||
|
@ -12,7 +15,8 @@ Vue.use(VueApollo);
|
||||||
|
|
||||||
export default (selector) => {
|
export default (selector) => {
|
||||||
const domEl = document.querySelector(selector);
|
const domEl = document.querySelector(selector);
|
||||||
const { alertId, projectPath, projectIssuesPath, projectId, page } = domEl.dataset;
|
const { alertId, projectPath, projectIssuesPath, projectId, page, canUpdate } = domEl.dataset;
|
||||||
|
const iid = alertId;
|
||||||
const router = createRouter();
|
const router = createRouter();
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
|
@ -54,15 +58,20 @@ export default (selector) => {
|
||||||
page,
|
page,
|
||||||
projectIssuesPath,
|
projectIssuesPath,
|
||||||
projectId,
|
projectId,
|
||||||
|
iid,
|
||||||
statuses: PAGE_CONFIG[page].STATUSES,
|
statuses: PAGE_CONFIG[page].STATUSES,
|
||||||
|
canUpdate: parseBoolean(canUpdate),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const opsProperties = {};
|
||||||
|
|
||||||
if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
|
if (page === PAGE_CONFIG.OPERATIONS.TITLE) {
|
||||||
const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
|
const { TRACK_ALERTS_DETAILS_VIEWS_OPTIONS, TRACK_ALERT_STATUS_UPDATE_OPTIONS } = PAGE_CONFIG[
|
||||||
page
|
page
|
||||||
];
|
];
|
||||||
provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
|
provide.trackAlertsDetailsViewsOptions = TRACK_ALERTS_DETAILS_VIEWS_OPTIONS;
|
||||||
provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
|
provide.trackAlertStatusUpdateOptions = TRACK_ALERT_STATUS_UPDATE_OPTIONS;
|
||||||
|
opsProperties.store = createStore({}, service);
|
||||||
} else if (page === PAGE_CONFIG.THREAT_MONITORING.TITLE) {
|
} else if (page === PAGE_CONFIG.THREAT_MONITORING.TITLE) {
|
||||||
provide.isThreatMonitoringPage = true;
|
provide.isThreatMonitoringPage = true;
|
||||||
}
|
}
|
||||||
|
@ -74,6 +83,7 @@ export default (selector) => {
|
||||||
components: {
|
components: {
|
||||||
AlertDetails,
|
AlertDetails,
|
||||||
},
|
},
|
||||||
|
...opsProperties,
|
||||||
provide,
|
provide,
|
||||||
apolloProvider,
|
apolloProvider,
|
||||||
router,
|
router,
|
||||||
|
|
43
app/assets/javascripts/vue_shared/alert_details/service.js
Normal file
43
app/assets/javascripts/vue_shared/alert_details/service.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import {
|
||||||
|
fetchAlertMetricImages,
|
||||||
|
uploadAlertMetricImage,
|
||||||
|
updateAlertMetricImage,
|
||||||
|
deleteAlertMetricImage,
|
||||||
|
} from '~/rest_api';
|
||||||
|
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||||
|
|
||||||
|
function replaceModelIId(payload = {}) {
|
||||||
|
delete Object.assign(payload, { alertIid: payload.modelIid }).modelIid;
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMetricImages = async (payload) => {
|
||||||
|
const apiPayload = replaceModelIId(payload);
|
||||||
|
const response = await fetchAlertMetricImages(apiPayload);
|
||||||
|
return convertObjectPropsToCamelCase(response.data, { deep: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadMetricImage = async (payload) => {
|
||||||
|
const apiPayload = replaceModelIId(payload);
|
||||||
|
const response = await uploadAlertMetricImage(apiPayload);
|
||||||
|
return convertObjectPropsToCamelCase(response.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateMetricImage = async (payload) => {
|
||||||
|
const apiPayload = replaceModelIId(payload);
|
||||||
|
const response = await updateAlertMetricImage(apiPayload);
|
||||||
|
return convertObjectPropsToCamelCase(response.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteMetricImage = async (payload) => {
|
||||||
|
const apiPayload = replaceModelIId(payload);
|
||||||
|
const response = await deleteAlertMetricImage(apiPayload);
|
||||||
|
return convertObjectPropsToCamelCase(response.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getMetricImages,
|
||||||
|
uploadMetricImage,
|
||||||
|
updateMetricImage,
|
||||||
|
deleteMetricImage,
|
||||||
|
};
|
|
@ -15,13 +15,14 @@ module Projects::AlertManagementHelper
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def alert_management_detail_data(project, alert_id)
|
def alert_management_detail_data(current_user, project, alert_id)
|
||||||
{
|
{
|
||||||
'alert-id' => alert_id,
|
'alert-id' => alert_id,
|
||||||
'project-path' => project.full_path,
|
'project-path' => project.full_path,
|
||||||
'project-id' => project.id,
|
'project-id' => project.id,
|
||||||
'project-issues-path' => project_issues_path(project),
|
'project-issues-path' => project_issues_path(project),
|
||||||
'page' => 'OPERATIONS'
|
'page' => 'OPERATIONS',
|
||||||
|
'can-update' => can?(current_user, :update_alert_management_alert, project).to_s
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terms-settings'), html: { class: 'fieldset-form', id: 'terms-settings' } do |f|
|
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terms-settings'), html: { class: 'fieldset-form', id: 'terms-settings' } do |f|
|
||||||
= form_errors(@application_setting)
|
= form_errors(@application_setting)
|
||||||
|
|
||||||
%fieldset
|
%fieldset
|
||||||
.form-group
|
.form-group
|
||||||
.form-check
|
= f.gitlab_ui_checkbox_component :enforce_terms, _("All users must accept the Terms of Service and Privacy Policy to access GitLab")
|
||||||
= f.check_box :enforce_terms, class: 'form-check-input'
|
|
||||||
= f.label :enforce_terms, class: 'form-check-label' do
|
|
||||||
= _("All users must accept the Terms of Service and Privacy Policy to access GitLab")
|
|
||||||
.form-text.text-muted
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :terms do
|
= f.label :terms do
|
||||||
= _("Terms of Service Agreement and Privacy Policy")
|
= _("Terms of Service Agreement and Privacy Policy")
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
- page_title s_('AlertManagement|Alert detail')
|
- page_title s_('AlertManagement|Alert detail')
|
||||||
- add_page_specific_style 'page_bundles/alert_management_details'
|
- add_page_specific_style 'page_bundles/alert_management_details'
|
||||||
|
|
||||||
#js-alert_details{ data: alert_management_detail_data(@project, @alert_id) }
|
#js-alert_details{ data: alert_management_detail_data(current_user, @project, @alert_id) }
|
||||||
|
|
|
@ -86,26 +86,22 @@ The **Alert details** tab has two sections. The top section provides a short lis
|
||||||
|
|
||||||
### Metrics tab
|
### Metrics tab
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217768) in GitLab 13.2.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217768) in GitLab 13.2.
|
||||||
|
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/340852) in GitLab 14.10. In GitLab 14.9 and earlier, this tab shows a metrics chart for alerts coming from Prometheus.
|
||||||
|
|
||||||
The **Metrics** tab displays a metrics chart for alerts coming from Prometheus. If the alert originated from any other tool, the **Metrics** tab is empty.
|
In many cases, alerts are associated to metrics. You can upload screenshots of metric
|
||||||
For externally-managed Prometheus instances, you must configure your alerting rules to display a chart in the alert. For information about how to configure
|
charts in the **Metrics** tab.
|
||||||
your alerting rules, see [Embedding metrics based on alerts in incident issues](../metrics/embed.md#embedding-metrics-based-on-alerts-in-incident-issues). See
|
|
||||||
[External Prometheus instances](../metrics/alerts.md#external-prometheus-instances) for information about setting up alerts for your self-managed Prometheus
|
|
||||||
instance.
|
|
||||||
|
|
||||||
Prerequisite:
|
To do so, either:
|
||||||
|
|
||||||
- You must have at least the Developer role.
|
- Select **upload** and then select an image from your file browser.
|
||||||
|
- Drag a file from your file browser and drop it in the drop zone.
|
||||||
|
|
||||||
To view the metrics for an alert:
|
When you upload an image, you can add text to the image and link it to the original graph.
|
||||||
|
|
||||||
1. On the top bar, select **Menu > Projects** and find your project.
|
![Text link modal](img/incident_metrics_tab_text_link_modal_v14_9.png)
|
||||||
1. On the left sidebar, select **Monitor > Alerts**.
|
|
||||||
1. Select the alert you want to view.
|
|
||||||
1. Below the title of the alert, select the **Metrics** tab.
|
|
||||||
|
|
||||||
![Alert Metrics View](img/alert_detail_metrics_v13_2.png)
|
If you add a link, it is shown above the uploaded image.
|
||||||
|
|
||||||
#### View an alert's logs
|
#### View an alert's logs
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 27 KiB |
140
spec/frontend/api/alert_management_alerts_api_spec.js
Normal file
140
spec/frontend/api/alert_management_alerts_api_spec.js
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import * as alertManagementAlertsApi from '~/api/alert_management_alerts_api';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
|
||||||
|
describe('~/api/alert_management_alerts_api.js', () => {
|
||||||
|
let mock;
|
||||||
|
let originalGon;
|
||||||
|
|
||||||
|
const projectId = 1;
|
||||||
|
const alertIid = 2;
|
||||||
|
|
||||||
|
const imageData = { filePath: 'test', filename: 'hello', id: 5, url: null };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mock = new MockAdapter(axios);
|
||||||
|
|
||||||
|
originalGon = window.gon;
|
||||||
|
window.gon = { api_version: 'v4' };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
window.gon = originalGon;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchAlertMetricImages', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(axios, 'get');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves metric images from the correct URL and returns them in the response data', () => {
|
||||||
|
const expectedUrl = `/api/v4/projects/${projectId}/alert_management_alerts/${alertIid}/metric_images`;
|
||||||
|
const expectedData = [imageData];
|
||||||
|
const options = { alertIid, id: projectId };
|
||||||
|
|
||||||
|
mock.onGet(expectedUrl).reply(200, { data: expectedData });
|
||||||
|
|
||||||
|
return alertManagementAlertsApi.fetchAlertMetricImages(options).then(({ data }) => {
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
|
||||||
|
expect(data.data).toEqual(expectedData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('uploadAlertMetricImage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(axios, 'post');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uploads a metric image to the correct URL and returns it in the response data', () => {
|
||||||
|
const expectedUrl = `/api/v4/projects/${projectId}/alert_management_alerts/${alertIid}/metric_images`;
|
||||||
|
const expectedData = [imageData];
|
||||||
|
|
||||||
|
const file = new File(['zip contents'], 'hello');
|
||||||
|
const url = 'https://www.example.com';
|
||||||
|
const urlText = 'Example website';
|
||||||
|
|
||||||
|
const expectedFormData = new FormData();
|
||||||
|
expectedFormData.append('file', file);
|
||||||
|
expectedFormData.append('url', url);
|
||||||
|
expectedFormData.append('url_text', urlText);
|
||||||
|
|
||||||
|
mock.onPost(expectedUrl).reply(201, { data: expectedData });
|
||||||
|
|
||||||
|
return alertManagementAlertsApi
|
||||||
|
.uploadAlertMetricImage({
|
||||||
|
alertIid,
|
||||||
|
id: projectId,
|
||||||
|
file,
|
||||||
|
url,
|
||||||
|
urlText,
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
expect(data).toEqual({ data: expectedData });
|
||||||
|
expect(axios.post).toHaveBeenCalledWith(expectedUrl, expectedFormData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateAlertMetricImage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(axios, 'put');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a metric image to the correct URL and returns it in the response data', () => {
|
||||||
|
const imageIid = 3;
|
||||||
|
const expectedUrl = `/api/v4/projects/${projectId}/alert_management_alerts/${alertIid}/metric_images/${imageIid}`;
|
||||||
|
const expectedData = [imageData];
|
||||||
|
|
||||||
|
const url = 'https://www.example.com';
|
||||||
|
const urlText = 'Example website';
|
||||||
|
|
||||||
|
const expectedFormData = new FormData();
|
||||||
|
expectedFormData.append('url', url);
|
||||||
|
expectedFormData.append('url_text', urlText);
|
||||||
|
|
||||||
|
mock.onPut(expectedUrl).reply(200, { data: expectedData });
|
||||||
|
|
||||||
|
return alertManagementAlertsApi
|
||||||
|
.updateAlertMetricImage({
|
||||||
|
alertIid,
|
||||||
|
id: projectId,
|
||||||
|
imageId: imageIid,
|
||||||
|
url,
|
||||||
|
urlText,
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
expect(data).toEqual({ data: expectedData });
|
||||||
|
expect(axios.put).toHaveBeenCalledWith(expectedUrl, expectedFormData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteAlertMetricImage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(axios, 'delete');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a metric image to the correct URL and returns it in the response data', () => {
|
||||||
|
const imageIid = 3;
|
||||||
|
const expectedUrl = `/api/v4/projects/${projectId}/alert_management_alerts/${alertIid}/metric_images/${imageIid}`;
|
||||||
|
const expectedData = [imageData];
|
||||||
|
|
||||||
|
mock.onDelete(expectedUrl).reply(204, { data: expectedData });
|
||||||
|
|
||||||
|
return alertManagementAlertsApi
|
||||||
|
.deleteAlertMetricImage({
|
||||||
|
alertIid,
|
||||||
|
id: projectId,
|
||||||
|
imageId: imageIid,
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
expect(data).toEqual({ data: expectedData });
|
||||||
|
expect(axios.delete).toHaveBeenCalledWith(expectedUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -12,12 +12,17 @@ import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary
|
||||||
import { PAGE_CONFIG, SEVERITY_LEVELS } from '~/vue_shared/alert_details/constants';
|
import { PAGE_CONFIG, SEVERITY_LEVELS } from '~/vue_shared/alert_details/constants';
|
||||||
import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql';
|
import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql';
|
||||||
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
|
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
|
||||||
|
import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue';
|
||||||
|
import createStore from '~/vue_shared/components/metric_images/store/';
|
||||||
|
import service from '~/vue_shared/alert_details/service';
|
||||||
import mockAlerts from './mocks/alerts.json';
|
import mockAlerts from './mocks/alerts.json';
|
||||||
|
|
||||||
const mockAlert = mockAlerts[0];
|
const mockAlert = mockAlerts[0];
|
||||||
const environmentName = 'Production';
|
const environmentName = 'Production';
|
||||||
const environmentPath = '/fake/path';
|
const environmentPath = '/fake/path';
|
||||||
|
|
||||||
|
jest.mock('~/vue_shared/alert_details/service');
|
||||||
|
|
||||||
describe('AlertDetails', () => {
|
describe('AlertDetails', () => {
|
||||||
let environmentData = { name: environmentName, path: environmentPath };
|
let environmentData = { name: environmentName, path: environmentPath };
|
||||||
let mock;
|
let mock;
|
||||||
|
@ -67,9 +72,11 @@ describe('AlertDetails', () => {
|
||||||
$route: { params: {} },
|
$route: { params: {} },
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
...stubs,
|
|
||||||
AlertSummaryRow,
|
AlertSummaryRow,
|
||||||
|
'metric-images-tab': true,
|
||||||
|
...stubs,
|
||||||
},
|
},
|
||||||
|
store: createStore({}, service),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -91,7 +98,7 @@ describe('AlertDetails', () => {
|
||||||
const findEnvironmentName = () => wrapper.findByTestId('environmentName');
|
const findEnvironmentName = () => wrapper.findByTestId('environmentName');
|
||||||
const findEnvironmentPath = () => wrapper.findByTestId('environmentPath');
|
const findEnvironmentPath = () => wrapper.findByTestId('environmentPath');
|
||||||
const findDetailsTable = () => wrapper.findComponent(AlertDetailsTable);
|
const findDetailsTable = () => wrapper.findComponent(AlertDetailsTable);
|
||||||
const findMetricsTab = () => wrapper.findByTestId('metrics');
|
const findMetricsTab = () => wrapper.findComponent(MetricImagesTab);
|
||||||
|
|
||||||
describe('Alert details', () => {
|
describe('Alert details', () => {
|
||||||
describe('when alert is null', () => {
|
describe('when alert is null', () => {
|
||||||
|
@ -129,8 +136,21 @@ describe('AlertDetails', () => {
|
||||||
expect(wrapper.findByTestId('startTimeItem').exists()).toBe(true);
|
expect(wrapper.findByTestId('startTimeItem').exists()).toBe(true);
|
||||||
expect(wrapper.findByTestId('startTimeItem').props('time')).toBe(mockAlert.startedAt);
|
expect(wrapper.findByTestId('startTimeItem').props('time')).toBe(mockAlert.startedAt);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Metrics tab', () => {
|
||||||
|
it('should mount without errors', () => {
|
||||||
|
mountComponent({
|
||||||
|
mountMethod: mount,
|
||||||
|
provide: {
|
||||||
|
canUpdate: true,
|
||||||
|
iid: '1',
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
MetricImagesTab,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
it('renders the metrics tab', () => {
|
|
||||||
expect(findMetricsTab().exists()).toBe(true);
|
expect(findMetricsTab().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -312,7 +332,9 @@ describe('AlertDetails', () => {
|
||||||
|
|
||||||
describe('header', () => {
|
describe('header', () => {
|
||||||
const findHeader = () => wrapper.findByTestId('alert-header');
|
const findHeader = () => wrapper.findByTestId('alert-header');
|
||||||
const stubs = { TimeAgoTooltip: { template: '<span>now</span>' } };
|
const stubs = {
|
||||||
|
TimeAgoTooltip: { template: '<span>now</span>' },
|
||||||
|
};
|
||||||
|
|
||||||
describe('individual header fields', () => {
|
describe('individual header fields', () => {
|
||||||
describe.each`
|
describe.each`
|
||||||
|
|
44
spec/frontend/vue_shared/alert_details/service_spec.js
Normal file
44
spec/frontend/vue_shared/alert_details/service_spec.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { fileList, fileListRaw } from 'jest/vue_shared/components/metric_images/mock_data';
|
||||||
|
import {
|
||||||
|
getMetricImages,
|
||||||
|
uploadMetricImage,
|
||||||
|
updateMetricImage,
|
||||||
|
deleteMetricImage,
|
||||||
|
} from '~/vue_shared/alert_details/service';
|
||||||
|
import * as alertManagementAlertsApi from '~/api/alert_management_alerts_api';
|
||||||
|
|
||||||
|
jest.mock('~/api/alert_management_alerts_api');
|
||||||
|
|
||||||
|
describe('Alert details service', () => {
|
||||||
|
it('fetches metric images', async () => {
|
||||||
|
alertManagementAlertsApi.fetchAlertMetricImages.mockResolvedValue({ data: fileListRaw });
|
||||||
|
const result = await getMetricImages();
|
||||||
|
|
||||||
|
expect(alertManagementAlertsApi.fetchAlertMetricImages).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(fileList);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uploads a metric image', async () => {
|
||||||
|
alertManagementAlertsApi.uploadAlertMetricImage.mockResolvedValue({ data: fileListRaw[0] });
|
||||||
|
const result = await uploadMetricImage();
|
||||||
|
|
||||||
|
expect(alertManagementAlertsApi.uploadAlertMetricImage).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(fileList[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a metric image', async () => {
|
||||||
|
alertManagementAlertsApi.updateAlertMetricImage.mockResolvedValue({ data: fileListRaw[0] });
|
||||||
|
const result = await updateMetricImage();
|
||||||
|
|
||||||
|
expect(alertManagementAlertsApi.updateAlertMetricImage).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(fileList[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a metric image', async () => {
|
||||||
|
alertManagementAlertsApi.deleteAlertMetricImage.mockResolvedValue({ data: '' });
|
||||||
|
const result = await deleteMetricImage();
|
||||||
|
|
||||||
|
expect(alertManagementAlertsApi.deleteAlertMetricImage).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
|
@ -110,15 +110,34 @@ RSpec.describe Projects::AlertManagementHelper do
|
||||||
describe '#alert_management_detail_data' do
|
describe '#alert_management_detail_data' do
|
||||||
let(:alert_id) { 1 }
|
let(:alert_id) { 1 }
|
||||||
let(:issues_path) { project_issues_path(project) }
|
let(:issues_path) { project_issues_path(project) }
|
||||||
|
let(:can_update_alert) { true }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(helper)
|
||||||
|
.to receive(:can?)
|
||||||
|
.with(current_user, :update_alert_management_alert, project)
|
||||||
|
.and_return(can_update_alert)
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns detail page configuration' do
|
it 'returns detail page configuration' do
|
||||||
expect(helper.alert_management_detail_data(project, alert_id)).to eq(
|
expect(helper.alert_management_detail_data(current_user, project, alert_id)).to eq(
|
||||||
'alert-id' => alert_id,
|
'alert-id' => alert_id,
|
||||||
'project-path' => project_path,
|
'project-path' => project_path,
|
||||||
'project-id' => project_id,
|
'project-id' => project_id,
|
||||||
'project-issues-path' => issues_path,
|
'project-issues-path' => issues_path,
|
||||||
'page' => 'OPERATIONS'
|
'page' => 'OPERATIONS',
|
||||||
|
'can-update' => 'true'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when user cannot update alert' do
|
||||||
|
let(:can_update_alert) { false }
|
||||||
|
|
||||||
|
it 'shows error tracking enablement as disabled' do
|
||||||
|
expect(helper.alert_management_detail_data(current_user, project, alert_id)).to include(
|
||||||
|
'can-update' => 'false'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue