Commit af2798f7 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 80486a9c 4579fd7d
......@@ -4,7 +4,7 @@ import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
import boardsStore from '../stores/boards_store';
export default {
name: 'BoardsIssueCard',
name: 'BoardCardLayout',
components: {
IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
},
......@@ -81,7 +81,7 @@ export default {
:data-issue-iid="issue.iid"
:data-issue-path="issue.referencePath"
data-testid="board_card"
class="board-card p-3 rounded"
class="board-card gl-p-5 gl-rounded-base"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)"
......
......@@ -17,15 +17,21 @@ export class CiSchemaExtension extends EditorLiteExtension {
* @param {String?} opts.ref - Current ref. Defaults to master
*/
registerCiSchema({ projectNamespace, projectPath, ref = 'master' } = {}) {
const ciSchemaUri = Api.buildUrl(Api.projectFileSchemaPath)
const ciSchemaPath = Api.buildUrl(Api.projectFileSchemaPath)
.replace(':namespace_path', projectNamespace)
.replace(':project_path', projectPath)
.replace(':ref', ref)
.replace(':filename', EXTENSION_CI_SCHEMA_FILE_NAME_MATCH);
// In order for workers loaded from `data://` as the
// ones loaded by monaco editor, we use absolute URLs
// to fetch schema files, hence the `gon.gitlab_url`
// reference. This prevents error:
// "Failed to execute 'fetch' on 'WorkerGlobalScope'"
const absoluteSchemaUrl = gon.gitlab_url + ciSchemaPath;
const modelFileName = this.getModel().uri.path.split('/').pop();
registerSchema({
uri: ciSchemaUri,
uri: absoluteSchemaUrl,
fileMatch: [modelFileName],
});
}
......
......@@ -3,6 +3,7 @@ import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import httpStatusCodes from '~/lib/utils/http_status';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import CiLint from './components/lint/ci_lint.vue';
......@@ -23,7 +24,6 @@ const COMMIT_FAILURE = 'COMMIT_FAILURE';
const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
const LOAD_FAILURE_NO_REF = 'LOAD_FAILURE_NO_REF';
const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export default {
......@@ -125,6 +125,9 @@ export default {
isBlobContentLoading() {
return this.$apollo.queries.content.loading;
},
isBlobContentError() {
return this.failureType === LOAD_FAILURE_NO_FILE || this.failureType === LOAD_FAILURE_UNKNOWN;
},
isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading;
},
......@@ -144,14 +147,11 @@ export default {
},
failure() {
switch (this.failureType) {
case LOAD_FAILURE_NO_REF:
return {
text: this.$options.alertTexts[LOAD_FAILURE_NO_REF],
variant: 'danger',
};
case LOAD_FAILURE_NO_FILE:
return {
text: this.$options.alertTexts[LOAD_FAILURE_NO_FILE],
text: sprintf(this.$options.alertTexts[LOAD_FAILURE_NO_FILE], {
filePath: this.ciConfigPath,
}),
variant: 'danger',
};
case LOAD_FAILURE_UNKNOWN:
......@@ -182,9 +182,8 @@ export default {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_NO_FILE]: s__('Pipelines|No CI file found in this repository, please add one.'),
[LOAD_FAILURE_NO_REF]: s__(
'Pipelines|Repository does not have a default branch, please set one.',
[LOAD_FAILURE_NO_FILE]: s__(
'Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again.',
),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
},
......@@ -193,12 +192,13 @@ export default {
const { networkError } = error;
const { response } = networkError;
if (response?.status === 404) {
// 404 for missing CI file
// 404 for missing CI file
// 400 for blank projects with no repository
if (
response?.status === httpStatusCodes.NOT_FOUND ||
response?.status === httpStatusCodes.BAD_REQUEST
) {
this.reportFailure(LOAD_FAILURE_NO_FILE);
} else if (response?.status === 400) {
// 400 for a missing ref when no default branch is set
this.reportFailure(LOAD_FAILURE_NO_REF);
} else {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
}
......@@ -299,9 +299,9 @@ export default {
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
</ul>
</gl-alert>
<div class="gl-mt-4">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<div v-else class="file-editor gl-mb-3">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<div v-else-if="!isBlobContentError" class="gl-mt-4">
<div class="file-editor gl-mb-3">
<div class="info-well gl-display-none gl-display-sm-block">
<validation-segment
class="well-segment"
......
......@@ -32,13 +32,17 @@
.rotations-modal {
.gl-card {
min-width: 75%;
width: fit-content;
@include gl-bg-gray-10;
}
&.gl-modal .modal-md {
max-width: 640px;
}
// TODO: move to gitlab/ui utilities
// https://gitlab.com/gitlab-org/gitlab/-/issues/297502
.gl-w-fit-content {
width: fit-content;
}
}
//// Copied from roadmaps.scss - adapted for on-call schedules
......
......@@ -208,7 +208,7 @@ export default {
<gl-dropdown
data-testid="rotation-start-time"
:text="format24HourTimeStringFromInt(form.startsAt.time)"
class="gl-w-12 gl-pl-3"
class="gl-px-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
......@@ -220,99 +220,117 @@ export default {
<span class="gl-white-space-nowrap"> {{ format24HourTimeStringFromInt(time) }}</span>
</gl-dropdown-item>
</gl-dropdown>
<span class="gl-pl-5"> {{ schedule.timezone }} </span>
<span> {{ schedule.timezone }} </span>
</div>
</gl-form-group>
</div>
<div class="gl-w-fit-content">
<gl-toggle
v-model="endDateEnabled"
:label="$options.i18n.fields.endsOn.enableToggle"
label-position="left"
class="gl-mb-5"
/>
<gl-toggle
v-model="endDateEnabled"
:label="$options.i18n.fields.endsOn.enableToggle"
label-position="left"
class="gl-mb-5"
/>
<gl-card v-if="endDateEnabled" data-testid="rotation-ends-on">
<gl-form-group
:label="$options.i18n.fields.endsOn.title"
label-size="sm"
:invalid-feedback="$options.i18n.fields.endsOn.error"
<gl-card
v-if="endDateEnabled"
data-testid="rotation-ends-on"
class="gl-border-gray-400 gl-bg-gray-10 gl-w-fit-content"
>
<div class="gl-display-flex gl-align-items-center">
<gl-datepicker
class="gl-mr-3"
@input="$emit('update-rotation-form', { type: 'endsOn.date', value: $event })"
/>
<span> {{ __('at') }} </span>
<gl-dropdown
data-testid="rotation-end-time"
:text="format24HourTimeStringFromInt(form.endsOn.time)"
class="gl-w-12 gl-pl-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.endsOn.time === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'endsOn.time', value: time })"
<gl-form-group
:label="$options.i18n.fields.endsOn.title"
label-size="sm"
:invalid-feedback="$options.i18n.fields.endsOn.error"
class="gl-mb-0"
>
<div class="gl-display-flex gl-align-items-center">
<gl-datepicker
class="gl-mr-3"
@input="$emit('update-rotation-form', { type: 'endsOn.date', value: $event })"
/>
<span> {{ __('at') }} </span>
<gl-dropdown
data-testid="rotation-end-time"
:text="format24HourTimeStringFromInt(form.endsOn.time)"
class="gl-px-3"
>
<span class="gl-white-space-nowrap"> {{ format24HourTimeStringFromInt(time) }}</span>
</gl-dropdown-item>
</gl-dropdown>
<div class="gl-mx-5">{{ schedule.timezone }}</div>
</div>
</gl-form-group>
</gl-card>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.endsOn.time === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'endsOn.time', value: time })"
>
<span class="gl-white-space-nowrap">
{{ format24HourTimeStringFromInt(time) }}</span
>
</gl-dropdown-item>
</gl-dropdown>
<span>{{ schedule.timezone }}</span>
</div>
</gl-form-group>
</gl-card>
<gl-toggle
v-model="restrictToTimeEnabled"
data-testid="restricted-to-toggle"
:label="$options.i18n.fields.restrictToTime.enableToggle"
label-position="left"
class="gl-my-5"
/>
<gl-toggle
v-model="restrictToTimeEnabled"
data-testid="restricted-to-toggle"
:label="$options.i18n.fields.restrictToTime.enableToggle"
label-position="left"
class="gl-mt-5"
/>
<gl-card v-if="restrictToTimeEnabled" data-testid="restricted-to-time">
<gl-form-group
:label="$options.i18n.fields.restrictToTime.title"
label-size="sm"
:invalid-feedback="$options.i18n.fields.endsOn.error"
<gl-card
v-if="restrictToTimeEnabled"
data-testid="restricted-to-time"
class="gl-mt-5 gl-border-gray-400 gl-bg-gray-10"
>
<div class="gl-display-flex gl-align-items-center">
<span> {{ __('From') }} </span>
<gl-dropdown
data-testid="restricted-from"
:text="format24HourTimeStringFromInt(form.restrictedTo.from)"
class="gl-px-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.restrictedTo.from === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'restrictedTo.from', value: time })"
<gl-form-group
:label="$options.i18n.fields.restrictToTime.title"
label-size="sm"
:invalid-feedback="$options.i18n.fields.endsOn.error"
class="gl-mb-0"
>
<div class="gl-display-flex gl-align-items-center">
<span> {{ __('From') }} </span>
<gl-dropdown
data-testid="restricted-from"
:text="format24HourTimeStringFromInt(form.restrictedTo.from)"
class="gl-px-3"
>
<span class="gl-white-space-nowrap"> {{ format24HourTimeStringFromInt(time) }}</span>
</gl-dropdown-item>
</gl-dropdown>
<span> {{ __('To') }} </span>
<gl-dropdown
data-testid="restricted-to"
:text="format24HourTimeStringFromInt(form.restrictedTo.to)"
class="gl-px-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.restrictedTo.to === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'restrictedTo.to', value: time })"
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.restrictedTo.from === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'restrictedTo.from', value: time })"
>
<span class="gl-white-space-nowrap">
{{ format24HourTimeStringFromInt(time) }}</span
>
</gl-dropdown-item>
</gl-dropdown>
<span> {{ __('To') }} </span>
<gl-dropdown
data-testid="restricted-to"
:text="format24HourTimeStringFromInt(form.restrictedTo.to)"
class="gl-px-3"
>
<span class="gl-white-space-nowrap"> {{ format24HourTimeStringFromInt(time) }}</span>
</gl-dropdown-item>
</gl-dropdown>
</div>
</gl-form-group>
</gl-card>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.restrictedTo.to === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'restrictedTo.to', value: time })"
>
<span class="gl-white-space-nowrap">
{{ format24HourTimeStringFromInt(time) }}</span
>
</gl-dropdown-item>
</gl-dropdown>
<span>{{ schedule.timezone }} </span>
</div>
</gl-form-group>
</gl-card>
</div>
</gl-form>
</template>
......@@ -266,7 +266,7 @@ RSpec.describe 'Epic show', :js do
end
end
describe 'epic actions' do
describe 'epic actions', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297505' do
shared_examples 'epic closed' do |selector|
it 'can close an epic' do
expect(find('.status-box')).to have_content 'Open'
......
......@@ -20790,9 +20790,6 @@ msgstr ""
msgid "Pipelines|More Information"
msgstr ""
msgid "Pipelines|No CI file found in this repository, please add one."
msgstr ""
msgid "Pipelines|No triggers have been created yet. Add one using the form above."
msgstr ""
......@@ -20805,9 +20802,6 @@ msgstr ""
msgid "Pipelines|Project cache successfully reset."
msgstr ""
msgid "Pipelines|Repository does not have a default branch, please set one."
msgstr ""
msgid "Pipelines|Revoke"
msgstr ""
......@@ -20829,6 +20823,9 @@ msgstr ""
msgid "Pipelines|There are currently no pipelines."
msgstr ""
msgid "Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again."
msgstr ""
msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
msgstr ""
......
......@@ -41,7 +41,7 @@ RSpec.describe 'issue state', :js do
end
end
describe 'when open' do
describe 'when open', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297348' do
context 'when clicking the top `Close issue` button', :aggregate_failures do
let(:open_issue) { create(:issue, project: project) }
......@@ -63,7 +63,7 @@ RSpec.describe 'issue state', :js do
end
end
describe 'when closed' do
describe 'when closed', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/297201' do
context 'when clicking the top `Reopen issue` button', :aggregate_failures do
let(:closed_issue) { create(:issue, project: project, state: 'closed') }
......
import { languages } from 'monaco-editor';
import { TEST_HOST } from 'helpers/test_constants';
import EditorLite from '~/editor/editor_lite';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
......@@ -9,6 +10,7 @@ describe('~/editor/editor_ci_config_ext', () => {
let editor;
let instance;
let editorEl;
let originalGitlabUrl;
const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => {
setFixtures('<div id="editor"></div>');
......@@ -22,6 +24,15 @@ describe('~/editor/editor_ci_config_ext', () => {
instance.use(new CiSchemaExtension());
};
beforeAll(() => {
originalGitlabUrl = gon.gitlab_url;
gon.gitlab_url = TEST_HOST;
});
afterAll(() => {
gon.gitlab_url = originalGitlabUrl;
});
beforeEach(() => {
createMockEditor();
});
......@@ -73,7 +84,7 @@ describe('~/editor/editor_ci_config_ext', () => {
});
expect(getConfiguredYmlSchema()).toEqual({
uri: `/${mockProjectNamespace}/${mockProjectPath}/-/schema/${mockRef}/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
uri: `${TEST_HOST}/${mockProjectNamespace}/${mockProjectPath}/-/schema/${mockRef}/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
fileMatch: [defaultBlobPath],
});
});
......@@ -87,7 +98,7 @@ describe('~/editor/editor_ci_config_ext', () => {
});
expect(getConfiguredYmlSchema()).toEqual({
uri: `/${mockProjectNamespace}/${mockProjectPath}/-/schema/master/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
uri: `${TEST_HOST}/${mockProjectNamespace}/${mockProjectPath}/-/schema/master/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
fileMatch: ['another-ci-filename.yml'],
});
});
......
......@@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import VueApollo from 'vue-apollo';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import { objectToQuery, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility';
import {
mockCiConfigPath,
......@@ -414,58 +415,81 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
});
it('no error is shown when data is set', async () => {
createComponentWithApollo();
describe('when file exists', () => {
beforeEach(async () => {
createComponentWithApollo();
await waitForPromises();
await waitForPromises();
});
expect(findAlert().exists()).toBe(false);
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
});
it('shows editor and commit form', () => {
expect(findEditorLite().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(true);
});
it('ci config query is called with correct variables', async () => {
createComponentWithApollo();
it('no error is shown when data is set', async () => {
expect(findAlert().exists()).toBe(false);
expect(findEditorLite().attributes('value')).toBe(mockCiYml);
});
await waitForPromises();
it('ci config query is called with correct variables', async () => {
createComponentWithApollo();
expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml,
projectPath: mockProjectFullPath,
await waitForPromises();
expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml,
projectPath: mockProjectFullPath,
});
});
});
it('shows a 404 error message', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: 404,
},
describe('when no file exists', () => {
const expectedAlertMsg =
'There is no .gitlab-ci.yml file in this repository, please add one and visit the Pipeline Editor again.';
it('does not show editor or commit form', async () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
createComponentWithApollo();
await waitForPromises();
expect(findEditorLite().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(false);
});
createComponentWithApollo();
await waitForPromises();
it('shows a 404 error message', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.NOT_FOUND,
},
});
createComponentWithApollo();
expect(findAlert().text()).toBe('No CI file found in this repository, please add one.');
});
await waitForPromises();
it('shows a 400 error message', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: 400,
},
expect(findAlert().text()).toBe(expectedAlertMsg);
});
createComponentWithApollo();
await waitForPromises();
it('shows a 400 error message', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.BAD_REQUEST,
},
});
createComponentWithApollo();
expect(findAlert().text()).toBe('Repository does not have a default branch, please set one.');
});
await waitForPromises();
it('shows a unkown error message', async () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
createComponentWithApollo();
await waitForPromises();
expect(findAlert().text()).toBe(expectedAlertMsg);
});
it('shows a unkown error message', async () => {
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
createComponentWithApollo();
await waitForPromises();
expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.');
expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.');
});
});
});
});
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