Commit c58011bc authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '219255-vue-update' into 'master'

Display Multiple Terraform Plans

Closes #219255

See merge request gitlab-org/gitlab!34392
parents c09a5bb5 d9936c59
<script>
import { __ } from '~/locale';
import { GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import Poll from '~/lib/utils/poll';
export default {
name: 'MRWidgetTerraformPlan',
components: {
GlIcon,
GlLink,
GlLoadingIcon,
GlSprintf,
},
props: {
endpoint: {
type: String,
required: true,
},
},
data() {
return {
loading: true,
plans: {},
};
},
computed: {
addNum() {
return Number(this.plan.create);
},
changeNum() {
return Number(this.plan.update);
},
deleteNum() {
return Number(this.plan.delete);
},
logUrl() {
return this.plan.job_path;
},
plan() {
const firstPlanKey = Object.keys(this.plans)[0];
return this.plans[firstPlanKey] ?? {};
},
validPlanValues() {
return this.addNum + this.changeNum + this.deleteNum >= 0;
},
},
created() {
this.fetchPlans();
},
methods: {
fetchPlans() {
this.loading = true;
const poll = new Poll({
resource: {
fetchPlans: () => axios.get(this.endpoint),
},
data: this.endpoint,
method: 'fetchPlans',
successCallback: ({ data }) => {
this.plans = data;
if (Object.keys(this.plan).length) {
this.loading = false;
poll.stop();
}
},
errorCallback: () => {
this.plans = {};
this.loading = false;
flash(__('An error occurred while loading terraform report'));
},
});
poll.makeRequest();
},
},
};
</script>
<template>
<section class="mr-widget-section">
<div class="mr-widget-body media d-flex flex-row">
<span class="append-right-default align-self-start align-self-lg-center">
<gl-icon name="status_warning" :size="24" />
</span>
<div class="d-flex flex-fill flex-column flex-md-row">
<div class="terraform-mr-plan-text normal d-flex flex-column flex-lg-row">
<p class="m-0 pr-1">{{ __('A terraform report was generated in your pipelines.') }}</p>
<gl-loading-icon v-if="loading" size="md" />
<p v-else-if="validPlanValues" class="m-0">
<gl-sprintf
:message="
__(
'Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
)
"
>
<template #addNum>
<strong>{{ addNum }}</strong>
</template>
<template #changeNum>
<strong>{{ changeNum }}</strong>
</template>
<template #deleteNum>
<strong>{{ deleteNum }}</strong>
</template>
</gl-sprintf>
</p>
<p v-else class="m-0">{{ __('Changes are unknown') }}</p>
</div>
<div class="terraform-mr-plan-actions">
<gl-link
v-if="logUrl"
:href="logUrl"
target="_blank"
data-track-event="click_terraform_mr_plan_button"
data-track-label="mr_widget_terraform_mr_plan_button"
data-track-property="terraform_mr_plan_button"
class="btn btn-sm js-terraform-report-link"
rel="noopener"
>
{{ __('View full log') }}
<gl-icon name="external-link" />
</gl-link>
</div>
</div>
</div>
</section>
</template>
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import TerraformPlan from './terraform_plan.vue';
export default {
name: 'MRWidgetTerraformContainer',
components: {
GlSkeletonLoading,
TerraformPlan,
},
props: {
endpoint: {
type: String,
required: true,
},
},
data() {
return {
loading: true,
plans: {},
poll: null,
};
},
created() {
this.fetchPlans();
},
beforeDestroy() {
this.poll.stop();
},
methods: {
fetchPlans() {
this.loading = true;
this.poll = new Poll({
resource: {
fetchPlans: () => axios.get(this.endpoint),
},
data: this.endpoint,
method: 'fetchPlans',
successCallback: ({ data }) => {
this.plans = data;
if (Object.keys(this.plans).length) {
this.loading = false;
this.poll.stop();
}
},
errorCallback: () => {
this.plans = { bad_plan: {} };
this.loading = false;
this.poll.stop();
},
});
this.poll.makeRequest();
},
},
};
</script>
<template>
<section class="mr-widget-section">
<div v-if="loading" class="mr-widget-body media">
<gl-skeleton-loading />
</div>
<terraform-plan
v-for="(plan, key) in plans"
v-else
:key="key"
:plan="plan"
class="mr-widget-body media"
/>
</section>
</template>
<script>
import { __ } from '~/locale';
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
export default {
name: 'TerraformPlan',
components: {
GlIcon,
GlLink,
GlSprintf,
},
props: {
plan: {
required: true,
type: Object,
},
},
computed: {
addNum() {
return Number(this.plan.create);
},
changeNum() {
return Number(this.plan.update);
},
deleteNum() {
return Number(this.plan.delete);
},
reportChangeText() {
if (this.validPlanValues) {
return __(
'Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
);
}
return __('Generating the report caused an error.');
},
reportHeaderText() {
if (this.plan.job_name) {
return __('The Terraform report %{name} was generated in your pipelines.');
}
return __('A Terraform report was generated in your pipelines.');
},
validPlanValues() {
return this.addNum + this.changeNum + this.deleteNum >= 0;
},
},
};
</script>
<template>
<div class="gl-display-flex">
<span
class="gl-display-flex gl-align-items-center gl-justify-content-center append-right-default gl-align-self-start gl-mt-1"
>
<gl-icon name="status_warning" :size="24" />
</span>
<div class="gl-display-flex gl-flex-fill-1 gl-flex-direction-column flex-md-row">
<div class="terraform-mr-plan-text normal gl-display-flex gl-flex-direction-column">
<p class="gl-m-0 gl-pr-1">
<gl-sprintf :message="reportHeaderText">
<template #name>
<strong>{{ plan.job_name }}</strong>
</template>
</gl-sprintf>
</p>
<p class="gl-m-0">
<gl-sprintf :message="reportChangeText">
<template #addNum>
<strong>{{ addNum }}</strong>
</template>
<template #changeNum>
<strong>{{ changeNum }}</strong>
</template>
<template #deleteNum>
<strong>{{ deleteNum }}</strong>
</template>
</gl-sprintf>
</p>
</div>
<div>
<gl-link
v-if="plan.job_path"
:href="plan.job_path"
target="_blank"
data-track-event="click_terraform_mr_plan_button"
data-track-label="mr_widget_terraform_mr_plan_button"
data-track-property="terraform_mr_plan_button"
class="btn btn-sm js-terraform-report-link"
rel="noopener"
>
{{ __('View full log') }}
<gl-icon name="external-link" />
</gl-link>
</div>
</div>
</div>
</template>
......@@ -36,7 +36,7 @@ import CheckingState from './components/states/mr_widget_checking.vue';
import eventHub from './event_hub';
import notify from '~/lib/utils/notify';
import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue';
import TerraformPlan from './components/mr_widget_terraform_plan.vue';
import TerraformPlan from './components/terraform/mr_widget_terraform_container.vue';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
import { setFaviconOverlay } from '../lib/utils/common_utils';
import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
......
---
title: Display Multiple Terraform Reports in MR Widget
merge_request: 34392
author:
type: added
......@@ -972,6 +972,9 @@ msgstr ""
msgid "A Let's Encrypt account will be configured for this GitLab installation using your email address. You will receive emails to warn of expiring certificates."
msgstr ""
msgid "A Terraform report was generated in your pipelines."
msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
......@@ -1059,9 +1062,6 @@ msgstr ""
msgid "A suggestion is not applicable."
msgstr ""
msgid "A terraform report was generated in your pipelines."
msgstr ""
msgid "A user with write access to the source branch selected this option"
msgstr ""
......@@ -2392,9 +2392,6 @@ msgstr ""
msgid "An error occurred while loading project creation UI"
msgstr ""
msgid "An error occurred while loading terraform report"
msgstr ""
msgid "An error occurred while loading the data. Please try again."
msgstr ""
......@@ -4054,9 +4051,6 @@ msgstr ""
msgid "Changes are still tracked. Useful for cluster/index migrations."
msgstr ""
msgid "Changes are unknown"
msgstr ""
msgid "Changes suppressed. Click to show."
msgstr ""
......@@ -10187,6 +10181,9 @@ msgstr ""
msgid "Generate new export"
msgstr ""
msgid "Generating the report caused an error."
msgstr ""
msgid "Geo"
msgstr ""
......@@ -22235,6 +22232,9 @@ msgstr ""
msgid "The Prometheus server responded with \"bad request\". Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}"
msgstr ""
msgid "The Terraform report %{name} was generated in your pipelines."
msgstr ""
msgid "The URL defined on the primary node that secondary nodes should use to contact it. Defaults to URL"
msgstr ""
......
export const invalidPlan = {};
export const validPlan = {
create: 10,
update: 20,
delete: 30,
job_name: 'Plan Changes',
job_path: '/path/to/ci/logs/1',
};
export const plans = {
'1': validPlan,
'2': invalidPlan,
'3': {
create: 1,
update: 2,
delete: 3,
job_name: 'Plan 3',
job_path: '/path/to/ci/logs/3',
},
};
import { GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { GlSkeletonLoading } from '@gitlab/ui';
import { plans } from './mock_data';
import { shallowMount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import MrWidgetTerraformPlan from '~/vue_merge_request_widget/components/mr_widget_terraform_plan.vue';
import MrWidgetTerraformContainer from '~/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue';
import Poll from '~/lib/utils/poll';
import TerraformPlan from '~/vue_merge_request_widget/components/terraform/terraform_plan.vue';
const plan = {
create: 10,
update: 20,
delete: 30,
job_path: '/path/to/ci/logs',
};
describe('MrWidgetTerraformPlan', () => {
describe('MrWidgetTerraformConainer', () => {
let mock;
let wrapper;
const propsData = { endpoint: '/path/to/terraform/report.json' };
const findPlans = () => wrapper.findAll(TerraformPlan).wrappers.map(x => x.props('plan'));
const mockPollingApi = (response, body, header) => {
mock.onGet(propsData.endpoint).reply(response, body, header);
};
const mountWrapper = () => {
wrapper = shallowMount(MrWidgetTerraformPlan, { propsData });
wrapper = shallowMount(MrWidgetTerraformContainer, { propsData });
return axios.waitForAll();
};
......@@ -36,9 +33,9 @@ describe('MrWidgetTerraformPlan', () => {
mock.restore();
});
describe('loading poll', () => {
describe('when data is loading', () => {
beforeEach(() => {
mockPollingApi(200, { '123': plan }, {});
mockPollingApi(200, plans, {});
return mountWrapper().then(() => {
wrapper.setData({ loading: true });
......@@ -46,28 +43,20 @@ describe('MrWidgetTerraformPlan', () => {
});
});
it('Diplays loading icon when loading is true', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(GlSprintf).exists()).toBe(false);
it('diplays loading skeleton', () => {
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
expect(wrapper.text()).not.toContain(
'A terraform report was generated in your pipelines. Changes are unknown',
);
expect(findPlans()).toEqual([]);
});
});
describe('successful poll', () => {
describe('polling', () => {
let pollRequest;
let pollStop;
beforeEach(() => {
pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
pollStop = jest.spyOn(Poll.prototype, 'stop');
mockPollingApi(200, { '123': plan }, {});
return mountWrapper();
});
afterEach(() => {
......@@ -75,33 +64,43 @@ describe('MrWidgetTerraformPlan', () => {
pollStop.mockRestore();
});
it('content change text', () => {
expect(wrapper.find(GlSprintf).exists()).toBe(true);
});
describe('successful poll', () => {
beforeEach(() => {
mockPollingApi(200, plans, {});
it('renders button when url is found', () => {
expect(wrapper.find(GlLink).exists()).toBe(true);
});
return mountWrapper();
});
it('does not make additional requests after poll is successful', () => {
expect(pollRequest).toHaveBeenCalledTimes(1);
expect(pollStop).toHaveBeenCalledTimes(1);
});
});
it('diplays terraform components and stops loading', () => {
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
describe('polling fails', () => {
beforeEach(() => {
mockPollingApi(500, null, {});
return mountWrapper();
expect(findPlans()).toEqual(Object.values(plans));
});
it('does not make additional requests after poll is successful', () => {
expect(pollRequest).toHaveBeenCalledTimes(1);
expect(pollStop).toHaveBeenCalledTimes(1);
});
});
it('does not display changes text when api fails', () => {
expect(wrapper.text()).toContain(
'A terraform report was generated in your pipelines. Changes are unknown',
);
describe('polling fails', () => {
beforeEach(() => {
mockPollingApi(500, null, {});
return mountWrapper();
});
it('stops loading', () => {
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
});
expect(wrapper.find('.js-terraform-report-link').exists()).toBe(false);
expect(wrapper.find(GlLink).exists()).toBe(false);
it('generates one broken plan', () => {
expect(findPlans()).toEqual([{}]);
});
it('does not make additional requests after poll is unsuccessful', () => {
expect(pollRequest).toHaveBeenCalledTimes(1);
expect(pollStop).toHaveBeenCalledTimes(1);
});
});
});
});
import { invalidPlan, validPlan } from './mock_data';
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import TerraformPlan from '~/vue_merge_request_widget/components/terraform/terraform_plan.vue';
describe('TerraformPlan', () => {
let wrapper;
const findLogButton = () => wrapper.find('.js-terraform-report-link');
const mountWrapper = propsData => {
wrapper = shallowMount(TerraformPlan, { stubs: { GlLink, GlSprintf }, propsData });
};
afterEach(() => {
wrapper.destroy();
});
describe('validPlan', () => {
beforeEach(() => {
mountWrapper({ plan: validPlan });
});
it('diplays the plan job_name', () => {
expect(wrapper.text()).toContain(
`The Terraform report ${validPlan.job_name} was generated in your pipelines.`,
);
});
it('diplays the reported changes', () => {
expect(wrapper.text()).toContain(
`Reported Resource Changes: ${validPlan.create} to add, ${validPlan.update} to change, ${validPlan.delete} to delete`,
);
});
it('renders button when url is found', () => {
expect(findLogButton().exists()).toBe(true);
expect(findLogButton().text()).toEqual('View full log');
});
});
describe('invalidPlan', () => {
beforeEach(() => {
mountWrapper({ plan: invalidPlan });
});
it('diplays generic header since job_name is missing', () => {
expect(wrapper.text()).toContain('A Terraform report was generated in your pipelines.');
});
it('diplays generic error since report values are missing', () => {
expect(wrapper.text()).toContain('Generating the report caused an error.');
});
it('does not render button because url is missing', () => {
expect(findLogButton().exists()).toBe(false);
});
});
});
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