Commit 145ab3c0 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'jlouw-spike-extended-status-checks' into 'master'

Add status checks merge request widget extension

See merge request gitlab-org/gitlab!74200
parents ef6f5f7e 311e7355
...@@ -158,4 +158,7 @@ export const EXTENSION_ICON_CLASS = { ...@@ -158,4 +158,7 @@ export const EXTENSION_ICON_CLASS = {
severityUnknown: 'gl-text-gray-400', 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 }; 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 ...@@ -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 MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
import loadPerformanceExtension from './extensions/load_performance'; import loadPerformanceExtension from './extensions/load_performance';
import browserPerformanceExtension from './extensions/browser_performance'; import browserPerformanceExtension from './extensions/browser_performance';
import statusChecksExtension from './extensions/status_checks';
export default { export default {
components: { components: {
...@@ -108,7 +109,7 @@ export default { ...@@ -108,7 +109,7 @@ export default {
); );
}, },
shouldRenderStatusReport() { shouldRenderStatusReport() {
return this.mr.apiStatusChecksPath && !this.mr.isNothingToMergeState; return this.mr?.apiStatusChecksPath && !this.mr?.isNothingToMergeState;
}, },
browserPerformanceText() { browserPerformanceText() {
...@@ -192,6 +193,11 @@ export default { ...@@ -192,6 +193,11 @@ export default {
this.fetchLoadPerformance(); this.fetchLoadPerformance();
} }
}, },
shouldRenderStatusReport(newVal) {
if (newVal) {
this.registerStatusCheck();
}
},
}, },
methods: { methods: {
registerLoadPerformance() { registerLoadPerformance() {
...@@ -204,6 +210,11 @@ export default { ...@@ -204,6 +210,11 @@ export default {
registerExtension(browserPerformanceExtension); registerExtension(browserPerformanceExtension);
} }
}, },
registerStatusCheck() {
if (this.shouldShowExtension) {
registerExtension(statusChecksExtension);
}
},
getServiceEndpoints(store) { getServiceEndpoints(store) {
const base = CEWidgetOptions.methods.getServiceEndpoints(store); const base = CEWidgetOptions.methods.getServiceEndpoints(store);
......
...@@ -16,4 +16,15 @@ export const pendingChecks = [ ...@@ -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'; ...@@ -8,7 +8,7 @@ import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
import { status as reportStatus } from '~/reports/constants'; import { status as reportStatus } from '~/reports/constants';
import { approvedChecks, pendingChecks, mixedChecks } from './mock_data'; import { approvedChecks, pendingChecks, approvedAndPendingChecks } from './mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -73,10 +73,10 @@ describe('Grouped test reports app', () => { ...@@ -73,10 +73,10 @@ describe('Grouped test reports app', () => {
}; };
describe.each` describe.each`
state | response | text | resolvedIssues | neutralIssues state | response | text | resolvedIssues | neutralIssues
${'approved'} | ${approvedChecks} | ${'All passed'} | ${approvedChecks} | ${[]} ${'approved'} | ${approvedChecks} | ${'All passed'} | ${approvedChecks} | ${[]}
${'pending'} | ${pendingChecks} | ${'1 pending'} | ${[]} | ${pendingChecks} ${'pending'} | ${pendingChecks} | ${'1 pending'} | ${[]} | ${pendingChecks}
${'mixed'} | ${mixedChecks} | ${'1 pending'} | ${approvedChecks} | ${pendingChecks} ${'mixed'} | ${approvedAndPendingChecks} | ${'1 pending'} | ${approvedChecks} | ${pendingChecks}
`('and the status checks are $state', ({ response, text, resolvedIssues, neutralIssues }) => { `('and the status checks are $state', ({ response, text, resolvedIssues, neutralIssues }) => {
beforeEach(() => { beforeEach(() => {
return mountWithResponse(httpStatus.OK, response); 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 "" ...@@ -1212,6 +1212,9 @@ msgstr ""
msgid "+%{tags} more" msgid "+%{tags} more"
msgstr "" msgstr ""
msgid ", and "
msgstr ""
msgid ", or " msgid ", or "
msgstr "" msgstr ""
...@@ -33287,6 +33290,9 @@ msgstr "" ...@@ -33287,6 +33290,9 @@ msgstr ""
msgid "Status: %{title}" msgid "Status: %{title}"
msgstr "" msgstr ""
msgid "StatusCheck|%{failed} failed"
msgstr ""
msgid "StatusCheck|%{pending} pending" msgid "StatusCheck|%{pending} pending"
msgstr "" msgstr ""
...@@ -33317,6 +33323,9 @@ msgstr "" ...@@ -33317,6 +33323,9 @@ msgstr ""
msgid "StatusCheck|External API is already in use by another status check." msgid "StatusCheck|External API is already in use by another status check."
msgstr "" msgstr ""
msgid "StatusCheck|Failed to load status checks"
msgstr ""
msgid "StatusCheck|Failed to load status checks." msgid "StatusCheck|Failed to load status checks."
msgstr "" msgstr ""
...@@ -33338,6 +33347,12 @@ msgstr "" ...@@ -33338,6 +33347,12 @@ msgstr ""
msgid "StatusCheck|Status checks" msgid "StatusCheck|Status checks"
msgstr "" msgstr ""
msgid "StatusCheck|Status checks all passed"
msgstr ""
msgid "StatusCheck|Status checks are being fetched"
msgstr ""
msgid "StatusCheck|Status to check" msgid "StatusCheck|Status to check"
msgstr "" msgstr ""
...@@ -33353,6 +33368,9 @@ msgstr "" ...@@ -33353,6 +33368,9 @@ msgstr ""
msgid "StatusCheck|You are about to remove the %{name} status check." msgid "StatusCheck|You are about to remove the %{name} status check."
msgstr "" msgstr ""
msgid "StatusCheck|status checks"
msgstr ""
msgid "StatusPage|AWS %{docsLink}" msgid "StatusPage|AWS %{docsLink}"
msgstr "" 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