Commit 311e7355 authored by Jiaan Louw's avatar Jiaan Louw Committed by Phil Hughes

Add status checks merge request widget extension

parent 9c647a13
......@@ -158,4 +158,7 @@ export const EXTENSION_ICON_CLASS = {
severityUnknown: 'gl-text-gray-400',
};
export const EXTENSION_SUMMARY_FAILED_CLASS = 'gl-text-red-500';
export const EXTENSION_SUMMARY_NEUTRAL_CLASS = 'gl-text-gray-700';
export { STATE_MACHINE };
import { s__, sprintf, __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import {
EXTENSION_ICONS,
EXTENSION_SUMMARY_FAILED_CLASS,
EXTENSION_SUMMARY_NEUTRAL_CLASS,
} from '~/vue_merge_request_widget/constants';
import { APPROVED, PENDING } from 'ee/reports/status_checks_report/constants';
export default {
name: 'WidgetStatusChecks',
i18n: {
label: s__('StatusCheck|status checks'),
loading: s__('StatusCheck|Status checks are being fetched'),
error: s__('StatusCheck|Failed to load status checks'),
},
expandEvent: 'i_testing_status_checks_widget',
props: ['apiStatusChecksPath'],
computed: {
// Extension computed props
summary({ approved = [], pending = [], failed = [] }) {
if (approved.length > 0 && failed.length === 0 && pending.length === 0) {
return s__('StatusCheck|Status checks all passed');
}
const reports = [];
if (failed.length > 0) {
reports.push(
`<strong class="${EXTENSION_SUMMARY_FAILED_CLASS}">${sprintf(
s__('StatusCheck|%{failed} failed'),
{
failed: failed.length,
},
)}</strong>`,
);
}
if (pending.length > 0) {
reports.push(
`<strong class="${EXTENSION_SUMMARY_NEUTRAL_CLASS}">${sprintf(
s__('StatusCheck|%{pending} pending'),
{
pending: pending.length,
},
)}</strong>`,
);
}
return `
${s__('StatusCheck|Status checks')}
<br>
<span class="gl-font-sm">
${reports.join(__(', and '))}
</span>
`;
},
statusIcon({ pending = [], failed = [] }) {
if (failed.length > 0) {
return EXTENSION_ICONS.warning;
}
if (pending.length > 0) {
return EXTENSION_ICONS.neutral;
}
return EXTENSION_ICONS.success;
},
tertiaryButtons() {
if (this.hasFetchError) {
return [
{
text: __('Retry'),
onClick: () => this.loadCollapsedData(),
},
];
}
return [];
},
},
methods: {
// Extension methods
fetchCollapsedData() {
return this.fetchStatusChecks(this.apiStatusChecksPath).then(this.compareStatusChecks);
},
fetchFullData() {
const { approved, pending, failed } = this.collapsedData;
return Promise.resolve([...approved, ...pending, ...failed]);
},
// Custom methods
fetchStatusChecks(endpoint) {
return axios.get(endpoint).then(({ data }) => data);
},
createReportRow(statusCheck, iconName) {
return {
id: statusCheck.id,
text: `${statusCheck.name}: ${statusCheck.external_url}`,
icon: { name: iconName },
};
},
compareStatusChecks(statusChecks) {
const approved = [];
const pending = [];
const failed = [];
statusChecks.forEach((statusCheck) => {
switch (statusCheck.status) {
case APPROVED:
approved.push(this.createReportRow(statusCheck, EXTENSION_ICONS.success));
break;
case PENDING:
pending.push(this.createReportRow(statusCheck, EXTENSION_ICONS.neutral));
break;
default:
failed.push(this.createReportRow(statusCheck, EXTENSION_ICONS.failed));
}
});
return { approved, pending, failed };
},
},
};
......@@ -11,6 +11,7 @@ import MrWidgetPolicyViolation from './components/states/mr_widget_policy_violat
import MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
import loadPerformanceExtension from './extensions/load_performance';
import browserPerformanceExtension from './extensions/browser_performance';
import statusChecksExtension from './extensions/status_checks';
export default {
components: {
......@@ -108,7 +109,7 @@ export default {
);
},
shouldRenderStatusReport() {
return this.mr.apiStatusChecksPath && !this.mr.isNothingToMergeState;
return this.mr?.apiStatusChecksPath && !this.mr?.isNothingToMergeState;
},
browserPerformanceText() {
......@@ -192,6 +193,11 @@ export default {
this.fetchLoadPerformance();
}
},
shouldRenderStatusReport(newVal) {
if (newVal) {
this.registerStatusCheck();
}
},
},
methods: {
registerLoadPerformance() {
......@@ -204,6 +210,11 @@ export default {
registerExtension(browserPerformanceExtension);
}
},
registerStatusCheck() {
if (this.shouldShowExtension) {
registerExtension(statusChecksExtension);
}
},
getServiceEndpoints(store) {
const base = CEWidgetOptions.methods.getServiceEndpoints(store);
......
......@@ -16,4 +16,15 @@ export const pendingChecks = [
},
];
export const mixedChecks = [...approvedChecks, ...pendingChecks];
export const failedChecks = [
{
id: 2,
name: 'Oh no',
external_url: 'http://noway',
status: 'failed',
},
];
export const pendingAndFailedChecks = [...pendingChecks, ...failedChecks];
export const approvedAndPendingChecks = [...approvedChecks, ...pendingChecks];
......@@ -8,7 +8,7 @@ import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import ReportSection from '~/reports/components/report_section.vue';
import { status as reportStatus } from '~/reports/constants';
import { approvedChecks, pendingChecks, mixedChecks } from './mock_data';
import { approvedChecks, pendingChecks, approvedAndPendingChecks } from './mock_data';
jest.mock('~/flash');
......@@ -73,10 +73,10 @@ describe('Grouped test reports app', () => {
};
describe.each`
state | response | text | resolvedIssues | neutralIssues
${'approved'} | ${approvedChecks} | ${'All passed'} | ${approvedChecks} | ${[]}
${'pending'} | ${pendingChecks} | ${'1 pending'} | ${[]} | ${pendingChecks}
${'mixed'} | ${mixedChecks} | ${'1 pending'} | ${approvedChecks} | ${pendingChecks}
state | response | text | resolvedIssues | neutralIssues
${'approved'} | ${approvedChecks} | ${'All passed'} | ${approvedChecks} | ${[]}
${'pending'} | ${pendingChecks} | ${'1 pending'} | ${[]} | ${pendingChecks}
${'mixed'} | ${approvedAndPendingChecks} | ${'1 pending'} | ${approvedChecks} | ${pendingChecks}
`('and the status checks are $state', ({ response, text, resolvedIssues, neutralIssues }) => {
beforeEach(() => {
return mountWithResponse(httpStatus.OK, response);
......
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import statusChecksExtension from 'ee/vue_merge_request_widget/extensions/status_checks';
import httpStatus from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import {
approvedChecks,
pendingChecks,
approvedAndPendingChecks,
pendingAndFailedChecks,
} from '../../../reports/status_checks_report/mock_data';
describe('Status checks extension', () => {
let wrapper;
let mock;
const endpoint = 'https://test';
registerExtension(statusChecksExtension);
const createComponent = () => {
wrapper = mount(extensionsContainer, {
propsData: {
mr: {
apiStatusChecksPath: endpoint,
},
},
});
};
const setupWithResponse = (statusCode, data) => {
mock.onGet(endpoint).reply(statusCode, data);
createComponent();
return waitForPromises();
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
});
describe('summary', () => {
describe('when loading', () => {
beforeEach(async () => {
await setupWithResponse(httpStatus.OK, new Promise(() => {}));
});
it('should render loading text', () => {
expect(wrapper.text()).toContain('Status checks are being fetched');
});
});
describe('when the fetching fails', () => {
beforeEach(async () => {
await setupWithResponse(httpStatus.NOT_FOUND);
});
it('should render the failed text', () => {
expect(wrapper.text()).toContain('Failed to load status checks');
});
it('should render the retry button', () => {
expect(wrapper.text()).toContain('Retry');
});
});
describe('when the fetching succeeds', () => {
describe.each`
state | response | text
${'approved'} | ${approvedChecks} | ${'Status checks all passed'}
${'pending'} | ${pendingChecks} | ${'1 pending'}
${'approved and pending'} | ${approvedAndPendingChecks} | ${'1 pending'}
${'failed and pending'} | ${pendingAndFailedChecks} | ${'1 failed, and 1 pending'}
`('and the status checks are $state', ({ response, text }) => {
beforeEach(async () => {
await setupWithResponse(httpStatus.OK, response);
});
it(`renders '${text}' in the report section`, () => {
expect(wrapper.text()).toContain(text);
});
});
});
});
describe('expanded data', () => {
beforeEach(async () => {
await setupWithResponse(httpStatus.OK, approvedAndPendingChecks);
wrapper
.find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
.trigger('click');
});
it('shows the expanded list of text items', () => {
const listItems = wrapper.findAll('[data-testid="extension-list-item"]');
expect(listItems).toHaveLength(2);
expect(listItems.at(0).text()).toBe('Foo: http://foo');
expect(listItems.at(1).text()).toBe('Foo Bar: http://foobar');
});
});
});
......@@ -1212,6 +1212,9 @@ msgstr ""
msgid "+%{tags} more"
msgstr ""
msgid ", and "
msgstr ""
msgid ", or "
msgstr ""
......@@ -33278,6 +33281,9 @@ msgstr ""
msgid "Status: %{title}"
msgstr ""
msgid "StatusCheck|%{failed} failed"
msgstr ""
msgid "StatusCheck|%{pending} pending"
msgstr ""
......@@ -33308,6 +33314,9 @@ msgstr ""
msgid "StatusCheck|External API is already in use by another status check."
msgstr ""
msgid "StatusCheck|Failed to load status checks"
msgstr ""
msgid "StatusCheck|Failed to load status checks."
msgstr ""
......@@ -33329,6 +33338,12 @@ msgstr ""
msgid "StatusCheck|Status checks"
msgstr ""
msgid "StatusCheck|Status checks all passed"
msgstr ""
msgid "StatusCheck|Status checks are being fetched"
msgstr ""
msgid "StatusCheck|Status to check"
msgstr ""
......@@ -33344,6 +33359,9 @@ msgstr ""
msgid "StatusCheck|You are about to remove the %{name} status check."
msgstr ""
msgid "StatusCheck|status checks"
msgstr ""
msgid "StatusPage|AWS %{docsLink}"
msgstr ""
......
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