Commit 618eb57f authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee-2018-03-06' into 'master'

CE upstream - 2018-03-06 20:12 UTC

Closes #4170

See merge request gitlab-org/gitlab-ee!4869
parents 35286350 7896f160
...@@ -140,3 +140,4 @@ export default { ...@@ -140,3 +140,4 @@ export default {
</div> </div>
</div> </div>
</template> </template>
<script>
/* global ListIssue */
import _ from 'underscore';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
export default {
name: 'BoardProjectSelect',
components: {
loadingIcon,
},
props: {
groupId: {
type: Number,
required: true,
default: 0,
},
},
data() {
return {
loading: true,
selectedProject: {},
};
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
},
},
mounted() {
$(this.$refs.projectsDropdown).glDropdown({
filterable: true,
filterRemote: true,
search: {
fields: ['name_with_namespace'],
},
clicked: ({ $el, e }) => {
e.preventDefault();
this.selectedProject = {
id: $el.data('project-id'),
name: $el.data('project-name'),
};
eventHub.$emit('setSelectedProject', this.selectedProject);
},
selectable: true,
data: (term, callback) => {
this.loading = true;
return Api.groupProjects(this.groupId, term, (projects) => {
this.loading = false;
callback(projects);
});
},
renderRow(project) {
return `
<li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
${_.escape(project.name)}
</a>
</li>
`;
},
text: project => project.name,
});
},
};
</script>
<template>
<div>
<label class="label-light prepend-top-10">
Project
</label>
<div
ref="projectsDropdown"
class="dropdown"
>
<button
class="dropdown-menu-toggle wide"
type="button"
data-toggle="dropdown"
aria-expanded="false"
>
{{ selectedProjectName }}
<i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">
<span>Projects</span>
<button
aria-label="Close"
type="button"
class="dropdown-title-button dropdown-menu-close"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-times dropdown-menu-close-icon"
>
</i>
</button>
</div>
<div class="dropdown-input">
<input
class="dropdown-input-field"
type="search"
placeholder="Search projects"
/>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search"
>
</i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
</div>
</template>
...@@ -13,6 +13,7 @@ import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import ...@@ -13,6 +13,7 @@ import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import
import './models/issue'; import './models/issue';
import './models/list'; import './models/list';
import './models/milestone'; import './models/milestone';
import './models/project';
import './models/assignee'; import './models/assignee';
import './stores/boards_store'; import './stores/boards_store';
import './stores/modal_store'; import './stores/modal_store';
......
export default class IssueProject {
constructor(obj) {
this.id = obj.id;
this.path = obj.path;
}
}
...@@ -117,7 +117,10 @@ ...@@ -117,7 +117,10 @@
</script> </script>
<template> <template>
<section class="settings no-animate expanded"> <section
id="cluster-applications"
class="settings no-animate expanded"
>
<div class="settings-header"> <div class="settings-header">
<h4> <h4>
{{ s__('ClusterIntegration|Applications') }} {{ s__('ClusterIntegration|Applications') }}
......
...@@ -7,34 +7,82 @@ ...@@ -7,34 +7,82 @@
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store'; import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
export default { export default {
components: { components: {
Graph, Graph,
GraphGroup, GraphGroup,
EmptyState, EmptyState,
}, },
data() { props: {
const metricsData = document.querySelector('#prometheus-graphs').dataset; hasMetrics: {
const store = new MonitoringStore(); type: Boolean,
required: false,
default: true,
},
showLegend: {
type: Boolean,
required: false,
default: true,
},
showPanels: {
type: Boolean,
required: false,
default: true,
},
forceSmallGraph: {
type: Boolean,
required: false,
default: false,
},
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: true,
},
clustersPath: {
type: String,
required: true,
},
tagsPath: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
metricsEndpoint: {
type: String,
required: true,
},
deploymentEndpoint: {
type: String,
required: false,
default: null,
},
emptyGettingStartedSvgPath: {
type: String,
required: true,
},
emptyLoadingSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
},
},
data() {
return { return {
store, store: new MonitoringStore(),
state: 'gettingStarted', state: 'gettingStarted',
hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
clustersPath: metricsData.clustersPath,
tagsPath: metricsData.tagsPath,
projectPath: metricsData.projectPath,
metricsEndpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint,
emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath,
emptyLoadingSvgPath: metricsData.emptyLoadingSvgPath,
emptyUnableToConnectSvgPath: metricsData.emptyUnableToConnectSvgPath,
showEmptyState: true, showEmptyState: true,
updateAspectRatio: false, updateAspectRatio: false,
updatedAspectRatios: 0, updatedAspectRatios: 0,
...@@ -67,6 +115,7 @@ ...@@ -67,6 +115,7 @@
window.addEventListener('resize', this.resizeThrottled, false); window.addEventListener('resize', this.resizeThrottled, false);
} }
}, },
methods: { methods: {
getGraphsData() { getGraphsData() {
this.state = 'loading'; this.state = 'loading';
...@@ -115,6 +164,7 @@ ...@@ -115,6 +164,7 @@
v-for="(groupData, index) in store.groups" v-for="(groupData, index) in store.groups"
:key="index" :key="index"
:name="groupData.group" :name="groupData.group"
:show-panels="showPanels"
> >
<graph <graph
v-for="(graphData, index) in groupData.metrics" v-for="(graphData, index) in groupData.metrics"
...@@ -125,6 +175,8 @@ ...@@ -125,6 +175,8 @@
:deployment-data="store.deploymentData" :deployment-data="store.deploymentData"
:project-path="projectPath" :project-path="projectPath"
:tags-path="tagsPath" :tags-path="tagsPath"
:show-legend="showLegend"
:small-graph="forceSmallGraph"
/> />
</graph-group> </graph-group>
</div> </div>
......
...@@ -52,6 +52,16 @@ ...@@ -52,6 +52,16 @@
type: String, type: String,
required: true, required: true,
}, },
showLegend: {
type: Boolean,
required: false,
default: true,
},
smallGraph: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
...@@ -130,7 +140,7 @@ ...@@ -130,7 +140,7 @@
const breakpointSize = bp.getBreakpointSize(); const breakpointSize = bp.getBreakpointSize();
const query = this.graphData.queries[0]; const query = this.graphData.queries[0];
this.margin = measurements.large.margin; this.margin = measurements.large.margin;
if (breakpointSize === 'xs' || breakpointSize === 'sm') { if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300; this.graphHeight = 300;
this.margin = measurements.small.margin; this.margin = measurements.small.margin;
this.measurements = measurements.small; this.measurements = measurements.small;
...@@ -182,7 +192,9 @@ ...@@ -182,7 +192,9 @@
this.graphHeightOffset, this.graphHeightOffset,
); );
if (this.timeSeries.length > 3) { if (!this.showLegend) {
this.baseGraphHeight -= 50;
} else if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
} }
...@@ -255,6 +267,7 @@ ...@@ -255,6 +267,7 @@
:time-series="timeSeries" :time-series="timeSeries"
:unit-of-display="unitOfDisplay" :unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex" :current-data-index="currentDataIndex"
:show-legend-group="showLegend"
/> />
<svg <svg
class="graph-data" class="graph-data"
......
...@@ -39,6 +39,11 @@ ...@@ -39,6 +39,11 @@
type: Number, type: Number,
required: true, required: true,
}, },
showLegendGroup: {
type: Boolean,
required: false,
default: true,
},
}, },
data() { data() {
return { return {
...@@ -57,8 +62,9 @@ ...@@ -57,8 +62,9 @@
}, },
rectTransform() { rectTransform() {
const yCoordinate = ((this.graphHeight - this.margin.top) / 2) const yCoordinate = (((this.graphHeight - this.margin.top)
+ (this.yLabelWidth / 2) + 10 || 0; + this.measurements.axisLabelLineOffset) / 2)
+ (this.yLabelWidth / 2) || 0;
return `translate(0, ${yCoordinate}) rotate(-90)`; return `translate(0, ${yCoordinate}) rotate(-90)`;
}, },
...@@ -166,6 +172,7 @@ ...@@ -166,6 +172,7 @@
> >
Time Time
</text> </text>
<template v-if="showLegendGroup">
<g <g
class="legend-group" class="legend-group"
v-for="(series, index) in timeSeries" v-for="(series, index) in timeSeries"
...@@ -200,5 +207,6 @@ ...@@ -200,5 +207,6 @@
{{ legendTitle }} {{ formatMetricUsage(series) }} {{ legendTitle }} {{ formatMetricUsage(series) }}
</text> </text>
</g> </g>
</template>
</g> </g>
</template> </template>
...@@ -5,12 +5,20 @@ ...@@ -5,12 +5,20 @@
type: String, type: String,
required: true, required: true,
}, },
showPanels: {
type: Boolean,
required: false,
default: true,
},
}, },
}; };
</script> </script>
<template> <template>
<div class="panel panel-default prometheus-panel"> <div
v-if="showPanels"
class="panel panel-default prometheus-panel"
>
<div class="panel-heading"> <div class="panel-heading">
<h4>{{ name }}</h4> <h4>{{ name }}</h4>
</div> </div>
...@@ -18,4 +26,10 @@ ...@@ -18,4 +26,10 @@
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
<div
v-else
class="prometheus-graph-group"
>
<slot></slot>
</div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import Dashboard from './components/dashboard.vue'; import Dashboard from './components/dashboard.vue';
export default () => new Vue({ export default () => {
el: '#prometheus-graphs', const el = document.getElementById('prometheus-graphs');
render: createElement => createElement(Dashboard),
}); if (el && el.dataset) {
// eslint-disable-next-line no-new
new Vue({
el,
render(createElement) {
return createElement(Dashboard, {
props: {
...el.dataset,
hasMetrics: convertPermissionToBoolean(el.dataset.hasMetrics),
},
});
},
});
}
};
...@@ -40,6 +40,9 @@ export default class MonitoringService { ...@@ -40,6 +40,9 @@ export default class MonitoringService {
} }
getDeploymentData() { getDeploymentData() {
if (!this.deploymentEndpoint) {
return Promise.resolve([]);
}
return backOffRequest(() => axios.get(this.deploymentEndpoint)) return backOffRequest(() => axios.get(this.deploymentEndpoint))
.then(resp => resp.data) .then(resp => resp.data)
.then((response) => { .then((response) => {
......
export default {
animation: 200,
forceFallback: true,
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
};
...@@ -529,7 +529,8 @@ ...@@ -529,7 +529,8 @@
} }
> text { > text {
font-size: 12px; fill: $theme-gray-600;
font-size: 10px;
} }
} }
......
...@@ -9,6 +9,7 @@ class Commit ...@@ -9,6 +9,7 @@ class Commit
include Mentionable include Mentionable
include Referable include Referable
include StaticModel include StaticModel
include ::Gitlab::Utils::StrongMemoize
attr_mentionable :safe_message, pipeline: :single_line attr_mentionable :safe_message, pipeline: :single_line
...@@ -225,11 +226,13 @@ class Commit ...@@ -225,11 +226,13 @@ class Commit
end end
def parents def parents
@parents ||= parent_ids.map { |id| project.commit(id) } @parents ||= parent_ids.map { |oid| Commit.lazy(project, oid) }
end end
def parent def parent
@parent ||= project.commit(self.parent_id) if self.parent_id strong_memoize(:parent) do
project.commit_by(oid: self.parent_id) if self.parent_id
end
end end
def notes def notes
......
...@@ -281,7 +281,8 @@ class Project < ActiveRecord::Base ...@@ -281,7 +281,8 @@ class Project < ActiveRecord::Base
scope :without_storage_feature, ->(feature) { where('storage_version < :version OR storage_version IS NULL', version: HASHED_STORAGE_FEATURES[feature]) } scope :without_storage_feature, ->(feature) { where('storage_version < :version OR storage_version IS NULL', version: HASHED_STORAGE_FEATURES[feature]) }
scope :with_unmigrated_storage, -> { where('storage_version < :version OR storage_version IS NULL', version: LATEST_STORAGE_VERSION) } scope :with_unmigrated_storage, -> { where('storage_version < :version OR storage_version IS NULL', version: LATEST_STORAGE_VERSION) }
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } # last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push
scope :sorted_by_activity, -> { reorder("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC") }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
...@@ -789,7 +790,7 @@ class Project < ActiveRecord::Base ...@@ -789,7 +790,7 @@ class Project < ActiveRecord::Base
end end
def last_activity_date def last_activity_date
last_repository_updated_at || last_activity_at || updated_at [last_activity_at, last_repository_updated_at, updated_at].compact.max
end end
def project_id def project_id
......
...@@ -15,7 +15,8 @@ ...@@ -15,7 +15,8 @@
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'), "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'), "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'), "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
"additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json), "metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json),
"deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json),
"project-path": project_path(@project), "project-path": project_path(@project),
"tags-path": project_tags_path(@project), "tags-path": project_tags_path(@project),
"has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } } "has-metrics": "#{@environment.has_metrics?}" } }
- @no_container = true - @no_container = true
- @sort ||= sort_value_recently_updated - @sort ||= sort_value_recently_updated
- page_title s_('TagsPage|Tags') - page_title s_('TagsPage|Tags')
- add_to_breadcrumbs("Repository", project_tree_path(@project))
.flex-list{ class: container_class } .flex-list{ class: container_class }
.top-area.adjust .top-area.adjust
......
...@@ -48,7 +48,6 @@ ...@@ -48,7 +48,6 @@
- pipeline_default:build_trace_sections - pipeline_default:build_trace_sections
- pipeline_default:pipeline_metrics - pipeline_default:pipeline_metrics
- pipeline_default:pipeline_notification - pipeline_default:pipeline_notification
- pipeline_default:update_head_pipeline_for_merge_request
- pipeline_hooks:build_hooks - pipeline_hooks:build_hooks
- pipeline_hooks:pipeline_hooks - pipeline_hooks:pipeline_hooks
- pipeline_processing:build_finished - pipeline_processing:build_finished
...@@ -58,6 +57,7 @@ ...@@ -58,6 +57,7 @@
- pipeline_processing:pipeline_success - pipeline_processing:pipeline_success
- pipeline_processing:pipeline_update - pipeline_processing:pipeline_update
- pipeline_processing:stage_update - pipeline_processing:stage_update
- pipeline_processing:update_head_pipeline_for_merge_request
- repository_check:repository_check_clear - repository_check:repository_check_clear
- repository_check:repository_check_single_repository - repository_check:repository_check_single_repository
......
...@@ -2,6 +2,8 @@ class UpdateHeadPipelineForMergeRequestWorker ...@@ -2,6 +2,8 @@ class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker include ApplicationWorker
include PipelineQueue include PipelineQueue
queue_namespace :pipeline_processing
def perform(merge_request_id) def perform(merge_request_id)
merge_request = MergeRequest.find(merge_request_id) merge_request = MergeRequest.find(merge_request_id)
pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last
......
---
title: Remove extra breadcrumb on tags
merge_request: 17562
author: Takuya Noguchi
type: fixed
---
title: Started translation into Turkish, Indonesian and Filipino
merge_request: 17526
author:
type: other
---
title: Add one group board to Libre
merge_request:
author:
type: added
---
title: Fix project dashboard showing the wrong timestamps
merge_request:
author:
type: fixed
class AddGroupIdToBoardsCe < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
def up
return if group_id_exists?
add_column :boards, :group_id, :integer
add_foreign_key :boards, :namespaces, column: :group_id, on_delete: :cascade
add_concurrent_index :boards, :group_id
change_column_null :boards, :project_id, true
end
def down
return unless group_id_exists?
remove_foreign_key :boards, column: :group_id
remove_index :boards, :group_id if index_exists? :boards, :group_id
remove_column :boards, :group_id
execute "DELETE from boards WHERE project_id IS NULL"
change_column_null :boards, :project_id, false
end
private
def group_id_exists?
column_exists?(:boards, :group_id)
end
end
class MigrateUpdateHeadPipelineForMergeRequestSidekiqQueue < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
sidekiq_queue_migrate 'pipeline_default:update_head_pipeline_for_merge_request',
to: 'pipeline_processing:update_head_pipeline_for_merge_request'
end
def down
sidekiq_queue_migrate 'pipeline_processing:update_head_pipeline_for_merge_request',
to: 'pipeline_default:update_head_pipeline_for_merge_request'
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180306074045) do ActiveRecord::Schema.define(version: 20180307012445) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -139,6 +139,14 @@ CREATE EXTENSION pg_trgm; ...@@ -139,6 +139,14 @@ CREATE EXTENSION pg_trgm;
On some systems you may need to install an additional package (e.g. On some systems you may need to install an additional package (e.g.
`postgresql-contrib`) for this extension to become available. `postgresql-contrib`) for this extension to become available.
#### Additional requirements for GitLab Geo
If you are using [GitLab Geo](https://docs.gitlab.com/ee/development/geo.html), the [tracking database](https://docs.gitlab.com/ee/development/geo.html#geo-tracking-database) also requires the `postgres_fdw` extension.
```
CREATE EXTENSION postgres_fdw;
```
## Unicorn Workers ## Unicorn Workers
It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests. It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests.
......
...@@ -329,6 +329,16 @@ Click the button at the top right to toggle focus mode on and off. In focus mode ...@@ -329,6 +329,16 @@ Click the button at the top right to toggle focus mode on and off. In focus mode
[Developers and up](../permissions.md) can use all the functionality of the [Developers and up](../permissions.md) can use all the functionality of the
Issue Board, that is create/delete lists and drag issues around. Issue Board, that is create/delete lists and drag issues around.
## Group Issue Board
>Introduced in GitLab 10.6
Group issue board is analogous to project-level issue board and it is accessible at the group
navigation level. A group-level issue board allows you to view all issues from all projects in that group
(currently, it does not see issues from projects in subgroups). Similarly, you can only filter by group labels for these
boards. When updating milestones and labels for an issue through the sidebar update mechanism, again only
group-level objects are available.
## Tips ## Tips
A few things to remember: A few things to remember:
......
...@@ -2,12 +2,16 @@ class AddGroupIdToBoards < ActiveRecord::Migration ...@@ -2,12 +2,16 @@ class AddGroupIdToBoards < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
def up def up
return if group_id_exists?
change_column_null :boards, :project_id, true change_column_null :boards, :project_id, true
add_column :boards, :group_id, :integer add_column :boards, :group_id, :integer
end end
def down def down
return unless group_id_exists?
# We cannot rollback project_id not null constraint if there are records # We cannot rollback project_id not null constraint if there are records
# with null values. # with null values.
execute "DELETE from boards WHERE project_id IS NULL" execute "DELETE from boards WHERE project_id IS NULL"
...@@ -15,4 +19,10 @@ class AddGroupIdToBoards < ActiveRecord::Migration ...@@ -15,4 +19,10 @@ class AddGroupIdToBoards < ActiveRecord::Migration
remove_column :boards, :group_id remove_column :boards, :group_id
change_column :boards, :project_id, :integer, null: false change_column :boards, :project_id, :integer, null: false
end end
private
def group_id_exists?
column_exists?(:boards, :group_id)
end
end end
...@@ -6,14 +6,26 @@ class AddGroupBoardsIndexes < ActiveRecord::Migration ...@@ -6,14 +6,26 @@ class AddGroupBoardsIndexes < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
def up def up
return if foreign_key_exists?(:boards, :group_id)
add_concurrent_foreign_key :boards, :namespaces, column: :group_id, on_delete: :cascade add_concurrent_foreign_key :boards, :namespaces, column: :group_id, on_delete: :cascade
add_concurrent_index :boards, :group_id add_concurrent_index :boards, :group_id
end end
def down def down
return unless foreign_key_exists?(:boards, :group_id)
remove_foreign_key :boards, column: :group_id remove_foreign_key :boards, column: :group_id
remove_concurrent_index :boards, :group_id remove_concurrent_index :boards, :group_id
end end
private
def foreign_key_exists?(table, column)
foreign_keys(table).any? do |key|
key.options[:column] == column.to_s
end
end
end end
...@@ -132,6 +132,7 @@ module API ...@@ -132,6 +132,7 @@ module API
mount ::API::Events mount ::API::Events
mount ::API::Features mount ::API::Features
mount ::API::Files mount ::API::Files
mount ::API::GroupBoards
mount ::API::Groups mount ::API::Groups
mount ::API::GroupBoards mount ::API::GroupBoards
mount ::API::GroupMilestones mount ::API::GroupMilestones
......
...@@ -137,7 +137,8 @@ module Gitlab ...@@ -137,7 +137,8 @@ module Gitlab
job.create_job_artifacts_trace!( job.create_job_artifacts_trace!(
project: job.project, project: job.project,
file_type: :trace, file_type: :trace,
file: stream) file: stream,
file_sha256: Digest::SHA256.file(path).hexdigest)
end end
end end
......
...@@ -18,7 +18,10 @@ module Gitlab ...@@ -18,7 +18,10 @@ module Gitlab
'uk' => 'Українська', 'uk' => 'Українська',
'ja' => '日本語', 'ja' => '日本語',
'ko' => '한국어', 'ko' => '한국어',
'nl_NL' => 'Nederlands' 'nl_NL' => 'Nederlands',
'tr_TR' => 'Türkçe',
'id_ID' => 'Bahasa Indonesia',
'fil_PH' => 'Filipino'
}.freeze }.freeze
def available_locales def available_locales
......
...@@ -37,6 +37,14 @@ feature 'Dashboard Projects' do ...@@ -37,6 +37,14 @@ feature 'Dashboard Projects' do
expect(page).to have_xpath("//time[@datetime='#{project.last_repository_updated_at.getutc.iso8601}']") expect(page).to have_xpath("//time[@datetime='#{project.last_repository_updated_at.getutc.iso8601}']")
end end
it 'shows the last_activity_at attribute as the update date' do
project.update_attributes!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.now)
visit dashboard_projects_path
expect(page).to have_xpath("//time[@datetime='#{project.last_activity_at.getutc.iso8601}']")
end
end end
context 'when last_repository_updated_at and last_activity_at are missing' do context 'when last_repository_updated_at and last_activity_at are missing' do
......
require 'spec_helper'
describe Projects::EnvironmentsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'environments-project') }
let(:environment) { create(:environment, name: 'production', project: project) }
render_views
before(:all) do
clean_frontend_fixtures('environments/metrics')
end
before do
sign_in(admin)
end
it 'environments/metrics/metrics.html.raw' do |example|
get :metrics,
namespace_id: project.namespace,
project_id: project,
id: environment.id
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
...@@ -5,24 +5,35 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -5,24 +5,35 @@ import axios from '~/lib/utils/axios_utils';
import { metricsGroupsAPIResponse, mockApiEndpoint } from './mock_data'; import { metricsGroupsAPIResponse, mockApiEndpoint } from './mock_data';
describe('Dashboard', () => { describe('Dashboard', () => {
const fixtureName = 'environments/metrics/metrics.html.raw';
let DashboardComponent; let DashboardComponent;
let component;
preloadFixtures(fixtureName); const propsData = {
hasMetrics: false,
documentationPath: '/path/to/docs',
settingsPath: '/path/to/settings',
clustersPath: '/path/to/clusters',
tagsPath: '/path/to/tags',
projectPath: '/path/to/project',
metricsEndpoint: mockApiEndpoint,
deploymentEndpoint: null,
emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
emptyLoadingSvgPath: '/path/to/loading.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
};
beforeEach(() => { beforeEach(() => {
loadFixtures(fixtureName); setFixtures('<div class="prometheus-graphs"></div>');
DashboardComponent = Vue.extend(Dashboard); DashboardComponent = Vue.extend(Dashboard);
}); });
describe('no metrics are available yet', () => { describe('no metrics are available yet', () => {
it('shows a getting started empty state when no metrics are present', () => { it('shows a getting started empty state when no metrics are present', () => {
component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('#prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData,
}); });
component.$mount(); expect(component.$el.querySelector('.prometheus-graphs')).toBe(null);
expect(component.$el.querySelector('#prometheus-graphs')).toBe(null);
expect(component.state).toEqual('gettingStarted'); expect(component.state).toEqual('gettingStarted');
}); });
}); });
...@@ -30,11 +41,8 @@ describe('Dashboard', () => { ...@@ -30,11 +41,8 @@ describe('Dashboard', () => {
describe('requests information to the server', () => { describe('requests information to the server', () => {
let mock; let mock;
beforeEach(() => { beforeEach(() => {
document.querySelector('#prometheus-graphs').setAttribute('data-has-metrics', 'true');
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockApiEndpoint).reply(200, { mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
metricsGroupsAPIResponse,
});
}); });
afterEach(() => { afterEach(() => {
...@@ -42,14 +50,43 @@ describe('Dashboard', () => { ...@@ -42,14 +50,43 @@ describe('Dashboard', () => {
}); });
it('shows up a loading state', (done) => { it('shows up a loading state', (done) => {
component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('#prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true },
}); });
component.$mount();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(component.state).toEqual('loading'); expect(component.state).toEqual('loading');
done(); done();
}); });
}); });
it('hides the legend when showLegend is false', (done) => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showLegend: false },
});
setTimeout(() => {
expect(component.showEmptyState).toEqual(false);
expect(component.$el.querySelector('.legend-group')).toEqual(null);
expect(component.$el.querySelector('.prometheus-graph-group')).toBeTruthy();
done();
});
});
it('hides the group panels when showPanels is false', (done) => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false },
});
setTimeout(() => {
expect(component.showEmptyState).toEqual(false);
expect(component.$el.querySelector('.prometheus-panel')).toEqual(null);
expect(component.$el.querySelector('.prometheus-graph-group')).toBeTruthy();
done();
});
});
}); });
}); });
...@@ -413,7 +413,8 @@ describe Gitlab::Ci::Trace do ...@@ -413,7 +413,8 @@ describe Gitlab::Ci::Trace do
expect(build.job_artifacts_trace.file.filename).to eq('job.log') expect(build.job_artifacts_trace.file.filename).to eq('job.log')
expect(File.exist?(src_path)).to be_falsy expect(File.exist?(src_path)).to be_falsy
expect(src_checksum) expect(src_checksum)
.to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).digest) .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
end end
end end
...@@ -438,7 +439,8 @@ describe Gitlab::Ci::Trace do ...@@ -438,7 +439,8 @@ describe Gitlab::Ci::Trace do
expect(build.job_artifacts_trace.file.filename).to eq('job.log') expect(build.job_artifacts_trace.file.filename).to eq('job.log')
expect(build.old_trace).to be_nil expect(build.old_trace).to be_nil
expect(src_checksum) expect(src_checksum)
.to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).digest) .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
end end
end end
...@@ -457,7 +459,7 @@ describe Gitlab::Ci::Trace do ...@@ -457,7 +459,7 @@ describe Gitlab::Ci::Trace do
context 'when trace file stored in default path' do context 'when trace file stored in default path' do
let!(:build) { create(:ci_build, :success, :trace_live) } let!(:build) { create(:ci_build, :success, :trace_live) }
let!(:src_path) { trace.read { |s| return s.path } } let!(:src_path) { trace.read { |s| return s.path } }
let!(:src_checksum) { Digest::SHA256.file(src_path).digest } let!(:src_checksum) { Digest::SHA256.file(src_path).hexdigest }
it_behaves_like 'archive trace file' it_behaves_like 'archive trace file'
...@@ -483,7 +485,7 @@ describe Gitlab::Ci::Trace do ...@@ -483,7 +485,7 @@ describe Gitlab::Ci::Trace do
context 'when trace is stored in database' do context 'when trace is stored in database' do
let(:build) { create(:ci_build, :success) } let(:build) { create(:ci_build, :success) }
let(:trace_content) { 'Sample trace' } let(:trace_content) { 'Sample trace' }
let!(:src_checksum) { Digest::SHA256.digest(trace_content) } let!(:src_checksum) { Digest::SHA256.hexdigest(trace_content) }
before do before do
build.update_column(:trace, trace_content) build.update_column(:trace, trace_content)
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180307012445_migrate_update_head_pipeline_for_merge_request_sidekiq_queue.rb')
describe MigrateUpdateHeadPipelineForMergeRequestSidekiqQueue, :sidekiq, :redis do
include Gitlab::Database::MigrationHelpers
context 'when there are jobs in the queues' do
it 'correctly migrates queue when migrating up' do
Sidekiq::Testing.disable! do
stubbed_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1])
stubbed_worker(queue: 'pipeline_processing:update_head_pipeline_for_merge_request').perform_async('Something', [1])
described_class.new.up
expect(sidekiq_queue_length('pipeline_default:update_head_pipeline_for_merge_request')).to eq 0
expect(sidekiq_queue_length('pipeline_processing:update_head_pipeline_for_merge_request')).to eq 2
end
end
it 'does not affect other queues under the same namespace' do
Sidekiq::Testing.disable! do
stubbed_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1])
stubbed_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1])
stubbed_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1])
stubbed_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1])
described_class.new.up
expect(sidekiq_queue_length('pipeline_default:build_coverage')).to eq 1
expect(sidekiq_queue_length('pipeline_default:build_trace_sections')).to eq 1
expect(sidekiq_queue_length('pipeline_default:pipeline_metrics')).to eq 1
expect(sidekiq_queue_length('pipeline_default:pipeline_notification')).to eq 1
end
end
it 'correctly migrates queue when migrating down' do
Sidekiq::Testing.disable! do
stubbed_worker(queue: 'pipeline_processing:update_head_pipeline_for_merge_request').perform_async('Something', [1])
described_class.new.down
expect(sidekiq_queue_length('pipeline_default:update_head_pipeline_for_merge_request')).to eq 1
expect(sidekiq_queue_length('pipeline_processing:update_head_pipeline_for_merge_request')).to eq 0
end
end
end
context 'when there are no jobs in the queues' do
it 'does not raise error when migrating up' do
expect { described_class.new.up }.not_to raise_error
end
it 'does not raise error when migrating down' do
expect { described_class.new.down }.not_to raise_error
end
end
def stubbed_worker(queue:)
Class.new do
include Sidekiq::Worker
sidekiq_options queue: queue
end
end
end
...@@ -564,6 +564,20 @@ describe Project do ...@@ -564,6 +564,20 @@ describe Project do
it 'returns the project\'s last update date if it has no events' do it 'returns the project\'s last update date if it has no events' do
expect(project.last_activity_date).to eq(project.updated_at) expect(project.last_activity_date).to eq(project.updated_at)
end end
it 'returns the most recent timestamp' do
project.update_attributes(updated_at: nil,
last_activity_at: timestamp,
last_repository_updated_at: timestamp - 1.hour)
expect(project.last_activity_date).to eq(timestamp)
project.update_attributes(updated_at: timestamp,
last_activity_at: timestamp - 1.hour,
last_repository_updated_at: nil)
expect(project.last_activity_date).to eq(timestamp)
end
end end
end end
......
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