summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue59
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue2
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue109
-rw-r--r--app/assets/javascripts/project_new.js63
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js6
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/responsive-tables.scss86
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/environments.scss72
-rw-r--r--app/assets/stylesheets/pages/projects.scss14
-rw-r--r--app/finders/events_finder.rb62
-rw-r--r--app/helpers/projects_helper.rb10
-rw-r--r--app/helpers/visibility_level_helper.rb4
-rw-r--r--app/models/event.rb32
-rw-r--r--app/services/users/activity_service.rb2
-rw-r--r--app/views/projects/edit.html.haml6
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml10
-rw-r--r--changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml4
-rw-r--r--changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml4
-rw-r--r--changelogs/unreleased/issue-23254.yml4
-rw-r--r--doc/api/README.md2
-rw-r--r--doc/api/environments.md (renamed from doc/api/enviroments.md)0
-rw-r--r--doc/api/events.md347
-rw-r--r--doc/api/projects.md138
-rw-r--r--doc/api/users.md141
-rw-r--r--doc/ci/examples/code_climate.md12
-rw-r--r--doc/development/architecture.md8
-rw-r--r--doc/install/kubernetes/gitlab_chart.md4
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md4
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/img/project_settings_list.pngbin5919 -> 0 bytes
-rw-r--r--doc/user/project/integrations/img/accessing_integrations.pngbin8941 -> 0 bytes
-rw-r--r--doc/user/project/integrations/index.md8
-rw-r--r--doc/user/project/integrations/project_services.md15
-rw-r--r--doc/user/project/integrations/webhooks.md7
-rw-r--r--doc/user/project/issues/index.md8
-rw-r--r--doc/user/project/pipelines/settings.md9
-rw-r--r--doc/user/project/protected_branches.md7
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/events.rb86
-rw-r--r--lib/api/projects.rb10
-rw-r--r--lib/api/users.rb21
-rw-r--r--spec/features/projects/environments/environments_spec.rb4
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb4
-rw-r--r--spec/features/unsubscribe_links_spec.rb4
-rw-r--r--spec/finders/events_finder_spec.rb44
-rw-r--r--spec/helpers/projects_helper_spec.rb4
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb2
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js4
-rw-r--r--spec/javascripts/environments/environment_spec.js2
-rw-r--r--spec/javascripts/environments/environment_table_spec.js2
-rw-r--r--spec/requests/api/events_spec.rb142
-rw-r--r--spec/requests/api/projects_spec.rb62
-rw-r--r--spec/requests/api/users_spec.rb77
58 files changed, 1100 insertions, 640 deletions
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index a2448520a5f..41d5453f1b2 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -70,7 +70,7 @@ export default {
</span>
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu">
<li v-for="action in actions">
<button
type="button"
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 012ff1f975b..03eb51ba1b2 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -421,14 +421,19 @@ export default {
};
</script>
<template>
- <tr :class="{ 'js-child-row': model.isChildren }">
- <td>
+ <div
+ :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }">
+ <div class="table-section section-10" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-mobile-header">
+ Environment
+ </div>
<a
v-if="!model.isFolder"
- class="environment-name"
- :class="{ 'prepend-left-default': model.isChildren }"
+ class="environment-name flex-truncate-parent table-mobile-content"
:href="environmentPath">
- {{model.name}}
+ <span class="flex-truncate-child">{{model.name}}</span>
</a>
<span
v-else
@@ -461,9 +466,9 @@ export default {
{{model.size}}
</span>
</span>
- </td>
+ </div>
- <td class="deployment-column">
+ <div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell">
<span v-if="shouldRenderDeploymentID">
{{deploymentInternalId}}
</span>
@@ -478,21 +483,26 @@ export default {
:tooltip-text="deploymentUser.username"
/>
</span>
- </td>
+ </div>
- <td class="environments-build-cell">
+ <div class="table-section section-15 hidden-xs hidden-sm" role="gridcell">
<a
v-if="shouldRenderBuildName"
class="build-link"
:href="buildPath">
{{buildName}}
</a>
- </td>
+ </div>
- <td>
+ <div class="table-section section-25" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-mobile-header">
+ Commit
+ </div>
<div
v-if="!model.isFolder && hasLastDeploymentKey"
- class="js-commit-component">
+ class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
@@ -501,25 +511,30 @@ export default {
:title="commitTitle"
:author="commitAuthor"/>
</div>
- <p
+ <div
v-if="!model.isFolder && !hasLastDeploymentKey"
class="commit-title">
No deployments yet
- </p>
- </td>
+ </div>
+ </div>
- <td>
+ <div class="table-section section-10" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-mobile-header">
+ Updated
+ </div>
<span
v-if="!model.isFolder && canShowDate"
- class="environment-created-date-timeago">
+ class="environment-created-date-timeago table-mobile-content">
{{createdDate}}
</span>
- </td>
+ </div>
- <td class="environments-actions">
+ <div class="table-section section-30 environments-actions table-button-footer" role="gridcell">
<div
v-if="!model.isFolder"
- class="btn-group pull-right"
+ class="btn-group environment-action-buttons"
role="group">
<actions-component
@@ -553,6 +568,6 @@ export default {
:retry-url="retryUrl"
/>
</div>
- </td>
- </tr>
+ </div>
+ </div>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 79c019b3491..07cf92281a0 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -19,7 +19,7 @@ export default {
</script>
<template>
<a
- class="btn monitoring-url has-tooltip"
+ class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 2ba985bfe3e..49dba38edfb 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -43,7 +43,7 @@ export default {
<template>
<button
type="button"
- class="btn"
+ class="btn hidden-xs hidden-sm"
@click="onClick"
:disabled="isLoading">
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index a904453ffa9..091c543860b 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -47,7 +47,7 @@ export default {
<template>
<button
type="button"
- class="btn stop-env-link has-tooltip"
+ class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
data-container="body"
@click="onClick"
:disabled="isLoading"
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index c8c1f17d4d8..1ca65a79951 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -29,7 +29,7 @@ export default {
</script>
<template>
<a
- class="btn terminal-button has-tooltip"
+ class="btn terminal-button has-tooltip hidden-xs hidden-sm"
data-container="body"
:title="title"
:aria-label="title"
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 5148a2ae79b..f9262ab85c5 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -45,68 +45,59 @@ export default {
};
</script>
<template>
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="environments-name">
- Environment
- </th>
- <th class="environments-deploy">
- Last deployment
- </th>
- <th class="environments-build">
- Job
- </th>
- <th class="environments-commit">
- Commit
- </th>
- <th class="environments-date">
- Updated
- </th>
- <th class="environments-actions"></th>
- </tr>
- </thead>
- <tbody>
- <template
- v-for="model in environments"
- v-bind:model="model">
- <tr
- is="environment-item"
- :model="model"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- />
+ <div class="ci-table" role="grid">
+ <div class="gl-responsive-table-row table-row-header" role="row">
+ <div class="table-section section-10 environments-name" role="rowheader">
+ Environment
+ </div>
+ <div class="table-section section-10 environments-deploy" role="rowheader">
+ Deployment
+ </div>
+ <div class="table-section section-15 environments-build" role="rowheader">
+ Job
+ </div>
+ <div class="table-section section-25 environments-commit" role="rowheader">
+ Commit
+ </div>
+ <div class="table-section section-10 environments-date" role="rowheader">
+ Updated
+ </div>
+ </div>
+ <template
+ v-for="model in environments"
+ v-bind:model="model">
+ <div
+ is="environment-item"
+ :model="model"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ />
- <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
- <tr v-if="isLoadingFolderContent">
- <td colspan="6">
- <loading-icon size="2" />
- </td>
- </tr>
+ <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
+ <div v-if="isLoadingFolderContent">
+ <loading-icon size="2" />
+ </div>
- <template v-else>
- <tr
- is="environment-item"
- v-for="children in model.children"
- :model="children"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- />
+ <template v-else>
+ <div
+ is="environment-item"
+ v-for="children in model.children"
+ :model="children"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ />
- <tr>
- <td
- colspan="6"
- class="text-center">
- <a
- :href="folderUrl(model)"
- class="btn btn-default">
- Show all
- </a>
- </td>
- </tr>
- </template>
+ <div>
+ <div class="text-center prepend-top-10">
+ <a
+ :href="folderUrl(model)"
+ class="btn btn-default">
+ Show all
+ </a>
+ </div>
+ </div>
</template>
</template>
- </tbody>
- </table>
+ </template>
+ </div>
</template>
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 04b381fe0e0..c0f757269cb 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,11 +1,17 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
+function highlightChanges($elm) {
+ $elm.addClass('highlight-changes');
+ setTimeout(() => $elm.removeClass('highlight-changes'), 10);
+}
+
(function() {
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = this.toggleSettings.bind(this);
this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-select');
+ this.$projectSelects = this.$selects.not('.js-repo-select');
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
@@ -26,6 +32,42 @@
if (!visibilityContainer) return;
const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
visibilitySelect.init();
+
+ const $visibilitySelect = $(visibilityContainer).find('select');
+ let projectVisibility = $visibilitySelect.val();
+ const PROJECT_VISIBILITY_PRIVATE = '0';
+
+ $visibilitySelect.on('change', () => {
+ const newProjectVisibility = $visibilitySelect.val();
+
+ if (projectVisibility !== newProjectVisibility) {
+ this.$projectSelects.each((idx, select) => {
+ const $select = $(select);
+ const $options = $select.find('option');
+ const values = $.map($options, e => e.value);
+
+ // if switched to "private", limit visibility options
+ if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
+ if ($select.val() !== values[0] && $select.val() !== values[1]) {
+ $select.val(values[1]).trigger('change');
+ highlightChanges($select);
+ }
+ $options.slice(2).disable();
+ }
+
+ // if switched from "private", increase visibility for non-disabled options
+ if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
+ $options.enable();
+ if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
+ $select.val(values[values.length - 1]).trigger('change');
+ highlightChanges($select);
+ }
+ }
+ });
+
+ projectVisibility = newProjectVisibility;
+ }
+ });
};
ProjectNew.prototype.toggleSettings = function() {
@@ -56,8 +98,10 @@
ProjectNew.prototype.toggleRepoVisibility = function () {
var $repoAccessLevel = $('.js-repo-access-level select');
+ var $lfsEnabledOption = $('.js-lfs-enabled select');
var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
+ var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
@@ -71,29 +115,40 @@
var $this = $(this);
var repoSelectVal = parseInt($this.val(), 10);
- $this.find('option').show();
+ $this.find('option').enable();
- if (selectedVal < repoSelectVal) {
- $this.val(selectedVal);
+ if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
+ $this.val(selectedVal).trigger('change');
+ highlightChanges($this);
}
- $this.find("option[value='" + selectedVal + "']").nextAll().hide();
+ $this.find("option[value='" + selectedVal + "']").nextAll().disable();
});
if (selectedVal) {
this.$repoSelects.removeClass('disabled');
+ if ($lfsEnabledOption.length) {
+ $lfsEnabledOption.removeClass('disabled');
+ highlightChanges($lfsEnabledOption);
+ }
if (containerRegistry) {
containerRegistry.style.display = '';
}
} else {
this.$repoSelects.addClass('disabled');
+ if ($lfsEnabledOption.length) {
+ $lfsEnabledOption.val('false').addClass('disabled');
+ highlightChanges($lfsEnabledOption);
+ }
if (containerRegistry) {
containerRegistry.style.display = 'none';
containerRegistryCheckbox.checked = false;
}
}
+
+ prevSelectedVal = selectedVal;
}.bind(this));
};
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
index 8e22057e2e9..ff5ae28e062 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -135,8 +135,8 @@ export default {
{{shortSha}}
</a>
- <p class="commit-title">
- <span v-if="title">
+ <div class="commit-title flex-truncate-parent">
+ <span v-if="title" class="flex-truncate-child">
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
@@ -153,7 +153,7 @@ export default {
<span v-else>
Cant find HEAD commit for this branch
</span>
- </p>
+ </div>
</div>
`,
};
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index b8ba77f4513..9dc9f9a9068 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -49,3 +49,4 @@
@import "framework/icons.scss";
@import "framework/snippets.scss";
@import "framework/memory_graph.scss";
+@import "framework/responsive-tables.scss";
diff --git a/app/assets/stylesheets/framework/responsive-tables.scss b/app/assets/stylesheets/framework/responsive-tables.scss
new file mode 100644
index 00000000000..94528c7a222
--- /dev/null
+++ b/app/assets/stylesheets/framework/responsive-tables.scss
@@ -0,0 +1,86 @@
+@mixin flex-max-width($max) {
+ flex: 0 0 #{$max + '%'};
+ max-width: #{$max + '%'};
+}
+
+.gl-responsive-table-row {
+ margin-top: 10px;
+ border: 1px solid $border-color;
+
+ @media (min-width: $screen-md-min) {
+ padding: 15px 0;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ border: none;
+ border-bottom: 1px solid $white-normal;
+ }
+
+ .table-section {
+ white-space: nowrap;
+
+ $section-widths: 10 15 25 30;
+ @each $width in $section-widths {
+ &.section-#{$width} {
+ flex: 0 0 #{$width + '%'};
+
+ @media (min-width: $screen-md-min) {
+ max-width: #{$width + '%'};
+ }
+ }
+ }
+
+ &:not(.table-button-footer) {
+ @media (max-width: $screen-sm-max) {
+ display: flex;
+ align-self: stretch;
+ padding: 10px;
+ align-items: center;
+ height: 62px;
+
+ &:not(:first-of-type) {
+ border-top: 1px solid $white-normal;
+ }
+ }
+ }
+ }
+}
+
+.table-row-header {
+ font-size: 13px;
+
+ @media (max-width: $screen-sm-max) {
+ display: none;
+ }
+}
+
+.table-mobile-header {
+ color: $gl-text-color-secondary;
+ @include flex-max-width(40);
+
+ @media (min-width: $screen-md-min) {
+ display: none;
+ }
+}
+
+.table-mobile-content {
+ @media (max-width: $screen-sm-max) {
+ @include flex-max-width(60);
+ text-align: right;
+ }
+}
+
+.flex-truncate-parent {
+ display: flex;
+}
+
+.flex-truncate-child {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ @media (min-width: $screen-md-min) {
+ flex: 0 0 90%;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index b3a86b92d93..4114a050d9a 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -187,6 +187,7 @@ $divergence-graph-bar-bg: #ccc;
$divergence-graph-separator-bg: #ccc;
$general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
+$highlight-changes-color: rgb(235, 255, 232);
/*
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index f269d53093d..bc151d2358f 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -11,34 +11,7 @@
}
.environments-container {
- .table-holder {
- width: 100%;
-
- @media (max-width: $screen-sm-max) {
- overflow: auto;
- }
- }
-
- .table.ci-table {
- .environments-actions {
- min-width: 300px;
- }
-
- .environments-commit,
- .environments-actions {
- width: 20%;
- }
-
- .environments-date {
- width: 10%;
- }
-
- .environments-name,
- .environments-deploy,
- .environments-build {
- width: 15%;
- }
-
+ .ci-table {
.deployment-column {
> span {
word-break: break-all;
@@ -150,6 +123,49 @@
}
}
+.gl-responsive-table-row {
+ .environments-actions {
+ @media (min-width: $screen-md-min) {
+ text-align: right;
+ }
+
+ @media (max-width: $screen-sm-max) {
+ background-color: $gray-normal;
+ align-self: stretch;
+ border-top: 1px solid $border-color;
+
+ .environment-action-buttons {
+ padding: 10px;
+ display: flex;
+
+ .btn {
+ border-radius: 3px;
+ }
+
+ > .btn-group,
+ .external-url {
+ flex: 1;
+ flex-basis: 28px;
+ }
+
+ .dropdown-new {
+ width: 100%;
+ }
+ }
+ }
+ }
+}
+
+.folder-row {
+ padding: 15px 0;
+ border-bottom: 1px solid $white-normal;
+
+ @media (max-width: $screen-sm-max) {
+ border-top: 1px solid $white-normal;
+ margin-top: 10px;
+ }
+}
+
.prometheus-graph {
text {
fill: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 4719bf826dc..a2f781a6a6e 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -29,6 +29,20 @@
& > .form-group {
padding-left: 0;
}
+
+ select option[disabled] {
+ display: none;
+ }
+ }
+
+ select {
+ background: transparent;
+ transition: background 2s ease-out;
+
+ &.highlight-changes {
+ background: $highlight-changes-color;
+ transition: none;
+ }
}
.help-block {
diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb
new file mode 100644
index 00000000000..b0450ddc1fd
--- /dev/null
+++ b/app/finders/events_finder.rb
@@ -0,0 +1,62 @@
+class EventsFinder
+ attr_reader :source, :params, :current_user
+
+ # Used to filter Events
+ #
+ # Arguments:
+ # source - which user or project to looks for events on
+ # current_user - only return events for projects visible to this user
+ # params:
+ # action: string
+ # target_type: string
+ # before: datetime
+ # after: datetime
+ #
+ def initialize(params = {})
+ @source = params.delete(:source)
+ @current_user = params.delete(:current_user)
+ @params = params
+ end
+
+ def execute
+ events = source.events
+
+ events = by_current_user_access(events)
+ events = by_action(events)
+ events = by_target_type(events)
+ events = by_created_at_before(events)
+ events = by_created_at_after(events)
+
+ events
+ end
+
+ private
+
+ def by_current_user_access(events)
+ events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project)
+ end
+
+ def by_action(events)
+ return events unless Event::ACTIONS[params[:action]]
+
+ events.where(action: Event::ACTIONS[params[:action]])
+ end
+
+ def by_target_type(events)
+ return events unless Event::TARGET_TYPES[params[:target_type]]
+
+ events.where(target_type: Event::TARGET_TYPES[params[:target_type]])
+ end
+
+ def by_created_at_before(events)
+ return events unless params[:before]
+
+ events.where('events.created_at < ?', params[:before].beginning_of_day)
+ end
+
+ def by_created_at_after(events)
+ return events unless params[:after]
+
+ events.where('events.created_at > ?', params[:after].end_of_day)
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 7b0584c42a2..f74e61c9481 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -138,11 +138,15 @@ module ProjectsHelper
if @project.private?
level = @project.project_feature.send(field)
- options.delete('Everyone with access')
- highest_available_option = options.values.max if level == ProjectFeature::ENABLED
+ disabled_option = ProjectFeature::ENABLED
+ highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
end
- options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
+ options = options_for_select(
+ options,
+ selected: highest_available_option || @project.project_feature.public_send(field),
+ disabled: disabled_option
+ )
content_tag(
:select,
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index b4aaf498068..50757b01538 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -31,9 +31,9 @@ module VisibilityLevelHelper
when Gitlab::VisibilityLevel::PRIVATE
"Project access must be granted explicitly to each user."
when Gitlab::VisibilityLevel::INTERNAL
- "The project can be cloned by any logged in user."
+ "The project can be accessed by any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
- "The project can be cloned without any authentication."
+ "The project can be accessed without any authentication."
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 46e89388bc1..d6d39473774 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -14,6 +14,30 @@ class Event < ActiveRecord::Base
DESTROYED = 10
EXPIRED = 11 # User left project due to expiry
+ ACTIONS = HashWithIndifferentAccess.new(
+ created: CREATED,
+ updated: UPDATED,
+ closed: CLOSED,
+ reopened: REOPENED,
+ pushed: PUSHED,
+ commented: COMMENTED,
+ merged: MERGED,
+ joined: JOINED,
+ left: LEFT,
+ destroyed: DESTROYED,
+ expired: EXPIRED
+ ).freeze
+
+ TARGET_TYPES = HashWithIndifferentAccess.new(
+ issue: Issue,
+ milestone: Milestone,
+ merge_request: MergeRequest,
+ note: Note,
+ project: Project,
+ snippet: Snippet,
+ user: User
+ ).freeze
+
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
@@ -55,6 +79,14 @@ class Event < ActiveRecord::Base
def limit_recent(limit = 20, offset = nil)
recent.limit(limit).offset(offset)
end
+
+ def actions
+ ACTIONS.keys
+ end
+
+ def target_types
+ TARGET_TYPES.keys
+ end
end
def visible_to_user?(user = nil)
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
index facf21a7f5c..ab532a1fdcf 100644
--- a/app/services/users/activity_service.rb
+++ b/app/services/users/activity_service.rb
@@ -16,7 +16,7 @@ module Users
def record_activity
Gitlab::UserActivities.record(@author.id)
- Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}")
+ Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})")
end
end
end
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index f5549d7f4cd..c3dab68cea5 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -42,7 +42,7 @@
.col-md-9
.label-light
= label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
- = link_to "(?)", help_page_path("public_access/public_access")
+ = link_to icon('question-circle'), help_page_path("public_access/public_access")
%span.help-block
.col-md-3.visibility-select-container
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
@@ -92,14 +92,14 @@
.form-group
= render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin?
- .row
+ .row.js-lfs-enabled
.col-md-9
= f.label :lfs_enabled, 'LFS', class: 'label-light'
%span.help-block
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
.col-md-3
- = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' }
+ = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select', data: { field: 'lfs_enabled' }
- if Gitlab.config.registry.enabled
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
index 9ce6a1aeef5..de52fd00157 100644
--- a/app/views/sent_notifications/unsubscribe.html.haml
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -1,16 +1,14 @@
- noteable = @sent_notification.noteable
-- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false)
+- noteable_type = @sent_notification.noteable_type.titleize.downcase
- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
-
-- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace
-
+- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace
%h3.page-title
- Unsubscribe from #{noteable_type} #{noteable_text}
+ Unsubscribe from #{noteable_type}
%p
= succeed '?' do
- Are you sure you want to unsubscribe from #{noteable_type}
+ Are you sure you want to unsubscribe from the #{noteable_type}:
= link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
%p
diff --git a/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml b/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml
new file mode 100644
index 00000000000..9c17c3b949c
--- /dev/null
+++ b/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce an Events API
+merge_request: 11755
+author:
diff --git a/changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml b/changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml
new file mode 100644
index 00000000000..dbd8a538d51
--- /dev/null
+++ b/changelogs/unreleased/24032-when-changing-project-visibility-setting-change-other-dropdowns-automatically.yml
@@ -0,0 +1,4 @@
+---
+title: Automatically adjust project settings to match changes in project visibility
+merge_request: 11831
+author:
diff --git a/changelogs/unreleased/issue-23254.yml b/changelogs/unreleased/issue-23254.yml
new file mode 100644
index 00000000000..568a7a41c30
--- /dev/null
+++ b/changelogs/unreleased/issue-23254.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed style on unsubscribe page
+merge_request:
+author: Gustav Ernberg
diff --git a/doc/api/README.md b/doc/api/README.md
index 45579ccac4e..e1d4009dedc 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -15,6 +15,8 @@ following locations:
- [Commits](commits.md)
- [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md)
+- [Environments](environments.md)
+- [Events](events.md)
- [Gitignores templates](templates/gitignores.md)
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md)
diff --git a/doc/api/enviroments.md b/doc/api/environments.md
index 5ca766bf87d..5ca766bf87d 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/environments.md
diff --git a/doc/api/events.md b/doc/api/events.md
new file mode 100644
index 00000000000..e7829c9f479
--- /dev/null
+++ b/doc/api/events.md
@@ -0,0 +1,347 @@
+# Events
+
+## Filter parameters
+
+### Action Types
+
+Available action types for the `action` parameter are:
+
+- `created`
+- `updated`
+- `closed`
+- `reopened`
+- `pushed`
+- `commented`
+- `merged`
+- `joined`
+- `left`
+- `destroyed`
+- `expired`
+
+Note that these options are downcased.
+
+### Target Types
+
+Available target types for the `target_type` parameter are:
+
+- `issue`
+- `milestone`
+- `merge_request`
+- `note`
+- `project`
+- `snippet`
+- `user`
+
+Note that these options are downcased.
+
+### Date formatting
+
+Dates for the `before` and `after` parameters should be supplied in the following format:
+
+```
+YYYY-MM-DD
+```
+
+## List currently authenticated user's events
+
+>**Note:** This endpoint was introduced in GitLab 9.3.
+
+Get a list of events for the authenticated user.
+
+```
+GET /events
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `action` | string | no | Include only events of a particular [action type][action-types] |
+| `target_type` | string | no | Include only events of a particular [target type][target-types] |
+| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
+| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
+| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
+
+Example request:
+
+```
+curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
+```
+
+Example response:
+
+```json
+[
+ {
+ "title":null,
+ "project_id":1,
+ "action_name":"opened",
+ "target_id":160,
+ "target_type":"Issue",
+ "author_id":25,
+ "data":null,
+ "target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
+ "created_at":"2017-02-09T10:43:19.667Z",
+ "author":{
+ "name":"User 3",
+ "username":"user3",
+ "id":25,
+ "state":"active",
+ "avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/user3"
+ },
+ "author_username":"user3"
+ },
+ {
+ "title":null,
+ "project_id":1,
+ "action_name":"opened",
+ "target_id":159,
+ "target_type":"Issue",
+ "author_id":21,
+ "data":null,
+ "target_title":"Nostrum enim non et sed optio illo deleniti non.",
+ "created_at":"2017-02-09T10:43:19.426Z",
+ "author":{
+ "name":"Test User",
+ "username":"ted",
+ "id":21,
+ "state":"active",
+ "avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/ted"
+ },
+ "author_username":"ted"
+ }
+]
+```
+
+### Get user contribution events
+
+>**Note:** Documentation was formerly located in the [Users API pages][users-api].
+
+Get the contribution events for the specified user, sorted from newest to oldest.
+
+```
+GET /users/:id/events
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID or Username of the user |
+| `action` | string | no | Include only events of a particular [action type][action-types] |
+| `target_type` | string | no | Include only events of a particular [target type][target-types] |
+| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
+| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
+| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
+```
+
+Example response:
+
+```json
+[
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "closed",
+ "target_id": 830,
+ "target_type": "Issue",
+ "author_id": 1,
+ "data": null,
+ "target_title": "Public project search field",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "opened",
+ "target_id": null,
+ "target_type": null,
+ "author_id": 1,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "john",
+ "data": {
+ "before": "50d4420237a9de7be1304607147aec22e4a14af7",
+ "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "ref": "refs/heads/master",
+ "user_id": 1,
+ "user_name": "Dmitriy Zaporozhets",
+ "repository": {
+ "name": "gitlabhq",
+ "url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
+ "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
+ "homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
+ },
+ "commits": [
+ {
+ "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "message": "Add simple search to projects in public area",
+ "timestamp": "2013-05-13T18:18:08+00:00",
+ "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "email": "dmitriy.zaporozhets@gmail.com"
+ }
+ }
+ ],
+ "total_commits_count": 1
+ },
+ "target_title": null
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "closed",
+ "target_id": 840,
+ "target_type": "Issue",
+ "author_id": 1,
+ "data": null,
+ "target_title": "Finish & merge Code search PR",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "commented on",
+ "target_id": 1312,
+ "target_type": "Note",
+ "author_id": 1,
+ "data": null,
+ "target_title": null,
+ "created_at": "2015-12-04T10:33:58.089Z",
+ "note": {
+ "id": 1312,
+ "body": "What an awesome day!",
+ "attachment": null,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2015-12-04T10:33:56.698Z",
+ "system": false,
+ "noteable_id": 377,
+ "noteable_type": "Issue"
+ },
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ }
+]
+```
+
+## List a Project's visible events
+
+>**Note:** This endpoint has been around longer than the others. Documentation was formerly located in the [Projects API pages][projects-api].
+
+Get a list of visible events for a particular project.
+
+```
+GET /:project_id/events
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `action` | string | no | Include only events of a particular [action type][action-types] |
+| `target_type` | string | no | Include only events of a particular [target type][target-types] |
+| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
+| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
+| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
+
+Example request:
+
+```
+curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
+```
+
+Example response:
+
+```json
+[
+ {
+ "title":null,
+ "project_id":1,
+ "action_name":"opened",
+ "target_id":160,
+ "target_type":"Issue",
+ "author_id":25,
+ "data":null,
+ "target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
+ "created_at":"2017-02-09T10:43:19.667Z",
+ "author":{
+ "name":"User 3",
+ "username":"user3",
+ "id":25,
+ "state":"active",
+ "avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/user3"
+ },
+ "author_username":"user3"
+ },
+ {
+ "title":null,
+ "project_id":1,
+ "action_name":"opened",
+ "target_id":159,
+ "target_type":"Issue",
+ "author_id":21,
+ "data":null,
+ "target_title":"Nostrum enim non et sed optio illo deleniti non.",
+ "created_at":"2017-02-09T10:43:19.426Z",
+ "author":{
+ "name":"Test User",
+ "username":"ted",
+ "id":21,
+ "state":"active",
+ "avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/ted"
+ },
+ "author_username":"ted"
+ }
+]
+```
+
+[target-types]: #target-types "Target Type parameter"
+[action-types]: #action-types "Action Type parameter"
+[date-formatting]: #date-formatting "Date Formatting guidance"
+[projects-api]: projects.md "Projects API pages"
+[users-api]: users.md "Users API pages"
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 70cad8a6025..0debdcfae89 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -310,143 +310,7 @@ GET /projects/:id/users
### Get project events
-Get the events for the specified project sorted from newest to oldest. This
-endpoint can be accessed without authentication if the project is publicly
-accessible.
-
-```
-GET /projects/:id/events
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
-
-```json
-[
- {
- "title": null,
- "project_id": 15,
- "action_name": "closed",
- "target_id": 830,
- "target_type": "Issue",
- "author_id": 1,
- "data": null,
- "target_title": "Public project search field",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "opened",
- "target_id": null,
- "target_type": null,
- "author_id": 1,
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "john",
- "data": {
- "before": "50d4420237a9de7be1304607147aec22e4a14af7",
- "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "ref": "refs/heads/master",
- "user_id": 1,
- "user_name": "Dmitriy Zaporozhets",
- "repository": {
- "name": "gitlabhq",
- "url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
- "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
- "homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
- },
- "commits": [
- {
- "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "message": "Add simple search to projects in public area",
- "timestamp": "2013-05-13T18:18:08+00:00",
- "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "email": "dmitriy.zaporozhets@gmail.com"
- }
- }
- ],
- "total_commits_count": 1
- },
- "target_title": null
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "closed",
- "target_id": 840,
- "target_type": "Issue",
- "author_id": 1,
- "data": null,
- "target_title": "Finish & merge Code search PR",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "commented on",
- "target_id": 1312,
- "target_type": "Note",
- "author_id": 1,
- "data": null,
- "target_title": null,
- "created_at": "2015-12-04T10:33:58.089Z",
- "note": {
- "id": 1312,
- "body": "What an awesome day!",
- "attachment": null,
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "created_at": "2015-12-04T10:33:56.698Z",
- "system": false,
- "noteable_id": 377,
- "noteable_type": "Issue"
- },
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- }
-]
-```
+Please refer to the [Events API documentation](events.md#list-a-projects-visible-events)
### Create project
diff --git a/doc/api/users.md b/doc/api/users.md
index 7e118dcf4a9..f4167ba2605 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -701,147 +701,8 @@ Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
### Get user contribution events
-Get the contribution events for the specified user, sorted from newest to oldest.
+Please refer to the [Events API documentation](events.md#get-user-contribution-events)
-```
-GET /users/:id/events
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the user |
-
-```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
-```
-
-Example response:
-
-```json
-[
- {
- "title": null,
- "project_id": 15,
- "action_name": "closed",
- "target_id": 830,
- "target_type": "Issue",
- "author_id": 1,
- "data": null,
- "target_title": "Public project search field",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "opened",
- "target_id": null,
- "target_type": null,
- "author_id": 1,
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "john",
- "data": {
- "before": "50d4420237a9de7be1304607147aec22e4a14af7",
- "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "ref": "refs/heads/master",
- "user_id": 1,
- "user_name": "Dmitriy Zaporozhets",
- "repository": {
- "name": "gitlabhq",
- "url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
- "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
- "homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
- },
- "commits": [
- {
- "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "message": "Add simple search to projects in public area",
- "timestamp": "2013-05-13T18:18:08+00:00",
- "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "email": "dmitriy.zaporozhets@gmail.com"
- }
- }
- ],
- "total_commits_count": 1
- },
- "target_title": null
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "closed",
- "target_id": 840,
- "target_type": "Issue",
- "author_id": 1,
- "data": null,
- "target_title": "Finish & merge Code search PR",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "commented on",
- "target_id": 1312,
- "target_type": "Note",
- "author_id": 1,
- "data": null,
- "target_title": null,
- "created_at": "2015-12-04T10:33:58.089Z",
- "note": {
- "id": 1312,
- "body": "What an awesome day!",
- "attachment": null,
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "created_at": "2015-12-04T10:33:56.698Z",
- "system": false,
- "noteable_id": 377,
- "noteable_type": "Issue"
- },
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- }
-]
-```
## Get all impersonation tokens of a user
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index bd53f80ce14..a047e809788 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -1,11 +1,11 @@
# Analyze project code quality with Code Climate CLI
-This example shows how to run [Code Climate CLI][cli] on your code by using\
+This example shows how to run [Code Climate CLI][cli] on your code by using
GitLab CI and Docker.
-First, you need GitLab Runner with [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
+First, you need GitLab Runner with [docker-in-docker executor][dind].
-Once you setup the Runner add new job to `.gitlab-ci.yml`:
+Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codeclimate`:
```yaml
codeclimate:
@@ -25,4 +25,10 @@ codeclimate:
This will create a `codeclimate` job in your CI pipeline and will allow you to
download and analyze the report artifact in JSON format.
+For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically
+extracted and shown right in the merge request widget. [Learn more on code quality
+diffs in merge requests](http://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.md).
+
[cli]: https://github.com/codeclimate/codeclimate
+[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
+[ee]: https://about.gitlab.com/gitlab-ee/
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index b36fd52603b..acd5e3c2093 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -12,7 +12,7 @@ Both EE and CE require some add-on components called gitlab-shell and Gitaly. Th
You can imagine GitLab as a physical office.
-**The repositories** are the goods GitLab handling.
+**The repositories** are the goods GitLab handles.
They can be stored in a warehouse.
This can be either a hard disk, or something more complex, such as a NFS filesystem;
@@ -54,7 +54,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell
### Components
-![GitLab Diagram Overview](gitlab_architecture_diagram.png)
+<img src="https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/pub?w=987&amp;h=797">
_[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_
@@ -66,7 +66,9 @@ When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to reso
The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access.
-Gitaly executes git operations from gitlab-shell and Workhorse, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files)
+Gitaly executes git operations from gitlab-shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files).
+
+You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/infrastructure/production-architecture/).
### Installation Folder Summary
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index b4ffd57afbb..d2442a4fbde 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -207,7 +207,9 @@ its class in an annotation.
>**Note:**
The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that.
Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure
-to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md)
+to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md).
+>**Note:**
+If you would like to use the Registry, you will also need to ensure your Ingress supports a [sufficiently large request size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size).
#### Preserving Source IPs
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index 305b4593c73..b8bc0795f2e 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -141,7 +141,7 @@ Once you [have configured](#configuration) GitLab Runner in your `values.yml` fi
run the following:
```bash
-helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner
+helm install --namespace <NAMESPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner
```
- `<NAMESPACE>` is the Kubernetes namespace where you want to install the GitLab Runner.
@@ -153,7 +153,7 @@ helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE>
Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade`
```bash
-helm upgrade --namepace <NAMEPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner
+helm upgrade --namespace <NAMESPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner
```
Where:
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index b0145b0a759..3fda47b9e34 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -126,7 +126,7 @@ which visibility level you select on project settings.
## GitLab CI
GitLab CI permissions rely on the role the user has in GitLab. There are four
-permission levels it total:
+permission levels in total:
- admin
- master
diff --git a/doc/user/project/img/project_settings_list.png b/doc/user/project/img/project_settings_list.png
deleted file mode 100644
index 0bb761b45c9..00000000000
--- a/doc/user/project/img/project_settings_list.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/img/accessing_integrations.png b/doc/user/project/integrations/img/accessing_integrations.png
deleted file mode 100644
index 3b941f64998..00000000000
--- a/doc/user/project/integrations/img/accessing_integrations.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md
index 99093ebaed5..e384ed57de9 100644
--- a/doc/user/project/integrations/index.md
+++ b/doc/user/project/integrations/index.md
@@ -1,10 +1,8 @@
# Project integrations
-You can find the available integrations under the **Integrations** page by
-navigating to the cog icon in the upper right corner of your project. You need
-to have at least [master permission][permissions] on the project.
-
-![Accessing the integrations](img/accessing_integrations.png)
+You can find the available integrations under your project's
+**Settings ➔ Integrations** page. You need to have at least
+[master permission][permissions] on the project.
## Project services
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 31baea507d7..51989ccaaea 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -6,18 +6,13 @@ functionality to GitLab.
## Accessing the project services
-You can find the available services under the **Integrations** page in your
-project's settings.
+You can find the available services under your project's
+**Settings ➔ Integrations** page.
-1. Navigate to the cog icon in the upper right corner of your project. You need
- to have at least [master permission][permissions] on the project.
+There are more than 20 services to integrate with. Click on the one that you
+want to configure.
- ![Accessing the services](img/accessing_integrations.png)
-
-1. There are more than 20 services to integrate with. Click on the one that you
- want to configure.
-
- ![Project services list](img/project_services.png)
+ ![Project services list](img/project_services.png)
Below, you will find a list of the currently supported ones accompanied with
comprehensive documentation.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index d0bb1cd11a8..0517ed3ec18 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -14,11 +14,8 @@ to the webhook URL.
Webhooks can be used to update an external issue tracker, trigger CI jobs,
update a backup mirror, or even deploy to your production server.
-Navigate to the webhooks page by going to the **Integrations** page from your
-project's settings which can be found under the wheel icon in the upper right
-corner.
-
-![Accessing the integrations](img/accessing_integrations.png)
+Navigate to the webhooks page by going to your project's
+**Settings ➔ Integrations**.
## Webhook endpoint tips
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 9598cb801be..fe87e6f9495 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -1,4 +1,4 @@
-# GitLab Issues Documentation
+# Issues documentation
The GitLab Issue Tracker is an advanced and complete tool
for tracking the evolution of a new idea or the process
@@ -41,13 +41,13 @@ The image bellow illustrates how an issue looks like:
Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md).
-## New Issue
+## New issue
Read through the [documentation on creating issues](create_new_issue.md).
## Closing issues
-Read through the distinct ways to [close issues](closing_issues.md) on GitLab.
+Learn distinct ways to [close issues](closing_issues.md) in GitLab.
## Create a merge request from an issue
@@ -84,7 +84,7 @@ Learn more about them on the [issue templates documentation](../../project/descr
Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests.
-### GitLab Issue Board
+### Issue Board
The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to
enhance your workflow by organizing and prioritizing issues in GitLab.
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 1b42c43cf8f..1d2eba4f74b 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -1,12 +1,7 @@
# Pipelines settings
-To reach the pipelines settings:
-
-1. Navigate to your project and click the cog icon in the upper right corner.
-
- ![Project settings menu](../img/project_settings_list.png)
-
-1. Select **Pipelines** from the menu.
+To reach the pipelines settings navigate to your project's
+**Settings ➔ CI/CD Pipelines**.
The following settings can be configured per project.
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index f7a686d2ccf..7650020b37e 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -27,11 +27,8 @@ See the [Changelog](#changelog) section for changes over time.
To protect a branch, you need to have at least Master permission level. Note
that the `master` branch is protected by default.
-1. Navigate to the main page of the project.
-1. In the upper right corner, click the settings wheel and select **Protected branches**.
-
- ![Project settings list](img/project_settings_list.png)
-
+1. Navigate to your project's **Settings ➔ Repository**
+1. Scroll to find the **Protected branches** section.
1. From the **Branch** dropdown menu, select the branch you want to protect and
click **Protect**. In the screenshot below, we chose the `develop` branch.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7ae2f3cad40..88f91c07194 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -94,6 +94,7 @@ module API
mount ::API::DeployKeys
mount ::API::Deployments
mount ::API::Environments
+ mount ::API::Events
mount ::API::Features
mount ::API::Files
mount ::API::Groups
diff --git a/lib/api/events.rb b/lib/api/events.rb
new file mode 100644
index 00000000000..dabdf579119
--- /dev/null
+++ b/lib/api/events.rb
@@ -0,0 +1,86 @@
+module API
+ class Events < Grape::API
+ include PaginationParams
+
+ helpers do
+ params :event_filter_params do
+ optional :action, type: String, values: Event.actions, desc: 'Event action to filter on'
+ optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on'
+ optional :before, type: Date, desc: 'Include only events created before this date'
+ optional :after, type: Date, desc: 'Include only events created after this date'
+ end
+
+ params :sort_params do
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return events sorted in ascending and descending order'
+ end
+
+ def present_events(events)
+ events = events.reorder(created_at: params[:sort])
+
+ present paginate(events), with: Entities::Event
+ end
+ end
+
+ resource :events do
+ desc "List currently authenticated user's events" do
+ detail 'This feature was introduced in GitLab 9.3.'
+ success Entities::Event
+ end
+ params do
+ use :pagination
+ use :event_filter_params
+ use :sort_params
+ end
+ get do
+ authenticate!
+
+ events = EventsFinder.new(params.merge(source: current_user, current_user: current_user)).execute.preload(:author, :target)
+
+ present_events(events)
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID or Username of the user'
+ end
+ resource :users do
+ desc 'Get the contribution events of a specified user' do
+ detail 'This feature was introduced in GitLab 8.13.'
+ success Entities::Event
+ end
+ params do
+ use :pagination
+ use :event_filter_params
+ use :sort_params
+ end
+ get ':id/events' do
+ user = find_user(params[:id])
+ not_found!('User') unless user
+
+ events = EventsFinder.new(params.merge(source: user, current_user: current_user)).execute.preload(:author, :target)
+
+ present_events(events)
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc "List a Project's visible events" do
+ success Entities::Event
+ end
+ params do
+ use :pagination
+ use :event_filter_params
+ use :sort_params
+ end
+ get ":id/events" do
+ events = EventsFinder.new(params.merge(source: user_project, current_user: current_user)).execute.preload(:author, :target)
+
+ present_events(events)
+ end
+ end
+ end
+end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index deac3934d57..56046742e08 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -167,16 +167,6 @@ module API
user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics]
end
- desc 'Get events for a single project' do
- success Entities::Event
- end
- params do
- use :pagination
- end
- get ":id/events" do
- present paginate(user_project.events.recent), with: Entities::Event
- end
-
desc 'Fork new project for the current user or provided namespace.' do
success Entities::Project
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index e8694e90cf2..3f87a403a09 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -328,27 +328,6 @@ module API
end
end
- desc 'Get the contribution events of a specified user' do
- detail 'This feature was introduced in GitLab 8.13.'
- success Entities::Event
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the user'
- use :pagination
- end
- get ':id/events' do
- user = User.find_by(id: params[:id])
- not_found!('User') unless user
-
- events = user.events.
- merge(ProjectsFinder.new(current_user: current_user).execute).
- references(:project).
- with_associations.
- recent
-
- present paginate(events), with: Entities::Event
- end
-
params do
requires :user_id, type: Integer, desc: 'The ID of the user'
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 31345403702..613b1edba36 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -31,7 +31,7 @@ feature 'Environments page', :feature, :js do
it 'should show one environment' do
visit namespace_project_environments_path(project.namespace, project, scope: 'available')
expect(page).to have_css('.environments-container')
- expect(page.all('tbody > tr').length).to eq(1)
+ expect(page.all('.environment-name').length).to eq(1)
end
end
@@ -59,7 +59,7 @@ feature 'Environments page', :feature, :js do
it 'should show one environment' do
visit namespace_project_environments_path(project.namespace, project, scope: 'stopped')
expect(page).to have_css('.environments-container')
- expect(page.all('tbody > tr').length).to eq(1)
+ expect(page.all('.environment-name').length).to eq(1)
end
end
end
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index cef315ac9cd..fac4506bdf6 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -14,7 +14,7 @@ feature 'Visibility settings', feature: true, js: true do
visibility_select_container = find('.js-visibility-select')
expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s
- expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
+ expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
end
scenario 'project visibility description updates on change' do
@@ -41,7 +41,7 @@ feature 'Visibility settings', feature: true, js: true do
expect(visibility_select_container).not_to have_select '.visibility-select'
expect(visibility_select_container).to have_content 'Public'
- expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
+ expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
end
end
end
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index a23c4ca2b92..8509551ce4a 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -24,8 +24,8 @@ describe 'Unsubscribe links', feature: true do
visit body_link
expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
- expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference})))
- expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?))
+ expect(page).to have_text(%(Unsubscribe from issue))
+ expect(page).to have_text(%(Are you sure you want to unsubscribe from the issue: #{issue.title} (#{issue.to_reference})?))
expect(issue.subscribed?(recipient, project)).to be_truthy
click_link 'Unsubscribe'
diff --git a/spec/finders/events_finder_spec.rb b/spec/finders/events_finder_spec.rb
new file mode 100644
index 00000000000..30a2bd14f10
--- /dev/null
+++ b/spec/finders/events_finder_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe EventsFinder do
+ let(:user) { create(:user) }
+ let(:other_user) { create(:user) }
+ let(:project1) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
+ let(:project2) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
+ let(:closed_issue) { create(:closed_issue, project: project1, author: user) }
+ let(:opened_merge_request) { create(:merge_request, source_project: project2, author: user) }
+ let!(:closed_issue_event) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
+ let!(:opened_merge_request_event) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 1, 31)) }
+ let(:closed_issue2) { create(:closed_issue, project: project1, author: user) }
+ let(:opened_merge_request2) { create(:merge_request, source_project: project2, author: user) }
+ let!(:closed_issue_event2) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 2, 2)) }
+ let!(:opened_merge_request_event2) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 2, 2)) }
+
+ context 'when targeting a user' do
+ it 'returns events between specified dates filtered on action and type' do
+ events = described_class.new(source: user, current_user: user, action: 'created', target_type: 'merge_request', after: Date.new(2017, 1, 1), before: Date.new(2017, 2, 1)).execute
+
+ expect(events).to eq([opened_merge_request_event])
+ end
+
+ it 'does not return events the current_user does not have access to' do
+ events = described_class.new(source: user, current_user: other_user).execute
+
+ expect(events).not_to include(opened_merge_request_event)
+ end
+ end
+
+ context 'when targeting a project' do
+ it 'returns project events between specified dates filtered on action and type' do
+ events = described_class.new(source: project1, current_user: user, action: 'closed', target_type: 'issue', after: Date.new(2016, 12, 1), before: Date.new(2017, 1, 1)).execute
+
+ expect(events).to eq([closed_issue_event])
+ end
+
+ it 'does not return events the current_user does not have access to' do
+ events = described_class.new(source: project2, current_user: other_user).execute
+
+ expect(events).to be_empty
+ end
+ end
+end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 54c5ba57bdf..a695621b87a 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -257,7 +257,7 @@ describe ProjectsHelper do
result = helper.project_feature_access_select(:issues_access_level)
expect(result).to include("Disabled")
expect(result).to include("Only team members")
- expect(result).not_to include("Everyone with access")
+ expect(result).to have_selector('option[disabled]', text: "Everyone with access")
end
end
@@ -272,7 +272,7 @@ describe ProjectsHelper do
expect(result).to include("Disabled")
expect(result).to include("Only team members")
- expect(result).not_to include("Everyone with access")
+ expect(result).to have_selector('option[disabled]', text: "Everyone with access")
expect(result).to have_selector('option[selected]', text: "Only team members")
end
end
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index 8942b00b128..ad19cf9263d 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -37,7 +37,7 @@ describe VisibilityLevelHelper do
it "describes public projects" do
expect(project_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC))
- .to eq "The project can be cloned without any authentication."
+ .to eq "The project can be accessed without any authentication."
end
end
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 398c593eec2..ebfd60198b2 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -71,7 +71,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('should render a table with the received pipelines', (done) => {
setTimeout(() => {
- expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
+ expect(this.component.$el.querySelectorAll('.ci-table .commit').length).toEqual(1);
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.empty-state')).toBe(null);
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null);
@@ -108,7 +108,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.js-empty-state')).toBe(null);
- expect(this.component.$el.querySelector('table')).toBe(null);
+ expect(this.component.$el.querySelector('.ci-table')).toBe(null);
done();
}, 0);
});
diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js
index c31642ac788..6639a6b5e7b 100644
--- a/spec/javascripts/environments/environment_spec.js
+++ b/spec/javascripts/environments/environment_spec.js
@@ -271,7 +271,7 @@ describe('Environment', () => {
// wait for next async request
setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
- expect(component.$el.querySelector('td.text-center > a.btn').textContent).toContain('Show all');
+ expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all');
Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
done();
diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js
index effbc6c3ee1..2862971bec4 100644
--- a/spec/javascripts/environments/environment_table_spec.js
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -29,6 +29,6 @@ describe('Environment item', () => {
},
}).$mount();
- expect(component.$el.tagName).toEqual('TABLE');
+ expect(component.$el.getAttribute('class')).toContain('ci-table');
});
});
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
new file mode 100644
index 00000000000..a19870a95e8
--- /dev/null
+++ b/spec/requests/api/events_spec.rb
@@ -0,0 +1,142 @@
+require 'spec_helper'
+
+describe API::Events, api: true do
+ include ApiHelpers
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:other_user) { create(:user, username: 'otheruser') }
+ let(:private_project) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
+ let(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
+ let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
+
+ describe 'GET /events' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/events')
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns users events' do
+ get api('/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31', user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+ end
+ end
+
+ describe 'GET /users/:id/events' do
+ context "as a user that cannot see the event's project" do
+ it 'returns no events' do
+ get api("/users/#{user.id}/events", other_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_empty
+ end
+ end
+
+ context "as a user that can see the event's project" do
+ it 'accepts a username' do
+ get api("/users/#{user.username}/events", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+
+ it 'returns the events' do
+ get api("/users/#{user.id}/events", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+
+ context 'when there are multiple events from different projects' do
+ let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
+
+ before do
+ second_note.project.add_user(user, :developer)
+
+ [second_note].each do |note|
+ EventCreateService.new.leave_note(note, user)
+ end
+ end
+
+ it 'returns events in the correct order (from newest to oldest)' do
+ get api("/users/#{user.id}/events", user)
+
+ comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
+ close_events = json_response.select { |e| e['action_name'] == 'closed' }
+
+ expect(comment_events[0]['target_id']).to eq(second_note.id)
+ expect(close_events[0]['target_id']).to eq(closed_issue.id)
+ end
+
+ it 'accepts filter parameters' do
+ get api("/users/#{user.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
+
+ expect(json_response.size).to eq(1)
+ expect(json_response[0]['target_id']).to eq(closed_issue.id)
+ end
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get api('/users/42/events', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
+
+ describe 'GET /projects/:id/events' do
+ context 'when unauthenticated ' do
+ it 'returns 404 for private project' do
+ get api("/projects/#{private_project.id}/events")
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 200 status for a public project' do
+ public_project = create(:empty_project, :public)
+
+ get api("/projects/#{public_project.id}/events")
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when not permitted to read' do
+ it 'returns 404' do
+ get api("/projects/#{private_project.id}/events", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns project events' do
+ get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+
+ it 'returns 404 if project does not exist' do
+ get api("/projects/1234/events", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 5c13cea69fb..86c57204971 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -762,64 +762,6 @@ describe API::Projects do
end
end
- describe 'GET /projects/:id/events' do
- shared_examples_for 'project events response' do
- it 'returns the project events' do
- member = create(:user)
- create(:project_member, :developer, user: member, project: project)
- note = create(:note_on_issue, note: 'What an awesome day!', project: project)
- EventCreateService.new.leave_note(note, note.author)
-
- get api("/projects/#{project.id}/events", current_user)
-
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
-
- first_event = json_response.first
- expect(first_event['action_name']).to eq('commented on')
- expect(first_event['note']['body']).to eq('What an awesome day!')
-
- last_event = json_response.last
-
- expect(last_event['action_name']).to eq('joined')
- expect(last_event['project_id'].to_i).to eq(project.id)
- expect(last_event['author_username']).to eq(member.username)
- expect(last_event['author']['name']).to eq(member.name)
- end
- end
-
- context 'when unauthenticated' do
- it_behaves_like 'project events response' do
- let(:project) { create(:empty_project, :public) }
- let(:current_user) { nil }
- end
- end
-
- context 'when authenticated' do
- context 'valid request' do
- it_behaves_like 'project events response' do
- let(:current_user) { user }
- end
- end
-
- it 'returns a 404 error if not found' do
- get api('/projects/42/events', user)
-
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
- end
-
- it 'returns a 404 error if user is not a member' do
- other_user = create(:user)
-
- get api("/projects/#{project.id}/events", other_user)
-
- expect(response).to have_http_status(404)
- end
- end
- end
-
describe 'GET /projects/:id/users' do
shared_examples_for 'project users response' do
it 'returns the project users' do
@@ -1480,7 +1422,7 @@ describe API::Projects do
expect(json_response['owner']['id']).to eq(user2.id)
expect(json_response['namespace']['id']).to eq(user2.namespace.id)
expect(json_response['forked_from_project']['id']).to eq(project.id)
- expect(json_response['import_status']).to eq('started')
+ expect(json_response['import_status']).to eq('scheduled')
expect(json_response).to include("import_error")
end
@@ -1493,7 +1435,7 @@ describe API::Projects do
expect(json_response['owner']['id']).to eq(admin.id)
expect(json_response['namespace']['id']).to eq(admin.namespace.id)
expect(json_response['forked_from_project']['id']).to eq(project.id)
- expect(json_response['import_status']).to eq('started')
+ expect(json_response['import_status']).to eq('scheduled')
expect(json_response).to include("import_error")
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 1c33b8f9502..4efc3e1a1e2 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1130,83 +1130,6 @@ describe API::Users do
end
end
- describe 'GET /users/:id/events' do
- let(:user) { create(:user) }
- let(:project) { create(:empty_project) }
- let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) }
-
- before do
- project.add_user(user, :developer)
- EventCreateService.new.leave_note(note, user)
- end
-
- context "as a user than cannot see the event's project" do
- it 'returns no events' do
- other_user = create(:user)
-
- get api("/users/#{user.id}/events", other_user)
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_empty
- end
- end
-
- context "as a user than can see the event's project" do
- context 'joined event' do
- it 'returns the "joined" event' do
- get api("/users/#{user.id}/events", user)
-
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
-
- comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
-
- expect(comment_event['project_id'].to_i).to eq(project.id)
- expect(comment_event['author_username']).to eq(user.username)
- expect(comment_event['note']['id']).to eq(note.id)
- expect(comment_event['note']['body']).to eq('What an awesome day!')
-
- joined_event = json_response.find { |e| e['action_name'] == 'joined' }
-
- expect(joined_event['project_id'].to_i).to eq(project.id)
- expect(joined_event['author_username']).to eq(user.username)
- expect(joined_event['author']['name']).to eq(user.name)
- end
- end
-
- context 'when there are multiple events from different projects' do
- let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
- let(:third_note) { create(:note_on_issue, project: project) }
-
- before do
- second_note.project.add_user(user, :developer)
-
- [second_note, third_note].each do |note|
- EventCreateService.new.leave_note(note, user)
- end
- end
-
- it 'returns events in the correct order (from newest to oldest)' do
- get api("/users/#{user.id}/events", user)
-
- comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
-
- expect(comment_events[0]['target_id']).to eq(third_note.id)
- expect(comment_events[1]['target_id']).to eq(second_note.id)
- expect(comment_events[2]['target_id']).to eq(note.id)
- end
- end
- end
-
- it 'returns a 404 error if not found' do
- get api('/users/42/events', user)
-
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 User Not Found')
- end
- end
-
context "user activities", :redis do
let!(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) }
let!(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) }