Commit e926bab8 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '340852-move-incident-metrics-ui-to-shared-location' into 'master'

Move incident metrics logic to shared location

See merge request gitlab-org/gitlab!84121
parents f06d1543 a85e11f7
......@@ -17,12 +17,13 @@ export default {
GlTab,
GlTabs,
HighlightBar,
MetricsTab: () => import('ee_component/issues/show/components/incidents/metrics_tab.vue'),
TimelineTab: () =>
import('ee_component/issues/show/components/incidents/timeline_events_tab.vue'),
IncidentMetricTab: () =>
import('ee_component/issues/show/components/incidents/incident_metric_tab.vue'),
},
mixins: [glFeatureFlagsMixin()],
inject: ['fullPath', 'iid', 'uploadMetricsFeatureAvailable'],
inject: ['fullPath', 'iid'],
apollo: {
alert: {
query: getAlert,
......@@ -93,7 +94,7 @@ export default {
<highlight-bar :alert="alert" />
<description-component v-bind="$attrs" />
</gl-tab>
<metrics-tab v-if="uploadMetricsFeatureAvailable" data-testid="metrics-tab" />
<incident-metric-tab />
<gl-tab
v-if="alert"
class="alert-management-details"
......
......@@ -3,8 +3,7 @@ import { GlFormGroup, GlFormInput, GlLoadingIcon, GlModal, GlTab } from '@gitlab
import { mapState, mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import MetricsImage from './metrics_image.vue';
import createStore from './store';
import MetricImagesTable from '~/vue_shared/components/metric_images/metric_images_table.vue';
export default {
components: {
......@@ -13,7 +12,7 @@ export default {
GlLoadingIcon,
GlModal,
GlTab,
MetricsImage,
MetricImagesTable,
UploadDropzone,
},
inject: ['canUpdate', 'projectId', 'iid'],
......@@ -25,7 +24,6 @@ export default {
modalUrlText: '',
};
},
store: createStore(),
computed: {
...mapState(['metricImages', 'isLoadingMetricImages', 'isUploadingImage']),
actionPrimaryProps() {
......@@ -41,11 +39,11 @@ export default {
},
},
mounted() {
this.setInitialData({ issueIid: this.iid, projectId: this.projectId });
this.fetchMetricImages();
this.setInitialData({ modelIid: this.iid, projectId: this.projectId });
this.fetchImages();
},
methods: {
...mapActions(['fetchMetricImages', 'uploadImage', 'setInitialData']),
...mapActions(['fetchImages', 'uploadImage', 'setInitialData']),
clearInputs() {
this.modalVisible = false;
this.modalUrl = '';
......@@ -111,7 +109,7 @@ export default {
<gl-form-input id="upload-url-input" v-model="modalUrl" />
</gl-form-group>
</gl-modal>
<metrics-image v-for="metric in metricImages" :key="metric.id" v-bind="metric" />
<metric-images-table v-for="metric in metricImages" :key="metric.id" v-bind="metric" />
<upload-dropzone
v-if="canUpdate"
:drop-description-message="$options.i18n.dropDescription"
......
import createFlash from '~/flash';
import { s__ } from '~/locale';
import {
deleteMetricImage,
getMetricImages,
uploadMetricImage,
updateMetricImage,
} from '../service';
import * as types from './mutation_types';
export const fetchMetricImages = async ({ state, commit }) => {
export const fetchImagesFactory = (service) => async ({ state, commit }) => {
commit(types.REQUEST_METRIC_IMAGES);
const { issueIid, projectId } = state;
const { modelIid, projectId } = state;
try {
const response = await getMetricImages({ id: projectId, issueIid });
const response = await service.getMetricImages({ id: projectId, modelIid });
commit(types.RECEIVE_METRIC_IMAGES_SUCCESS, response);
} catch (error) {
commit(types.RECEIVE_METRIC_IMAGES_ERROR);
createFlash({ message: s__('Incidents|There was an issue loading metric images.') });
createFlash({ message: s__('MetricImages|There was an issue loading metric images.') });
}
};
export const uploadImage = async ({ state, commit }, { files, url, urlText }) => {
export const uploadImageFactory = (service) => async (
{ state, commit },
{ files, url, urlText },
) => {
commit(types.REQUEST_METRIC_UPLOAD);
const { issueIid, projectId } = state;
const { modelIid, projectId } = state;
try {
const response = await uploadMetricImage({
const response = await service.uploadMetricImage({
file: files.item(0),
id: projectId,
issueIid,
modelIid,
url,
urlText,
});
commit(types.RECEIVE_METRIC_UPLOAD_SUCCESS, response);
} catch (error) {
commit(types.RECEIVE_METRIC_UPLOAD_ERROR);
createFlash({ message: s__('Incidents|There was an issue uploading your image.') });
createFlash({ message: s__('MetricImages|There was an issue uploading your image.') });
}
};
export const updateImage = async ({ state, commit }, { imageId, url, urlText }) => {
export const updateImageFactory = (service) => async (
{ state, commit },
{ imageId, url, urlText },
) => {
commit(types.REQUEST_METRIC_UPLOAD);
const { issueIid, projectId } = state;
const { modelIid, projectId } = state;
try {
const response = await updateMetricImage({
issueIid,
const response = await service.updateMetricImage({
modelIid,
id: projectId,
imageId,
url,
......@@ -58,21 +57,29 @@ export const updateImage = async ({ state, commit }, { imageId, url, urlText })
commit(types.RECEIVE_METRIC_UPDATE_SUCCESS, response);
} catch (error) {
commit(types.RECEIVE_METRIC_UPLOAD_ERROR);
createFlash({ message: s__('Incidents|There was an issue updating your image.') });
createFlash({ message: s__('MetricImages|There was an issue updating your image.') });
}
};
export const deleteImage = async ({ state, commit }, imageId) => {
const { issueIid, projectId } = state;
export const deleteImageFactory = (service) => async ({ state, commit }, imageId) => {
const { modelIid, projectId } = state;
try {
await deleteMetricImage({ imageId, id: projectId, issueIid });
await service.deleteMetricImage({ imageId, id: projectId, modelIid });
commit(types.RECEIVE_METRIC_DELETE_SUCCESS, imageId);
} catch (error) {
createFlash({ message: s__('Incidents|There was an issue deleting the image.') });
createFlash({ message: s__('MetricImages|There was an issue deleting the image.') });
}
};
export const setInitialData = ({ commit }, data) => {
commit(types.SET_INITIAL_DATA, data);
};
export default (service) => ({
fetchImages: fetchImagesFactory(service),
uploadImage: uploadImageFactory(service),
updateImage: updateImageFactory(service),
deleteImage: deleteImageFactory(service),
setInitialData,
});
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import actionsFactory from './actions';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export default (initialState) =>
export default (initialState, service) =>
new Vuex.Store({
actions,
actions: actionsFactory(service),
mutations,
state: createState(initialState),
});
......@@ -32,8 +32,8 @@ export default {
const metricIndex = state.metricImages.findIndex((image) => image.id === imageId);
state.metricImages.splice(metricIndex, 1);
},
[types.SET_INITIAL_DATA](state, { issueIid, projectId }) {
state.issueIid = issueIid;
[types.SET_INITIAL_DATA](state, { modelIid, projectId }) {
state.modelIid = modelIid;
state.projectId = projectId;
},
};
export default ({ issueIid, projectId } = {}) => ({
export default ({ modelIid, projectId } = {}) => ({
// Initial state
issueIid,
modelIid,
projectId,
// View state
......
<script>
import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue';
import service from 'ee_component/issues/show/components/incidents/service';
import createStore from '~/vue_shared/components/metric_images/store';
export default {
components: {
MetricImagesTab,
},
inject: ['uploadMetricsFeatureAvailable'],
store: createStore({}, service),
};
</script>
<template>
<metric-images-tab v-if="uploadMetricsFeatureAvailable" data-testid="metrics-tab" />
</template>
import Api from 'ee/api';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
function replaceModelIId(payload = {}) {
const { modelIid, ...rest } = payload;
return { issueIid: modelIid, ...rest };
}
export const getMetricImages = async (payload) => {
const response = await Api.fetchIssueMetricImages(payload);
const apiPayload = replaceModelIId(payload);
const response = await Api.fetchIssueMetricImages(apiPayload);
return convertObjectPropsToCamelCase(response.data, { deep: true });
};
export const uploadMetricImage = async (payload) => {
const response = await Api.uploadIssueMetricImage(payload);
const apiPayload = replaceModelIId(payload);
const response = await Api.uploadIssueMetricImage(apiPayload);
return convertObjectPropsToCamelCase(response.data);
};
export const updateMetricImage = async (payload) => {
const response = await Api.updateIssueMetricImage(payload);
const apiPayload = replaceModelIId(payload);
const response = await Api.updateIssueMetricImage(apiPayload);
return convertObjectPropsToCamelCase(response.data);
};
export const deleteMetricImage = async (payload) => {
const response = await Api.deleteMetricImage(payload);
const apiPayload = replaceModelIId(payload);
const response = await Api.deleteMetricImage(apiPayload);
return convertObjectPropsToCamelCase(response.data);
};
export default {
getMetricImages,
uploadMetricImage,
updateMetricImage,
deleteMetricImage,
};
......@@ -28,6 +28,7 @@ describe('Incident Tabs component', () => {
provide: {
fullPath: '',
iid: '',
projectId: '',
uploadMetricsFeatureAvailable: true,
glFeatures: { incidentTimeline: true, incidentTimelineEvents: true },
},
......
import { fileList, fileListRaw } from 'jest/vue_shared/components/metric_images/mock_data';
import Api from 'ee/api';
import {
getMetricImages,
uploadMetricImage,
updateMetricImage,
deleteMetricImage,
} from 'ee/issues/show/components/incidents/service';
import { fileList, fileListRaw } from './mock_data';
jest.mock('ee/api', () => ({
fetchIssueMetricImages: jest.fn(),
uploadIssueMetricImage: jest.fn(),
updateIssueMetricImage: jest.fn(),
}));
jest.mock('ee/api');
describe('Incidents service', () => {
it('fetches metric images', async () => {
......@@ -36,4 +33,12 @@ describe('Incidents service', () => {
expect(Api.updateIssueMetricImage).toHaveBeenCalled();
expect(result).toEqual(fileList[0]);
});
it('deletes a metric image', async () => {
Api.deleteMetricImage.mockResolvedValue({ data: '' });
const result = await deleteMetricImage();
expect(Api.deleteMetricImage).toHaveBeenCalled();
expect(result).toEqual({});
});
});
......@@ -19916,18 +19916,6 @@ msgstr ""
msgid "Incidents|Must start with http or https"
msgstr ""
msgid "Incidents|There was an issue deleting the image."
msgstr ""
msgid "Incidents|There was an issue loading metric images."
msgstr ""
msgid "Incidents|There was an issue updating your image."
msgstr ""
msgid "Incidents|There was an issue uploading your image."
msgstr ""
msgid "Incident|Add new timeline event"
msgstr ""
......@@ -23874,6 +23862,18 @@ msgstr ""
msgid "MetricChart|There is too much data to calculate. Please change your selection."
msgstr ""
msgid "MetricImages|There was an issue deleting the image."
msgstr ""
msgid "MetricImages|There was an issue loading metric images."
msgstr ""
msgid "MetricImages|There was an issue updating your image."
msgstr ""
msgid "MetricImages|There was an issue uploading your image."
msgstr ""
msgid "Metrics"
msgstr ""
......
......@@ -34,6 +34,7 @@ describe('Incident Tabs component', () => {
provide: {
fullPath: '',
iid: '',
projectId: '',
uploadMetricsFeatureAvailable: true,
glFeatures: { incidentTimeline: true, incidentTimelineEvents: true },
},
......
......@@ -3,31 +3,30 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import merge from 'lodash/merge';
import Vuex from 'vuex';
import MetricsImage from 'ee/issues/show/components/incidents/metrics_image.vue';
import MetricsTab from 'ee/issues/show/components/incidents/metrics_tab.vue';
import { getMetricImages } from 'ee/issues/show/components/incidents/service';
import createStore from 'ee/issues/show/components/incidents/store';
import MetricImagesTable from '~/vue_shared/components/metric_images/metric_images_table.vue';
import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue';
import createStore from '~/vue_shared/components/metric_images/store';
import waitForPromises from 'helpers/wait_for_promises';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import { fileList, initialData } from './mock_data';
jest.mock('ee/issues/show/components/incidents/service', () => ({
const service = {
getMetricImages: jest.fn(),
}));
};
const mockEvent = { preventDefault: jest.fn() };
Vue.use(Vuex);
describe('Metrics tab', () => {
describe('Metric images tab', () => {
let wrapper;
let store;
const mountComponent = (options = {}) => {
store = createStore();
store = createStore({}, service);
wrapper = shallowMount(
MetricsTab,
MetricImagesTab,
merge(
{
store,
......@@ -54,7 +53,7 @@ describe('Metrics tab', () => {
});
const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
const findImages = () => wrapper.findAllComponents(MetricsImage);
const findImages = () => wrapper.findAllComponents(MetricImagesTable);
const findModal = () => wrapper.findComponent(GlModal);
const submitModal = () => findModal().vm.$emit('primary', mockEvent);
const cancelModal = () => findModal().vm.$emit('hidden');
......@@ -81,7 +80,7 @@ describe('Metrics tab', () => {
describe('onLoad action', () => {
it('should load images', async () => {
getMetricImages.mockImplementation(() => Promise.resolve(fileList));
service.getMetricImages.mockImplementation(() => Promise.resolve(fileList));
mountComponent();
......
......@@ -3,8 +3,8 @@ import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
import merge from 'lodash/merge';
import Vuex from 'vuex';
import MetricsImage from 'ee/issues/show/components/incidents/metrics_image.vue';
import createStore from 'ee/issues/show/components/incidents/store';
import createStore from '~/vue_shared/components/metric_images/store';
import MetricsImageTable from '~/vue_shared/components/metric_images/metric_images_table.vue';
import waitForPromises from 'helpers/wait_for_promises';
const defaultProps = {
......@@ -25,7 +25,7 @@ describe('Metrics upload item', () => {
store = createStore();
wrapper = mountMethod(
MetricsImage,
MetricsImageTable,
merge(
{
store,
......
import Vue from 'vue';
import Vuex from 'vuex';
import {
getMetricImages,
uploadMetricImage,
updateMetricImage,
deleteMetricImage,
} from 'ee/issues/show/components/incidents/service';
import createStore from 'ee/issues/show/components/incidents/store';
import * as actions from 'ee/issues/show/components/incidents/store/actions';
import * as types from 'ee/issues/show/components/incidents/store/mutation_types';
import actionsFactory from '~/vue_shared/components/metric_images/store/actions';
import * as types from '~/vue_shared/components/metric_images/store/mutation_types';
import createStore from '~/vue_shared/components/metric_images/store';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { fileList, initialData } from '../mock_data';
jest.mock('~/flash');
jest.mock('ee/issues/show/components/incidents/service', () => ({
const service = {
getMetricImages: jest.fn(),
uploadMetricImage: jest.fn(),
updateMetricImage: jest.fn(),
deleteMetricImage: jest.fn(),
}));
};
const actions = actionsFactory(service);
const defaultState = {
issueIid: 1,
......@@ -44,9 +40,9 @@ describe('Metrics tab store actions', () => {
describe('fetching metric images', () => {
it('should call success action when fetching metric images', () => {
getMetricImages.mockImplementation(() => Promise.resolve(fileList));
service.getMetricImages.mockImplementation(() => Promise.resolve(fileList));
testAction(actions.fetchMetricImages, null, state, [
testAction(actions.fetchImages, null, state, [
{ type: types.REQUEST_METRIC_IMAGES },
{
type: types.RECEIVE_METRIC_IMAGES_SUCCESS,
......@@ -56,10 +52,10 @@ describe('Metrics tab store actions', () => {
});
it('should call error action when fetching metric images with an error', async () => {
getMetricImages.mockImplementation(() => Promise.reject());
service.getMetricImages.mockImplementation(() => Promise.reject());
await testAction(
actions.fetchMetricImages,
actions.fetchImages,
null,
state,
[{ type: types.REQUEST_METRIC_IMAGES }, { type: types.RECEIVE_METRIC_IMAGES_ERROR }],
......@@ -81,7 +77,7 @@ describe('Metrics tab store actions', () => {
};
it('should call success action when uploading an image', () => {
uploadMetricImage.mockImplementation(() => Promise.resolve(fileList[0]));
service.uploadMetricImage.mockImplementation(() => Promise.resolve(fileList[0]));
testAction(actions.uploadImage, payload, state, [
{ type: types.REQUEST_METRIC_UPLOAD },
......@@ -93,7 +89,7 @@ describe('Metrics tab store actions', () => {
});
it('should call error action when failing to upload an image', async () => {
uploadMetricImage.mockImplementation(() => Promise.reject());
service.uploadMetricImage.mockImplementation(() => Promise.reject());
await testAction(
actions.uploadImage,
......@@ -113,7 +109,7 @@ describe('Metrics tab store actions', () => {
};
it('should call success action when updating an image', () => {
updateMetricImage.mockImplementation(() => Promise.resolve());
service.updateMetricImage.mockImplementation(() => Promise.resolve());
testAction(actions.updateImage, payload, state, [
{ type: types.REQUEST_METRIC_UPLOAD },
......@@ -124,7 +120,7 @@ describe('Metrics tab store actions', () => {
});
it('should call error action when failing to update an image', async () => {
updateMetricImage.mockImplementation(() => Promise.reject());
service.updateMetricImage.mockImplementation(() => Promise.reject());
await testAction(
actions.updateImage,
......@@ -141,7 +137,7 @@ describe('Metrics tab store actions', () => {
const payload = fileList[0].id;
it('should call success action when deleting an image', () => {
deleteMetricImage.mockImplementation(() => Promise.resolve());
service.deleteMetricImage.mockImplementation(() => Promise.resolve());
testAction(actions.deleteImage, payload, state, [
{
......
import { cloneDeep } from 'lodash';
import * as types from 'ee/issues/show/components/incidents/store/mutation_types';
import mutations from 'ee/issues/show/components/incidents/store/mutations';
import * as types from '~/vue_shared/components/metric_images/store/mutation_types';
import mutations from '~/vue_shared/components/metric_images/store/mutations';
import { initialData } from '../mock_data';
const defaultState = {
......@@ -140,7 +140,7 @@ describe('Metric images mutations', () => {
});
it('should unset the loading state', () => {
expect(state.issueIid).toBe(initialData.issueIid);
expect(state.modelIid).toBe(initialData.modelIid);
expect(state.projectId).toBe(initialData.projectId);
});
});
......
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