Commit cd753a8c authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee

parents bf079a07 db38f9ae
......@@ -35,4 +35,10 @@ if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
plugins.push('babel-plugin-rewire');
}
// Jest is running in node environment
if (BABEL_ENV === 'jest') {
plugins.push('transform-es2015-modules-commonjs');
plugins.push('dynamic-import-node');
}
module.exports = { presets, plugins };
......@@ -2,6 +2,7 @@
/config/
/builds/
/coverage/
/coverage-frontend/
/coverage-javascript/
/node_modules/
/public/
......
......@@ -78,5 +78,5 @@ eslint-report.html
/plugins/*
/.gitlab_pages_secret
package-lock.json
/junit_rspec.xml
/junit_karma.xml
/junit_*.xml
/coverage-frontend/
......@@ -877,6 +877,32 @@ karma:
reports:
junit: junit_karma.xml
jest:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
dependencies:
- compile-assets
- setup-test-env
script:
- scripts/gitaly-test-spawn
- date
- bundle exec rake karma:fixtures
- date
- yarn jest --ci --coverage
artifacts:
name: coverage-frontend
expire_in: 31d
when: always
paths:
- coverage-frontend/
- junit_jest.xml
reports:
junit: junit_jest.xml
cache:
key: jest
paths:
- tmp/jest/jest/
code_quality:
<<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
......
......@@ -14,6 +14,7 @@ const Api = {
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
projectRunnersPath: '/api/:version/projects/:id/runners',
mergeRequestsPath: '/api/:version/merge_requests',
groupLabelsPath: '/groups/:namespace_path/-/labels',
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
......@@ -126,6 +127,15 @@ const Api = {
return axios.get(url);
},
projectRunners(projectPath, config = {}) {
const url = Api.buildUrl(Api.projectRunnersPath).replace(
':id',
encodeURIComponent(projectPath),
);
return axios.get(url, config);
},
mergeRequests(params = {}) {
const url = Api.buildUrl(Api.mergeRequestsPath);
......
......@@ -147,8 +147,14 @@ export const scrollToLineIfNeededParallel = (_, line) => {
}
};
export const loadCollapsedDiff = ({ commit }, file) =>
axios.get(file.load_collapsed_diff_url).then(res => {
export const loadCollapsedDiff = ({ commit, getters }, file) =>
axios
.get(file.load_collapsed_diff_url, {
params: {
commit_id: getters.commitId,
},
})
.then(res => {
commit(types.ADD_COLLAPSED_DIFFS, {
file,
data: res.data,
......
......@@ -130,7 +130,7 @@ export default {
if (file.highlighted_diff_lines) {
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => {
if (lineCheck(line)) {
if (!line.discussions.some(({ id }) => discussion.id === id) && lineCheck(line)) {
return {
...line,
discussions: line.discussions.concat(discussion),
......@@ -150,11 +150,17 @@ export default {
return {
left: {
...line.left,
discussions: left ? line.left.discussions.concat(discussion) : [],
discussions:
left && !line.left.discussions.some(({ id }) => id === discussion.id)
? line.left.discussions.concat(discussion)
: (line.left && line.left.discussions) || [],
},
right: {
...line.right,
discussions: right && !left ? line.right.discussions.concat(discussion) : [],
discussions:
right && !left && !line.right.discussions.some(({ id }) => id === discussion.id)
? line.right.discussions.concat(discussion)
: (line.right && line.right.discussions) || [],
},
};
}
......
import Vue from 'vue';
import { mapActions } from 'vuex';
import _ from 'underscore';
import Translate from '~/vue_shared/translate';
import ide from './components/ide.vue';
import store from './stores';
......@@ -13,19 +14,19 @@ Vue.use(Translate);
*
* @param {Element} el - The element that will contain the IDE.
* @param {Object} options - Extra options for the IDE (Used by EE).
* @param {(e:Element) => Object} options.extraInitialData -
* Function that returns extra properties to seed initial data.
* @param {Component} options.rootComponent -
* Component that overrides the root component.
* @param {(store:Vuex.Store, el:Element) => Vuex.Store} options.extendStore -
* Function that receives the default store and returns an extended one.
*/
export function initIde(el, options = {}) {
if (!el) return null;
const { extraInitialData = () => ({}), rootComponent = ide } = options;
const { rootComponent = ide, extendStore = _.identity } = options;
return new Vue({
el,
store,
store: extendStore(store, el),
router,
created() {
this.setEmptyStateSvgs({
......@@ -41,7 +42,6 @@ export function initIde(el, options = {}) {
});
this.setInitialData({
clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled),
...extraInitialData(el),
});
},
methods: {
......
......@@ -16,7 +16,9 @@ const httpStatusCodes = {
IM_USED: 226,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
FORBIDDEN: 403,
NOT_FOUND: 404,
UNPROCESSABLE_ENTITY: 422,
};
export const successCodes = [
......
......@@ -19,3 +19,4 @@ $info: $blue-500;
$warning: $orange-500;
$danger: $red-500;
$zindex-modal-backdrop: 1040;
$nav-divider-margin-y: ($grid-size / 2);
......@@ -9,7 +9,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_workhorse_authorize]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
layout 'project'
......
......@@ -24,12 +24,9 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def render_diffs
@environment = @merge_request.environments_for(current_user).last
notes_grouped_by_path = renderable_notes.group_by { |note| note.position.file_path }
@diffs.diff_files.each do |diff_file|
notes = notes_grouped_by_path.fetch(diff_file.file_path, [])
notes.each { |note| diff_file.unfold_diff_lines(note.position) }
end
note_positions = renderable_notes.map(&:position).compact
@diffs.unfold_diff_files(note_positions)
@diffs.write_cache
......
# frozen_string_literal: true
module IdeHelper
def ide_data
{
"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
"pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
"promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'),
"ci-help-page-path" => help_page_path('ci/quick_start/README'),
"web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'),
"clientside-preview-enabled": Gitlab::CurrentSettings.current_application_settings.web_ide_clientside_preview_enabled.to_s
}
end
end
......@@ -114,7 +114,8 @@ module Ci
cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout,
error_message: 'Maximum job timeout has a value which could not be accepted'
validates :maximum_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 600,
......
......@@ -24,7 +24,7 @@ module ChronicDurationAttribute
end
end
validates virtual_attribute, allow_nil: true, duration: true
validates virtual_attribute, allow_nil: true, duration: { message: parameters[:error_message] }
end
alias_method :chronic_duration_attr, :chronic_duration_attr_writer
......
......@@ -404,7 +404,8 @@ class Project < ActiveRecord::Base
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
chronic_duration_attr :build_timeout_human_readable, :build_timeout,
default: 3600, error_message: 'Maximum job timeout has a value which could not be accepted'
validates :build_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 10.minutes,
......
......@@ -14,6 +14,10 @@ class DurationValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
ChronicDuration.parse(value)
rescue ChronicDuration::DurationParseError
if options[:message]
record.errors.add(:base, options[:message])
else
record.errors.add(attribute, "is not a correct duration")
end
end
end
- @body_class = 'ide-layout'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
#ide.ide-loading{ data: ide_data() }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
- @body_class = 'ide-layout'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
"pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
"promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'),
"ci-help-page-path" => help_page_path('ci/quick_start/README'),
"web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'),
"clientside-preview-enabled": Gitlab::CurrentSettings.current_application_settings.web_ide_clientside_preview_enabled.to_s } }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
= render 'ide/show'
- page_title import_in_progress_title
- @no_container = true
- @content_class = "limit-container-width" unless fluid_layout
.save-project-loader
.center
......
......@@ -29,7 +29,7 @@
= f.label :build_timeout_human_readable, _('Timeout'), class: 'label-bold'
= f.text_field :build_timeout_human_readable, class: 'form-control'
%p.form-text.text-muted
= _("Per job. If a job passes this threshold, it will be marked as failed")
= _('If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like "1 hour". Values without specification represent seconds.')
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
......
......@@ -45,7 +45,7 @@
= _('Maximum job timeout')
.col-sm-10
= f.text_field :maximum_timeout_human_readable, class: 'form-control'
.form-text.text-muted= _('This timeout will take precedence when lower than Project-defined timeout')
.form-text.text-muted= _('This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like "1 hour". Values without specification represent seconds.')
.form-group.row
= label_tag :tag_list, class: 'col-form-label col-sm-2' do
= _('Tags')
......
---
title: Use read_repository scope on read-only files API
merge_request: 23534
author:
type: fixed
---
title: Fixed diff files expanding not loading commit content
merge_request:
author:
type: fixed
---
title: Improve help and validation sections of maximum build timeout inputs
merge_request: 23586
author:
type: fixed
---
title: Change container width for project import
merge_request: 23318
author: George Tsiolis
type: fixed
---
title: Fixed duplicate discussions getting added to diff lines
merge_request:
author:
type: fixed
---
title: Avoid 500's when serializing legacy diff notes
merge_request: 23544
author:
type: fixed
---
title: Adjust divider margin to comply with design specs
merge_request: 23548
author:
type: changed
/* eslint-disable filenames/match-regex */
const reporters = ['default'];
if (process.env.CI) {
reporters.push([
'jest-junit',
{
output: './junit_jest.xml',
},
]);
}
// eslint-disable-next-line import/no-commonjs
module.exports = {
testMatch: ['<rootDir>/spec/frontend/**/*_spec.js'],
moduleNameMapper: {
'^~(.*)$': '<rootDir>/app/assets/javascripts$1',
},
collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/coverage-frontend/',
coverageReporters: ['json', 'lcov', 'text-summary', 'clover'],
cacheDirectory: '<rootDir>/tmp/cache/jest',
modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'],
reporters,
rootDir: '..', // necessary because this file is in the config/ subdirectory
};
......@@ -2,7 +2,7 @@
require 'yaml'
NO_CHANGELOG_LABELS = %w[backstage Documentation QA test].freeze
NO_CHANGELOG_LABELS = %w[backstage ci-build Documentation meta QA test].freeze
SEE_DOC = "See [the documentation](https://docs.gitlab.com/ce/development/changelog.html).".freeze
CREATE_CHANGELOG_MESSAGE = <<~MSG.freeze
You can create one with:
......
......@@ -2834,6 +2834,7 @@ ActiveRecord::Schema.define(version: 20181204135932) do
t.integer "merge_request_notes_filter", limit: 2, default: 0, null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "epics_sort"
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
end
......
......@@ -4,6 +4,16 @@
**Create, read, update and delete repository files using this API**
The different scopes available using [personal access tokens](../user/profile/personal_access_tokens.md) are depicted
in the following table.
| Scope | Description |
| ----- | ----------- |
| `read_repository` | Allows read-access to the repository files. |
| `api` | Allows read-write access to the repository files. |
> `read_repository` scope was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23534) in GitLab 11.6.
## Get file from repository
Allows you to receive information about file in repository like name, size,
......
......@@ -57,12 +57,14 @@ are very appreciative of the work done by translators and proofreaders!
- Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang)
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Ji Hun Oh - [GitLab](https://gitlab.com/Baw-Appie), [Crowdin](https://crowdin.com/profile/BawAppie)
- Jeongwhan Choi - [GitLab](https://gitlab.com/jeongwhanchoi), [Crowdin](https://crowdin.com/profile/jeongwhanchoi)
- Mongolian
- Proofreaders needed.
- Norwegian Bokmal
- Proofreaders needed.
- Polish
- Filip Mech - [GitLab](https://gitlab.com/mehenz), [Crowdin](https://crowdin.com/profile/mehenz)
- Maksymilian Roman - [GitLab](https://gitlab.com/villaincandle), [Crowdin](https://crowdin.com/profile/villaincandle)
- Portuguese
- Proofreaders needed.
- Portuguese, Brazilian
......
......@@ -6,9 +6,15 @@ Tests relevant for frontend development can be found at two places:
- [frontend unit tests](#frontend-unit-tests)
- [frontend component tests](#frontend-component-tests)
- [frontend integration tests](#frontend-integration-tests)
- `spec/frontend/` which are run by Jest and contain
- [frontend unit tests](#frontend-unit-tests)
- [frontend component tests](#frontend-component-tests)
- [frontend integration tests](#frontend-integration-tests)
- `spec/features/` which are run by RSpec and contain
- [feature tests](#feature-tests)
All tests in `spec/javascripts/` will eventually be migrated to `spec/frontend/` (see also [#53757]).
In addition there were feature tests in `features/` run by Spinach in the past.
These have been removed from our codebase in May 2018 ([#23036](https://gitlab.com/gitlab-org/gitlab-ce/issues/23036)).
......@@ -17,6 +23,8 @@ See also:
- [old testing guide](../../testing_guide/frontend_testing.html)
- [notes on testing Vue components](../../fe_guide/vue.html#testing-vue-components)
[#53757]: https://gitlab.com/gitlab-org/gitlab-ce/issues/53757
## Frontend unit tests
Unit tests are on the lowest abstraction level and typically test functionality that is not directly perceivable by a user.
......
......@@ -7,9 +7,10 @@ GitLab provides official Docker images to allowing you to easily take advantage
## Omnibus GitLab based images
GitLab maintains a set of [official Docker images](https://hub.docker.com/r/gitlab) based on our [Omnibus GitLab package](https://docs.gitlab.com/omnibus/README.html). These images include:
* [GitLab Community Edition](https://hub.docker.com/r/gitlab/gitlab-ce/)
* [GitLab Enterprise Edition](https://hub.docker.com/r/gitlab/gitlab-ee/)
* [GitLab Runner](https://hub.docker.com/r/gitlab/gitlab-runner/)
- [GitLab Community Edition](https://hub.docker.com/r/gitlab/gitlab-ce/).
- [GitLab Enterprise Edition](https://hub.docker.com/r/gitlab/gitlab-ee/).
- [GitLab Runner](https://hub.docker.com/r/gitlab/gitlab-runner/).
A [complete usage guide](https://docs.gitlab.com/omnibus/docker/) to these images is available, as well as the [Dockerfile used for building the images](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker).
......
......@@ -6,7 +6,7 @@ password to login, they'll be prompted for a code generated by an application on
their phone.
You can read more about it here:
[Two-factor Authentication (2FA)](../profile/two_factor_authentication.md)
[Two-factor Authentication (2FA)](../user/profile/account/two_factor_authentication.md)
## Enforcing 2FA for all users
......
......@@ -123,6 +123,8 @@ You can also sort epics list by:
- **Start date**
- **Due date**
Each option contains a button that can toggle the order between **ascending** and **descending**. The sort option and order will be persisted to be used wherever epics are browsed including the [roadmap](../roadmap/index.md).
![epics sort](img/epics_sort.png)
## Permissions
......
......@@ -13,6 +13,8 @@ Epics in the view can be sorted by:
- **Start date**
- **Due date**
Each option contains a button that can toggle the order between **ascending** and **descending**. The sort option and order will be persisted to be used wherever epics are browsed including the [epics list view](../epics/index.md).
![roadmap view](img/roadmap_view.png)
## Timeline duration
......
<script>
import { mapState } from 'vuex';
import { __ } from '~/locale';
import RightPane from '~/ide/components/panes/right.vue';
import TerminalView from '../terminal/view.vue';
......@@ -8,17 +9,14 @@ export default {
components: {
RightPane,
},
data() {
return {
// this will come from Vuex store in https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
isTerminalEnabled: false,
};
},
computed: {
...mapState('terminal', {
isTerminalVisible: 'isVisible',
}),
extensionTabs() {
return [
{
show: this.isTerminalEnabled,
show: this.isTerminalVisible,
title: __('Terminal'),
views: [{ name: 'terminal', keepAlive: true, component: TerminalView }],
icon: 'terminal',
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlLoadingIcon,
},
props: {
isLoading: {
type: Boolean,
required: false,
default: true,
},
isValid: {
type: Boolean,
required: false,
default: false,
},
message: {
type: String,
required: false,
default: '',
},
helpPath: {
type: String,
required: false,
default: '',
},
illustrationPath: {
type: String,
required: false,
default: '',
},
},
methods: {
onStart() {
this.$emit('start');
},
},
};
</script>
<template>
<div class="text-center">
<div v-if="illustrationPath" class="svg-content svg-130"><img :src="illustrationPath" /></div>
<h4>{{ __('Web Terminal') }}</h4>
<gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default" />
<template v-else>
<p>{{ __('Run tests against your code live using the Web Terminal') }}</p>
<div class="bs-callout text-left">The Web Terminal is coming soon. Stay tuned!</div>
<p>
<button :disabled="!isValid" class="btn btn-info" type="button" @click="onStart">
{{ __('Start Web Terminal') }}
</button>
</p>
<div v-if="!isValid && message" class="bs-callout text-left" v-html="message"></div>
<p v-else>
<a
v-if="helpPath"
:href="helpPath"
target="_blank"
v-text="__('Learn more about Web Terminal')"
></a>
</p>
</template>
</div>
</template>
<script>
import { mapState } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import EmptyState from './empty_state.vue';
export default {
......@@ -7,15 +7,32 @@ export default {
EmptyState,
},
computed: {
...mapState(['emptyStateSvgPath']),
...mapState('terminal', ['isShowSplash', 'paths']),
...mapGetters('terminal', ['allCheck']),
},
methods: {
...mapActions('terminal', ['hideSplash']),
start() {
this.hideSplash();
},
},
};
</script>
<template>
<div class="h-100">
<div class="h-100 d-flex flex-column justify-content-center">
<empty-state :illustration-path="emptyStateSvgPath" />
<div v-if="isShowSplash" class="h-100 d-flex flex-column justify-content-center">
<empty-state
:is-loading="allCheck.isLoading"
:is-valid="allCheck.isValid"
:message="allCheck.message"
:help-path="paths.webTerminalHelpPath"
:illustration-path="paths.webTerminalSvgPath"
@start="start();"
/>
</div>
<template v-else>
<h5>{{ __('Web Terminal') }}</h5>
</template>
</div>
</template>
export const CHECK_CONFIG = 'config';
export const CHECK_RUNNERS = 'runners';
export const RETRY_RUNNERS_INTERVAL = 10000;
import * as mutationTypes from '~/ide/stores/mutation_types';
import terminalModule from './modules/terminal';
function getPathsFromData(el) {
return {
ciYamlHelpPath: el.dataset.eeCiYamlHelpPath,
ciRunnersHelpPath: el.dataset.eeCiRunnersHelpPath,
webTerminalHelpPath: el.dataset.eeWebTerminalHelpPath,
webTerminalSvgPath: el.dataset.eeWebTerminalSvgPath,
};
}
export default (store, el) => {
store.registerModule('terminal', terminalModule());
store.dispatch('terminal/setPaths', getPathsFromData(el));
store.subscribe(({ type }) => {
if (type === mutationTypes.SET_BRANCH_WORKING_REFERENCE) {
store.dispatch('terminal/init');
}
});
return store;
};
import Api from '~/api';
import httpStatus from '~/lib/utils/http_status';
import * as types from '../mutation_types';
import * as messages from '../messages';
import { CHECK_CONFIG, CHECK_RUNNERS, RETRY_RUNNERS_INTERVAL } from '../../../../constants';
export const requestConfigCheck = ({ commit }) => {
commit(types.REQUEST_CHECK, CHECK_CONFIG);
};
export const receiveConfigCheckSuccess = ({ commit }) => {
commit(types.SET_VISIBLE, true);
commit(types.RECEIVE_CHECK_SUCCESS, CHECK_CONFIG);
};
export const receiveConfigCheckError = ({ commit, state }, e) => {
const { status } = e.response;
const { paths } = state;
const isVisible = status !== httpStatus.FORBIDDEN && status !== httpStatus.NOT_FOUND;
commit(types.SET_VISIBLE, isVisible);
const message = messages.configCheckError(status, paths.ciYamlHelpPath);
commit(types.RECEIVE_CHECK_ERROR, { type: CHECK_CONFIG, message });
};
export const fetchConfigCheck = ({ dispatch }) => {
dispatch('requestConfigCheck');
// This will use a real endpoint in https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
Promise.resolve({})
.then(() => {
dispatch('receiveConfigCheckSuccess');
})
.catch(e => {
dispatch('receiveConfigCheckError', e);
});
};
export const requestRunnersCheck = ({ commit }) => {
commit(types.REQUEST_CHECK, CHECK_RUNNERS);
};
export const receiveRunnersCheckSuccess = ({ commit, dispatch, state }, data) => {
if (data.length) {
commit(types.RECEIVE_CHECK_SUCCESS, CHECK_RUNNERS);
} else {
const { paths } = state;
commit(types.RECEIVE_CHECK_ERROR, {
type: CHECK_RUNNERS,
message: messages.runnersCheckEmpty(paths.ciRunnersHelpPath),
});
dispatch('retryRunnersCheck');
}
};
export const receiveRunnersCheckError = ({ commit }) => {
commit(types.RECEIVE_CHECK_ERROR, {
type: CHECK_RUNNERS,
message: messages.UNEXPECTED_ERROR_RUNNERS,
});
};
export const retryRunnersCheck = ({ dispatch, state }) => {
// if the overall check has failed, don't worry about retrying
const check = state.checks[CHECK_CONFIG];
if (!check.isLoading && !check.isValid) {
return;
}
setTimeout(() => {
dispatch('fetchRunnersCheck', { background: true });
}, RETRY_RUNNERS_INTERVAL);
};
export const fetchRunnersCheck = ({ dispatch, rootGetters }, options = {}) => {
const { background = false } = options;
if (!background) {
dispatch('requestRunnersCheck');
}
const { currentProject } = rootGetters;
Api.projectRunners(currentProject.id, { params: { scope: 'active' } })
.then(({ data }) => {
dispatch('receiveRunnersCheckSuccess', data);
})
.catch(e => {
dispatch('receiveRunnersCheckError', e);
});
};
export * from './setup';
export * from './checks';
export default () => {};
import * as types from '../mutation_types';
// This will be used in https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
// export const init = ({ dispatch }) => {
// dispatch('fetchConfigCheck');
// dispatch('fetchRunnersCheck');
// };
export const init = () => {};
export const hideSplash = ({ commit }) => {
commit(types.HIDE_SPLASH);
};
export const setPaths = ({ commit }, paths) => {
commit(types.SET_PATHS, paths);
};
export const allCheck = state => {
const checks = Object.values(state.checks);
if (checks.some(check => check.isLoading)) {
return { isLoading: true };
}
const invalidCheck = checks.find(check => !check.isValid);
const isValid = !invalidCheck;
const message = !invalidCheck ? '' : invalidCheck.message;
return {
isLoading: false,
isValid,
message,
};
};
export default () => {};
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
export default () => ({
namespaced: true,
actions,
getters,
mutations,
state: state(),
});
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
export const UNEXPECTED_ERROR_CONFIG = __(
'An unexpected error occurred while checking the project environment.',
);
export const UNEXPECTED_ERROR_RUNNERS = __(
'An unexpected error occurred while checking the project runners.',
);
export const EMPTY_RUNNERS = __(
'Configure GitLab runners to start using Web Terminal. %{helpStart}Learn more.%{helpEnd}',
);
export const ERROR_CONFIG = __(
'Configure a <code>.gitlab-webide.yml</code> file in the <code>.gitlab</code> directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}',
);
export const ERROR_PERMISSION = __(
'You do not have permission to run the Web Terminal. Please contact a project administrator.',
);
export const configCheckError = (status, helpUrl) => {
if (status === httpStatus.UNPROCESSABLE_ENTITY) {
return sprintf(
ERROR_CONFIG,
{
helpStart: `<a href="${_.escape(helpUrl)}" target="_blank">`,
helpEnd: '</a>',
},
false,
);
} else if (status === httpStatus.FORBIDDEN) {
return ERROR_PERMISSION;
}
return UNEXPECTED_ERROR_CONFIG;
};
export const runnersCheckEmpty = helpUrl =>
sprintf(
EMPTY_RUNNERS,
{
helpStart: `<a href="${_.escape(helpUrl)}" target="_blank">`,
helpEnd: '</a>',
},
false,
);
export const SET_VISIBLE = 'SET_VISIBLE';
export const HIDE_SPLASH = 'HIDE_SPLASH';
export const SET_PATHS = 'SET_PATHS';
export const REQUEST_CHECK = 'REQUEST_CHECK';
export const RECEIVE_CHECK_SUCCESS = 'RECEIVE_CHECK_SUCCESS';
export const RECEIVE_CHECK_ERROR = 'RECEIVE_CHECK_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_VISIBLE](state, isVisible) {
Object.assign(state, {
isVisible,
});
},
[types.HIDE_SPLASH](state) {
Object.assign(state, {
isShowSplash: false,
});
},
[types.SET_PATHS](state, paths) {
Object.assign(state, {
paths,
});
},
[types.REQUEST_CHECK](state, type) {
Object.assign(state.checks, {
[type]: {
isLoading: true,
},
});
},
[types.RECEIVE_CHECK_ERROR](state, { type, message }) {
Object.assign(state.checks, {
[type]: {
isLoading: false,
isValid: false,
message,
},
});
},
[types.RECEIVE_CHECK_SUCCESS](state, type) {
Object.assign(state.checks, {
[type]: {
isLoading: false,
isValid: true,
message: null,
},
});
},
};
import { CHECK_CONFIG, CHECK_RUNNERS } from '../../../constants';
export default () => ({
checks: {
[CHECK_CONFIG]: { isLoading: true },
[CHECK_RUNNERS]: { isLoading: true },
},
isVisible: false,
isShowSplash: true,
paths: {},
});
import { startIde } from '~/ide/index';
import EEIde from 'ee/ide/components/ide.vue';
function extraInitialData() {
// This is empty now, but it will be used in: https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
return {};
}
import extendStore from 'ee/ide/stores/extend';
startIde({
extraInitialData,
extendStore,
rootComponent: EEIde,
});
......@@ -114,7 +114,7 @@ export default {
RelatedIssuesService.remove(issueToRemove.relation_path)
.then(res => res.json())
.then(data => {
this.store.setRelatedIssues(data.issues);
this.store.setRelatedIssues(data.issuables);
})
.catch(res => {
if (res && res.status !== 404) {
......@@ -142,7 +142,7 @@ export default {
.then(data => {
// We could potentially lose some pending issues in the interim here
this.store.setPendingReferences([]);
this.store.setRelatedIssues(data.issues);
this.store.setRelatedIssues(data.issuables);
this.isSubmitting = false;
// Close the form on submission
......
......@@ -16,7 +16,7 @@ class RelatedIssuesService {
return this.relatedIssuesResource.save(
{},
{
issue_references: newIssueReferences,
issuable_references: newIssueReferences,
},
);
}
......
......@@ -4,9 +4,21 @@ $details-cell-width: 320px;
$border-style: 1px solid $border-gray-normal;
$roadmap-gradient-dark-gray: rgba(0, 0, 0, 0.15);
$roadmap-gradient-gray: rgba(255, 255, 255, 0.001);
$scroll-top-gradient: linear-gradient(to bottom, $roadmap-gradient-dark-gray 0%, $roadmap-gradient-gray 100%);
$scroll-bottom-gradient: linear-gradient(to bottom, $roadmap-gradient-gray 0%, $roadmap-gradient-dark-gray 100%);
$column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%, $roadmap-gradient-gray 100%);
$scroll-top-gradient: linear-gradient(
to bottom,
$roadmap-gradient-dark-gray 0%,
$roadmap-gradient-gray 100%
);
$scroll-bottom-gradient: linear-gradient(
to bottom,
$roadmap-gradient-gray 0%,
$roadmap-gradient-dark-gray 100%
);
$column-right-gradient: linear-gradient(
to right,
$roadmap-gradient-dark-gray 0%,
$roadmap-gradient-gray 100%
);
@mixin roadmap-scroll-mixin {
height: $grid-size;
......@@ -14,6 +26,39 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
pointer-events: none;
}
.epics-details-filters {
.btn-group {
.dropdown-toggle {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-sort-direction {
border-left: 0;
&:hover {
border-color: $gray-darkest;
}
}
@include media-breakpoint-down(xs) {
display: flex;
.dropdown-menu-sort {
// This is a hack to fix dropdown alignment in small screens
// where Bootstrap applies inline `transform: translate3d(...)`
// and since our dropdown button has sort direction button
// present, alignment needs to compensate for that space
// without which it appears shifted towards left.
//
// One more approach is to override `transform` using `!important`
// but that too involves using magic number
margin-left: 27px;
}
}
}
}
.epics-roadmap-filters {
.epics-details-filters {
.btn-roadmap-preset {
......@@ -52,7 +97,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.roadmap-timeline-section .timeline-header-blank::after,
.epics-list-section .epic-details-cell::after {
content: '';
content: "";
position: absolute;
top: 0;
right: -$grid-size;
......@@ -136,7 +181,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
.today-bar::before {
content: '';
content: "";
position: absolute;
top: -2px;
left: -3px;
......@@ -150,7 +195,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
&.scroll-top-shadow .timeline-header-blank::before {
@include roadmap-scroll-mixin;
content: '';
content: "";
position: absolute;
left: 0;
bottom: -$grid-size;
......@@ -256,7 +301,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
&.start-date-outside::before,
&.end-date-outside::after {
content: '';
content: "";
position: absolute;
top: 0;
height: 100%;
......@@ -269,11 +314,21 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
}
&.start-date-undefined {
background: linear-gradient(to right, $roadmap-gradient-gray 0%, $blue-200 50%, $blue-500 100%);
background: linear-gradient(
to right,
$roadmap-gradient-gray 0%,
$blue-200 50%,
$blue-500 100%
);
}
&.end-date-undefined {
background: linear-gradient(to right, $blue-500 0%, $blue-200 50%, $roadmap-gradient-gray 100%);
background: linear-gradient(
to right,
$blue-500 0%,
$blue-200 50%,
$roadmap-gradient-gray 100%
);
}
&.start-date-outside {
......
# frozen_string_literal: true
module EpicRelations
extend ActiveSupport::Concern
include IssuableLinks
included do
skip_before_action :authorize_destroy_issuable!
skip_before_action :authorize_create_epic!
skip_before_action :authorize_update_issuable!
before_action :authorize_admin_epic!, only: [:create, :destroy, :update]
end
def authorize_admin_epic!
render_403 unless can?(current_user, :admin_epic, epic)
end
end
......@@ -11,11 +11,13 @@ module EpicsActions
end
def default_sort_order
sort_value_end_date
sort_value_recently_created
end
def update_cookie_value(value)
case value
when 'created_asc' then sort_value_oldest_created
when 'created_desc' then sort_value_recently_created
when 'start_date_asc' then sort_value_start_date
when 'end_date_asc' then sort_value_end_date
else
......
......@@ -2,25 +2,33 @@
module IssuableLinks
def index
render json: issues
render json: issuables
end
def create
result = create_service.execute
render json: { message: result[:message], issues: issues }, status: result[:http_status]
render json: { message: result[:message], issuables: issuables }, status: result[:http_status]
end
def destroy
result = destroy_service.execute
render json: { issues: issues }, status: result[:http_status]
render json: { issuables: issuables }, status: result[:http_status]
end
private
def issuables
list_service.execute
end
def list_service
raise NotImplementedError
end
def create_params
params.slice(:issue_references)
params.slice(:issuable_references)
end
def create_service
......
# frozen_string_literal: true
class Groups::EpicIssuesController < Groups::EpicsController
include IssuableLinks
include EpicRelations
skip_before_action :authorize_destroy_issuable!
skip_before_action :authorize_create_epic!
skip_before_action :authorize_update_issuable!
before_action :authorize_admin_epic!, only: [:create, :destroy, :update]
before_action :authorize_issue_link_association!, only: [:destroy, :update]
def update
......@@ -26,12 +21,8 @@ class Groups::EpicIssuesController < Groups::EpicsController
EpicIssues::DestroyService.new(link, current_user)
end
def issues
EpicIssues::ListService.new(epic, current_user).execute
end
def authorize_admin_epic!
render_403 unless can?(current_user, :admin_epic, epic)
def list_service
EpicIssues::ListService.new(epic, current_user)
end
def authorize_issue_link_association!
......
......@@ -95,6 +95,10 @@ class Groups::EpicsController < Groups::ApplicationController
EpicsFinder
end
def issuable_sorting_field
:epics_sort
end
def preload_for_collection
@preload_for_collection ||= [:group, :author]
end
......
......@@ -9,14 +9,20 @@ module Groups
before_action :group
before_action :persist_roadmap_layout, only: [:show]
def show
# show roadmap for a group
@sort = set_sort_order_from_cookie || default_sort_order
def show
# Used to persist the order and show the correct sorting dropdown on UI.
@sort = set_sort_order
@epics_count = EpicsFinder.new(current_user, group_id: @group.id).execute.count
end
private
def issuable_sorting_field
:epics_sort
end
def persist_roadmap_layout
return unless current_user
......
......@@ -9,10 +9,6 @@ module Projects
private
def issues
IssueLinks::ListService.new(issue, current_user).execute
end
def authorize_admin_issue_link!
render_403 unless can?(current_user, :admin_issue_link, @project)
end
......@@ -29,6 +25,10 @@ module Projects
end
# rubocop: enable CodeReuse/ActiveRecord
def list_service
IssueLinks::ListService.new(issue, current_user)
end
def create_service
IssueLinks::CreateService.new(issue, current_user, create_params)
end
......
# frozen_string_literal: true
class Projects::WebIdeTerminalsController < Projects::ApplicationController
before_action :authenticate_user!
before_action :build, except: [:check_config, :create]
before_action :authorize_create_web_ide_terminal!
before_action :authorize_read_web_ide_terminal!, except: [:check_config, :create]
before_action :authorize_update_web_ide_terminal!, only: [:cancel, :retry]
def check_config
return respond_422 unless branch_sha
result = ::Ci::WebIdeConfigService.new(project, current_user, sha: branch_sha).execute
if result[:status] == :success
head :ok
else
respond_422
end
end
def show
render_terminal(build)
end
def create
result = ::Ci::CreateWebIdeTerminalService.new(project,
current_user,
ref: params[:branch])
.execute
if result[:status] == :error
render status: :bad_request, json: result[:message]
else
pipeline = result[:pipeline]
current_build = pipeline.builds.last
if current_build
render_terminal(current_build)
else
render status: :bad_request, json: pipeline.errors.full_messages
end
end
end
def cancel
return respond_422 unless build.cancelable?
build.cancel
head :ok
end
def retry
return respond_422 unless build.retryable?
new_build = Ci::Build.retry(build, current_user)
render_terminal(new_build)
end
private
def authorize_create_web_ide_terminal!
return access_denied! unless can?(current_user, :create_web_ide_terminal, project)
end
def authorize_read_web_ide_terminal!
authorize_build_ability!(:read_web_ide_terminal)
end
def authorize_update_web_ide_terminal!
authorize_build_ability!(:update_web_ide_terminal)
end
def authorize_build_ability!(ability)
return access_denied! unless can?(current_user, ability, build)
end
def build
@build ||= project.builds.find(params[:id])
end
def branch_sha
return unless params[:branch].present?
project.commit(params[:branch])&.id
end
def render_terminal(current_build)
render json: WebIdeTerminalSerializer
.new(project: project, current_user: current_user)
.represent(current_build)
end
end
# frozen_string_literal: true
module EE
module IdeHelper
extend ::Gitlab::Utils::Override
override :ide_data
def ide_data
super.merge({
"ee-web-terminal-svg-path" => image_path('illustrations/web-ide_promotion.svg'),
"ee-ci-yaml-help-path" => help_page_path('ci/yaml/README.md'),
"ee-ci-runners-help-path" => help_page_path('ci/runners/README.md'),
"ee-web-terminal-help-path" => help_page_path('user/project/web_ide/index.md', anchor: 'client-side-evaluation')
})
end
end
end
::IdeHelper.prepend(::EE::IdeHelper)
......@@ -14,6 +14,44 @@ module EE
}.merge(super)
end
def epics_sort_options_hash
{
sort_value_created_date => sort_title_created_date,
sort_value_oldest_created => sort_title_created_date,
sort_value_recently_created => sort_title_created_date,
sort_value_oldest_updated => sort_title_recently_updated,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_start_date_later => sort_title_start_date,
sort_value_start_date_soon => sort_title_start_date,
sort_value_end_date_later => sort_title_end_date,
sort_value_end_date => sort_title_end_date
}
end
# This method is used to find the opposite ordering parameter for the sort button in the UI.
# Hash key is the descending sorting order and the sort value is the opposite of it for the same field.
# For example: created_at_asc => created_at_desc
def epics_ordering_options_hash
{
sort_value_oldest_created => sort_value_recently_created,
sort_value_oldest_updated => sort_value_recently_updated,
sort_value_start_date_soon => sort_value_start_date_later,
sort_value_end_date => sort_value_end_date_later
}
end
# Creates a button with the opposite ordering for the current field in UI.
def sort_order_button(sort)
opposite_sorting_param = epics_ordering_options_hash[sort] || epics_ordering_options_hash.key(sort)
sort_icon = sort.end_with?('desc') ? 'sort-highest' : 'sort-lowest'
link_to sprite_icon(sort_icon, size: 16),
page_filter_path(sort: opposite_sorting_param, label: true),
class: "btn btn-default has-tooltip qa-reverse-sort btn-sort-direction",
title: _("Sort direction")
end
def sort_title_start_date
s_('SortOptions|Start date')
end
......@@ -42,6 +80,10 @@ module EE
'end_date_asc'
end
def sort_value_end_date_later
'end_date_desc'
end
def sort_value_less_weight
'weight_asc'
end
......
......@@ -56,6 +56,14 @@ module EE
reorder(::Gitlab::Database.nulls_last_order('end_date'), 'id DESC')
end
scope :order_end_date_desc, -> do
reorder(::Gitlab::Database.nulls_last_order('end_date', 'DESC'), 'id DESC')
end
scope :order_start_date_desc, -> do
reorder(::Gitlab::Database.nulls_last_order('start_date', 'DESC'), 'id DESC')
end
def etag_caching_enabled?
true
end
......@@ -104,7 +112,9 @@ module EE
case method.to_s
when 'start_or_end_date' then order_start_or_end_date_asc
when 'start_date_asc' then order_start_date_asc
when 'start_date_desc' then order_start_date_desc
when 'end_date_asc' then order_end_date_asc
when 'end_date_desc' then order_end_date_desc
else
super
end
......
......@@ -92,7 +92,7 @@ class License < ActiveRecord::Base
prometheus_alerts
operations_dashboard
tracing
webide_terminal
web_ide_terminal
].freeze
# List all features available for early adopters,
......
# frozen_string_literal: true
class WebIdeTerminal
include ::Gitlab::Routing
attr_reader :build, :project
delegate :id, :status, to: :build
def initialize(build)
@build = build
@project = build.project
end
def show_path
web_ide_terminal_route_generator(:show)
end
def retry_path
web_ide_terminal_route_generator(:retry)
end
def cancel_path
web_ide_terminal_route_generator(:cancel)
end
def terminal_path
terminal_project_job_path(project, build, format: :ws)
end
private
def web_ide_terminal_route_generator(action)
url_for(action: action,
controller: 'projects/web_ide_terminals',
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: build.id,
only_path: true)
end
end
......@@ -11,6 +11,23 @@ module EE
prevent :update_build
end
condition(:is_web_ide_terminal, scope: :subject) do
@subject.pipeline.webide?
end
rule { is_web_ide_terminal & can?(:create_web_ide_terminal) & (admin | owner_of_job) }.policy do
enable :read_web_ide_terminal
enable :update_web_ide_terminal
end
rule { is_web_ide_terminal & ~can?(:update_web_ide_terminal) }.policy do
prevent :create_build_terminal
end
rule { can?(:update_web_ide_terminal) & terminal }.policy do
enable :create_build_terminal
end
private
alias_method :current_user, :user
......
......@@ -203,7 +203,7 @@ module EE
end
condition(:web_ide_terminal_available) do
@subject.feature_available?(:webide_terminal)
@subject.feature_available?(:web_ide_terminal)
end
rule { web_ide_terminal_available & can?(:create_pipeline) & can?(:maintainer_access) }.enable :create_web_ide_terminal
......
......@@ -66,7 +66,7 @@ module EE
end
expose :managed_licenses_path do |merge_request|
api_v4_projects_managed_licenses_path(id: merge_request.source_project.id)
api_v4_projects_managed_licenses_path(id: merge_request.target_project.id)
end
expose :can_manage_licenses do |merge_request|
......
# frozen_string_literal: true
class WebIdeTerminalEntity < Grape::Entity
expose :id
expose :status
expose :show_path
expose :cancel_path
expose :retry_path
expose :terminal_path
end
# frozen_string_literal: true
class WebIdeTerminalSerializer < BaseSerializer
entity WebIdeTerminalEntity
def represent(resource, opts = {})
resource = WebIdeTerminal.new(resource) if resource.is_a?(Ci::Build)
super
end
end
# frozen_string_literal: true
module Ci
class CreateWebideTerminalService < ::BaseService
class CreateWebIdeTerminalService < ::BaseService
include ::Gitlab::Utils::StrongMemoize
TerminalCreationError = Class.new(StandardError)
......@@ -62,7 +62,7 @@ module Ci
end
def load_terminal_config!
result = ::Ci::WebideConfigService.new(project, current_user, sha: sha).execute
result = ::Ci::WebIdeConfigService.new(project, current_user, sha: sha).execute
raise TerminalCreationError, result[:message] if result[:status] != :success
@terminal = result[:terminal]
......
# frozen_string_literal: true
module Ci
class WebideConfigService < ::BaseService
class WebIdeConfigService < ::BaseService
include ::Gitlab::Utils::StrongMemoize
ValidationError = Class.new(StandardError)
......@@ -37,12 +37,12 @@ module Ci
end
def load_config!
@config = Gitlab::Webide::Config.new(config_content)
@config = Gitlab::WebIde::Config.new(config_content)
unless @config.valid?
raise ValidationError, @config.errors.first
end
rescue Gitlab::Webide::Config::ConfigError => e
rescue Gitlab::WebIde::Config::ConfigError => e
raise ValidationError, e.message
end
......
......@@ -25,7 +25,7 @@ module EE
epic_param = params.delete(:epic)
if epic_param
EpicIssues::CreateService.new(epic_param, current_user, { target_issue: issue }).execute
EpicIssues::CreateService.new(epic_param, current_user, { target_issuable: issue }).execute
else
link = EpicIssue.find_by(issue_id: issue.id) # rubocop: disable CodeReuse/ActiveRecord
......
......@@ -3,7 +3,7 @@ module EpicIssues
private
# rubocop: disable CodeReuse/ActiveRecord
def relate_issues(referenced_issue)
def relate_issuables(referenced_issue)
link = EpicIssue.find_or_initialize_by(issue: referenced_issue)
affected_epics = [issuable]
......@@ -41,17 +41,17 @@ module EpicIssues
{ group: issuable.group }
end
def linkable_issues(issues)
def linkable_issuables(issues)
@linkable_issues ||= begin
return [] unless can?(current_user, :admin_epic, issuable.group)
issues.select do |issue|
issuable_group_descendants.include?(issue.project.group) && !previous_related_issues.include?(issue)
issuable_group_descendants.include?(issue.project.group) && !previous_related_issuables.include?(issue)
end
end
end
def previous_related_issues
def previous_related_issuables
@related_issues ||= issuable.issues.to_a
end
......
......@@ -2,7 +2,7 @@ module EpicIssues
class ListService < IssuableLinks::ListService
private
def issues
def child_issuables
return [] unless issuable&.group&.feature_available?(:epics)
issuable.issues_readable_by(current_user)
......
......@@ -11,58 +11,62 @@ module IssuableLinks
# otherwise create issue links for the issues which
# are still not assigned and return success message.
if render_conflict_error?
return error('Issue(s) already assigned', 409)
return error(issuables_assigned_message, 409)
end
if render_not_found_error?
return error('No Issue found for given params', 404)
return error(issuables_not_found_message, 404)
end
create_issue_links
create_links
success
end
private
def render_conflict_error?
referenced_issues.present? && (referenced_issues - previous_related_issues).empty?
referenced_issuables.present? && (referenced_issuables - previous_related_issuables).empty?
end
def render_not_found_error?
linkable_issues(referenced_issues).empty?
linkable_issuables(referenced_issuables).empty?
end
def create_issue_links
issues = linkable_issues(referenced_issues)
def create_links
objects = linkable_issuables(referenced_issuables)
issues.each do |referenced_issue|
relate_issues(referenced_issue) do |params|
create_notes(referenced_issue, params)
objects.each do |referenced_object|
relate_issuables(referenced_object) do |params|
create_notes(referenced_object, params)
end
end
end
def referenced_issues
@referenced_issues ||= begin
target_issue = params[:target_issue]
def referenced_issuables
@referenced_issuables ||= begin
target_issuable = params[:target_issuable]
if params[:issue_references].present?
extract_issues_from_references
elsif target_issue
[target_issue]
if params[:issuable_references].present?
extract_references
elsif target_issuable
[target_issuable]
else
[]
end
end
end
def extract_issues_from_references
issue_references = params[:issue_references]
text = issue_references.join(' ')
def extract_references
issuable_references = params[:issuable_references]
text = issuable_references.join(' ')
extractor = Gitlab::ReferenceExtractor.new(issuable.project, @current_user)
extractor = Gitlab::ReferenceExtractor.new(issuable.project, current_user)
extractor.analyze(text, extractor_context)
references(extractor)
end
def references(extractor)
extractor.issues
end
......@@ -70,16 +74,24 @@ module IssuableLinks
{}
end
def linkable_issues(issues)
def linkable_issuables(objects)
raise NotImplementedError
end
def previous_related_issues
def previous_related_issuables
raise NotImplementedError
end
def relate_issues(referenced_issue)
def relate_issuables(referenced_object)
raise NotImplementedError
end
def issuables_assigned_message
'Issue(s) already assigned'
end
def issuables_not_found_message
'No Issue found for given params'
end
end
end
......@@ -9,29 +9,33 @@ module IssuableLinks
end
def execute
issues.map do |referenced_issue|
to_hash(referenced_issue)
child_issuables.map do |referenced_object|
to_hash(referenced_object)
end
end
private
def relation_path(issue)
def relation_path(object)
raise NotImplementedError
end
def reference(issue)
issue.to_reference(issuable.project)
def reference(object)
object.to_reference(issuable.project)
end
def to_hash(issue)
def issuable_path(object)
project_issue_path(object.project, object.iid)
end
def to_hash(object)
{
id: issue.id,
title: issue.title,
state: issue.state,
reference: reference(issue),
path: project_issue_path(issue.project, issue.iid),
relation_path: relation_path(issue)
id: object.id,
title: object.title,
state: object.state,
reference: reference(object),
path: issuable_path(object),
relation_path: relation_path(object)
}
end
end
......
module IssueLinks
class CreateService < IssuableLinks::CreateService
def relate_issues(referenced_issue)
def relate_issuables(referenced_issue)
link = IssueLink.create(source: issuable, target: referenced_issue)
yield if link.persisted?
end
def linkable_issues(issues)
@linkable_issues ||= begin
def linkable_issuables(issues)
@linkable_issuables ||= begin
issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
end
end
......@@ -17,7 +17,7 @@ module IssueLinks
SystemNoteService.relate_issue(referenced_issue, issuable, current_user)
end
def previous_related_issues
def previous_related_issuables
@related_issues ||= issuable.related_issues(current_user).to_a
end
end
......
......@@ -4,7 +4,7 @@ module IssueLinks
private
def issues
def child_issuables
issuable.related_issues(current_user, preload: { project: :namespace })
end
......
= render partial: "ide/show"
- sorted_by = sort_options_hash[@sort]
- sorted_by = epics_sort_options_hash[@sort]
.dropdown.inline.prepend-left-10
.btn-group
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
= sorted_by
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by)
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_recently_created, label: true), sorted_by)
= sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by)
= sortable_item(sort_title_start_date, page_filter_path(sort: sort_value_start_date, label: true), sorted_by)
= sortable_item(sort_title_start_date, page_filter_path(sort: sort_value_start_date_soon, label: true), sorted_by)
= sortable_item(sort_title_end_date, page_filter_path(sort: sort_value_end_date, label: true), sorted_by)
= sort_order_button(@sort)
---
title: Fix issue board api with special milestones
merge_request: 8653
author:
type: fixed
---
title: Add sort direction button with sort dropdown for Epics and Roadmap
merge_request:
author:
type: changed
---
title: Fixed license managment path in MR widget for fork cases
merge_request: 8700
author:
type: fixed
---
title: Added web terminals to Web IDE
merge_request: 7386
author:
type: added
......@@ -20,6 +20,17 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'epics'
end
end
resources :web_ide_terminals, path: :ide_terminals, only: [:create, :show], constraints: { id: /\d+/, format: :json } do
member do
post :cancel
post :retry
end
collection do
post :check_config
end
end
end
end
end
class AddEpicsSortToUserPreference < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :user_preferences, :epics_sort, :string
end
end
......@@ -89,7 +89,7 @@ module API
issue = Issue.find(params[:issue_id])
create_params = { target_issue: issue }
create_params = { target_issuable: issue }
result = ::EpicIssues::CreateService.new(epic, current_user, create_params).execute
......
......@@ -35,7 +35,7 @@ module API
target_issue = find_project_issue(declared_params[:target_issue_iid],
declared_params[:target_project_id])
create_params = { target_issue: target_issue }
create_params = { target_issuable: target_issue }
result = ::IssueLinks::CreateService
.new(source_issue, current_user, create_params)
......
......@@ -93,17 +93,22 @@ module EE
extend ActiveSupport::Concern
prepended do
def scoped_issue_available?(board)
board.parent.feature_available?(:scoped_issue_board)
end
# Default filtering configuration
expose :name
expose :group
expose :milestone, using: ::API::Entities::Milestone, if: ->(board, _) { scoped_issue_available?(board) }
expose :assignee, using: ::API::Entities::UserBasic, if: ->(board, _) { scoped_issue_available?(board) }
expose :labels, using: ::API::Entities::LabelBasic, if: ->(board, _) { scoped_issue_available?(board) }
expose :weight, if: ->(board, _) { scoped_issue_available?(board) }
with_options if: ->(board, _) { board.parent.feature_available?(:scoped_issue_board) } do
expose :milestone do |board|
if board.milestone.is_a?(Milestone)
::API::Entities::Milestone.represent(board.milestone)
else
SpecialBoardFilter.represent(board.milestone)
end
end
expose :assignee, using: ::API::Entities::UserBasic
expose :labels, using: ::API::Entities::LabelBasic
expose :weight
end
end
end
......@@ -213,6 +218,10 @@ module EE
expose :target, as: :target_issue, using: ::API::Entities::IssueBasic
end
class SpecialBoardFilter < Grape::Entity
expose :title
end
class Approvals < Grape::Entity
expose :user, using: ::API::Entities::UserBasic
end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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