Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-16 21:08:46 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-16 21:08:46 +0300
commitaa0f0e992153e84e1cdec8a1c7310d5eb93a9f8f (patch)
tree4a662bc77fb43e1d1deec78cc7a95d911c0da1c5 /app
parentd47f9d2304dbc3a23bba7fe7a5cd07218eeb41cd (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue50
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue139
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue138
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js24
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue18
-rw-r--r--app/controllers/projects/performance_monitoring/dashboards_controller.rb77
-rw-r--r--app/graphql/resolvers/environments_resolver.rb23
-rw-r--r--app/graphql/types/environment_type.rb16
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/helpers/environments_helper.rb2
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/user.rb6
-rw-r--r--app/services/metrics/dashboard/clone_dashboard_service.rb113
-rw-r--r--app/views/admin/users/index.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml7
20 files changed, 559 insertions, 105 deletions
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 797fd0e7e19..b03ee12aef3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -17,10 +17,13 @@ import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
+
import DateTimePicker from './date_time_picker/date_time_picker.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import GroupEmptyState from './group_empty_state.vue';
+import DashboardsDropdown from './dashboards_dropdown.vue';
+
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getTimeDiff, getAddMetricTrackingOptions } from '../utils';
import { metricStates } from '../constants';
@@ -31,16 +34,18 @@ export default {
components: {
VueDraggable,
PanelType,
- GraphGroup,
- EmptyState,
- GroupEmptyState,
Icon,
GlButton,
GlDropdown,
GlDropdownItem,
GlFormGroup,
GlModal,
+
DateTimePicker,
+ GraphGroup,
+ EmptyState,
+ GroupEmptyState,
+ DashboardsDropdown,
},
directives: {
GlModal: GlModalDirective,
@@ -83,6 +88,10 @@ export default {
type: String,
required: true,
},
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
metricsEndpoint: {
type: String,
required: true,
@@ -140,6 +149,11 @@ export default {
required: false,
default: invalidUrl,
},
+ dashboardsEndpoint: {
+ type: String,
+ required: false,
+ default: invalidUrl,
+ },
currentDashboard: {
type: String,
required: false,
@@ -199,9 +213,6 @@ export default {
selectedDashboard() {
return this.allDashboards.find(d => d.path === this.currentDashboard) || this.firstDashboard;
},
- selectedDashboardText() {
- return this.selectedDashboard.display_name;
- },
showRearrangePanelsBtn() {
return !this.showEmptyState && this.rearrangePanelsAvailable;
},
@@ -223,6 +234,7 @@ export default {
environmentsEndpoint: this.environmentsEndpoint,
deploymentsEndpoint: this.deploymentsEndpoint,
dashboardEndpoint: this.dashboardEndpoint,
+ dashboardsEndpoint: this.dashboardsEndpoint,
currentDashboard: this.currentDashboard,
projectPath: this.projectPath,
});
@@ -314,6 +326,13 @@ export default {
return !this.getMetricStates(groupKey).includes(metricStates.OK);
},
getAddMetricTrackingOptions,
+
+ selectDashboard(dashboard) {
+ const params = {
+ dashboard: dashboard.path,
+ };
+ redirectTo(mergeUrlParams(params, window.location.href));
+ },
},
addMetric: {
title: s__('Metrics|Add metric'),
@@ -333,21 +352,14 @@ export default {
label-for="monitor-dashboards-dropdown"
class="col-sm-12 col-md-6 col-lg-2"
>
- <gl-dropdown
+ <dashboards-dropdown
id="monitor-dashboards-dropdown"
- class="mb-0 d-flex js-dashboards-dropdown"
+ class="mb-0 d-flex"
toggle-class="dropdown-menu-toggle"
- :text="selectedDashboardText"
- >
- <gl-dropdown-item
- v-for="dashboard in allDashboards"
- :key="dashboard.path"
- :active="dashboard.path === currentDashboard"
- active-class="is-active"
- :href="`?dashboard=${dashboard.path}`"
- >{{ dashboard.display_name || dashboard.path }}</gl-dropdown-item
- >
- </gl-dropdown>
+ :default-branch="defaultBranch"
+ :selected-dashboard="selectedDashboard"
+ @selectDashboard="selectDashboard($event)"
+ />
</gl-form-group>
<gl-form-group
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
new file mode 100644
index 00000000000..6d93eee0b4f
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -0,0 +1,139 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import {
+ GlAlert,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlModal,
+ GlLoadingIcon,
+ GlModalDirective,
+} from '@gitlab/ui';
+import DuplicateDashboardForm from './duplicate_dashboard_form.vue';
+
+const events = {
+ selectDashboard: 'selectDashboard',
+};
+
+export default {
+ components: {
+ GlAlert,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlModal,
+ GlLoadingIcon,
+ DuplicateDashboardForm,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ selectedDashboard: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ alert: null,
+ loading: false,
+ form: {},
+ };
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['allDashboards']),
+ isSystemDashboard() {
+ return this.selectedDashboard.system_dashboard;
+ },
+ selectedDashboardText() {
+ return this.selectedDashboard.display_name;
+ },
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
+ selectDashboard(dashboard) {
+ this.$emit(events.selectDashboard, dashboard);
+ },
+ ok(bvModalEvt) {
+ // Prevent modal from hiding in case submit fails
+ bvModalEvt.preventDefault();
+
+ this.loading = true;
+ this.alert = null;
+ this.duplicateSystemDashboard(this.form)
+ .then(createdDashboard => {
+ this.loading = false;
+ this.alert = null;
+
+ // Trigger hide modal as submit is successful
+ this.$refs.duplicateDashboardModal.hide();
+
+ // Dashboards in the default branch become available immediately.
+ // Not so in other branches, so we refresh the current dashboard
+ const dashboard =
+ this.form.branch === this.defaultBranch ? createdDashboard : this.selectedDashboard;
+ this.$emit(events.selectDashboard, dashboard);
+ })
+ .catch(error => {
+ this.loading = false;
+ this.alert = error;
+ });
+ },
+ hide() {
+ this.alert = null;
+ },
+ formChange(form) {
+ this.form = form;
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown toggle-class="dropdown-menu-toggle" :text="selectedDashboardText">
+ <gl-dropdown-item
+ v-for="dashboard in allDashboards"
+ :key="dashboard.path"
+ :active="dashboard.path === selectedDashboard.path"
+ active-class="is-active"
+ @click="selectDashboard(dashboard)"
+ >
+ {{ dashboard.display_name || dashboard.path }}
+ </gl-dropdown-item>
+
+ <template v-if="isSystemDashboard">
+ <gl-dropdown-divider />
+
+ <gl-modal
+ ref="duplicateDashboardModal"
+ modal-id="duplicateDashboardModal"
+ :title="s__('Metrics|Duplicate dashboard')"
+ ok-variant="success"
+ @ok="ok"
+ @hide="hide"
+ >
+ <gl-alert v-if="alert" class="mb-3" variant="danger" @dismiss="alert = null">
+ {{ alert }}
+ </gl-alert>
+ <duplicate-dashboard-form
+ :dashboard="selectedDashboard"
+ :default-branch="defaultBranch"
+ @change="formChange"
+ />
+ <template #modal-ok>
+ <gl-loading-icon v-if="loading" inline color="light" />
+ {{ loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate') }}
+ </template>
+ </gl-modal>
+
+ <gl-dropdown-item ref="duplicateDashboardItem" v-gl-modal="'duplicateDashboardModal'">
+ {{ s__('Metrics|Duplicate dashboard') }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
new file mode 100644
index 00000000000..e678957c1e5
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
@@ -0,0 +1,138 @@
+<script>
+import { __, s__, sprintf } from '~/locale';
+import { GlFormGroup, GlFormInput, GlFormRadioGroup, GlFormTextarea } from '@gitlab/ui';
+
+const defaultFileName = dashboard => dashboard.path.split('/').reverse()[0];
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ GlFormRadioGroup,
+ GlFormTextarea,
+ },
+ props: {
+ dashboard: {
+ type: Object,
+ required: true,
+ },
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ },
+ radioVals: {
+ /* Use the default branch (e.g. master) */
+ DEFAULT: 'DEFAULT',
+ /* Create a new branch */
+ NEW: 'NEW',
+ },
+ data() {
+ return {
+ form: {
+ dashboard: this.dashboard.path,
+ fileName: defaultFileName(this.dashboard),
+ commitMessage: '',
+ },
+ branchName: '',
+ branchOption: this.$options.radioVals.NEW,
+ branchOptions: [
+ {
+ value: this.$options.radioVals.DEFAULT,
+ html: sprintf(
+ __('Commit to %{branchName} branch'),
+ {
+ branchName: `<strong>${this.defaultBranch}</strong>`,
+ },
+ false,
+ ),
+ },
+ { value: this.$options.radioVals.NEW, text: __('Create new branch') },
+ ],
+ };
+ },
+ computed: {
+ defaultCommitMsg() {
+ return sprintf(s__('Metrics|Create custom dashboard %{fileName}'), {
+ fileName: this.form.fileName,
+ });
+ },
+ fileNameState() {
+ // valid if empty or *.yml
+ return !(this.form.fileName && !this.form.fileName.endsWith('.yml'));
+ },
+ fileNameFeedback() {
+ return !this.fileNameState ? s__('The file name should have a .yml extension') : '';
+ },
+ },
+ mounted() {
+ this.change();
+ },
+ methods: {
+ change() {
+ this.$emit('change', {
+ ...this.form,
+ commitMessage: this.form.commitMessage || this.defaultCommitMsg,
+ branch:
+ this.branchOption === this.$options.radioVals.NEW ? this.branchName : this.defaultBranch,
+ });
+ },
+ focus(option) {
+ if (option === this.$options.radioVals.NEW) {
+ this.$nextTick(() => {
+ this.$refs.branchName.$el.focus();
+ });
+ }
+ },
+ },
+};
+</script>
+<template>
+ <form @change="change">
+ <p class="text-muted">
+ {{
+ s__(`Metrics|You can save a copy of this dashboard to your repository
+ so it can be customized. Select a file name and branch to
+ save it.`)
+ }}
+ </p>
+ <gl-form-group
+ ref="fileNameFormGroup"
+ :label="__('File name')"
+ :state="fileNameState"
+ :invalid-feedback="fileNameFeedback"
+ label-size="sm"
+ label-for="fileName"
+ >
+ <gl-form-input id="fileName" ref="fileName" v-model="form.fileName" :required="true" />
+ </gl-form-group>
+ <gl-form-group :label="__('Branch')" label-size="sm" label-for="branch">
+ <gl-form-radio-group
+ ref="branchOption"
+ v-model="branchOption"
+ :checked="$options.radioVals.NEW"
+ :stacked="true"
+ :options="branchOptions"
+ @change="focus"
+ />
+ <gl-form-input
+ v-show="branchOption === $options.radioVals.NEW"
+ id="branchName"
+ ref="branchName"
+ v-model="branchName"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="__('Commit message (optional)')"
+ label-size="sm"
+ label-for="commitMessage"
+ >
+ <gl-form-textarea
+ id="commitMessage"
+ ref="commitMessage"
+ v-model="form.commitMessage"
+ :placeholder="defaultCommitMsg"
+ />
+ </gl-form-group>
+ </form>
+</template>
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index fce89b450e4..61cd8621902 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -214,5 +214,29 @@ export const setPanelGroupMetrics = ({ commit }, data) => {
commit(types.SET_PANEL_GROUP_METRICS, data);
};
+export const duplicateSystemDashboard = ({ state }, payload) => {
+ const params = {
+ dashboard: payload.dashboard,
+ file_name: payload.fileName,
+ branch: payload.branch,
+ commit_message: payload.commitMessage,
+ };
+
+ return axios
+ .post(state.dashboardsEndpoint, params)
+ .then(response => response.data)
+ .then(data => data.dashboard)
+ .catch(error => {
+ const { response } = error;
+ if (response && response.data && response.data.error) {
+ throw sprintf(s__('Metrics|There was an error creating the dashboard. %{error}'), {
+ error: response.data.error,
+ });
+ } else {
+ throw s__('Metrics|There was an error creating the dashboard.');
+ }
+ });
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 0b848de9562..506a30ae619 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -175,6 +175,7 @@ export default {
state.environmentsEndpoint = endpoints.environmentsEndpoint;
state.deploymentsEndpoint = endpoints.deploymentsEndpoint;
state.dashboardEndpoint = endpoints.dashboardEndpoint;
+ state.dashboardsEndpoint = endpoints.dashboardsEndpoint;
state.currentDashboard = endpoints.currentDashboard;
state.projectPath = endpoints.projectPath;
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
index e03b1e6d6a6..34866cdfa6f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import DeploymentInfo from './deployment_info.vue';
import DeploymentViewButton from './deployment_view_button.vue';
import DeploymentStopButton from './deployment_stop_button.vue';
@@ -14,9 +14,6 @@ export default {
DeploymentStopButton,
DeploymentViewButton,
},
- directives: {
- GlTooltip: GlTooltipDirective,
- },
props: {
deployment: {
type: Object,
@@ -43,6 +40,14 @@ export default {
},
},
computed: {
+ appButtonText() {
+ return {
+ text: this.isCurrent ? s__('Review App|View app') : s__('Review App|View latest app'),
+ tooltip: this.isCurrent
+ ? ''
+ : __('View the latest successful deployment to this environment'),
+ };
+ },
canBeManuallyDeployed() {
return this.computedDeploymentStatus === MANUAL_DEPLOY;
},
@@ -55,9 +60,6 @@ export default {
hasExternalUrls() {
return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
- hasPreviousDeployment() {
- return Boolean(!this.isCurrent && this.deployment.deployed_at);
- },
isCurrent() {
return this.computedDeploymentStatus === SUCCESS;
},
@@ -89,7 +91,7 @@ export default {
<!-- show appropriate version of review app button -->
<deployment-view-button
v-if="hasExternalUrls"
- :is-current="isCurrent"
+ :app-button-text="appButtonText"
:deployment="deployment"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index 9965e3d5203..18d4073ecd4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -11,12 +11,12 @@ export default {
import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
},
props: {
- deployment: {
+ appButtonText: {
type: Object,
required: true,
},
- isCurrent: {
- type: Boolean,
+ deployment: {
+ type: Object,
required: true,
},
showVisualReviewApp: {
@@ -60,7 +60,7 @@ export default {
>
<template slot="mainAction" slot-scope="slotProps">
<review-app-link
- :is-current="isCurrent"
+ :display="appButtonText"
:link="deploymentExternalUrl"
:css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
/>
@@ -85,7 +85,7 @@ export default {
</filtered-search-dropdown>
<template v-else>
<review-app-link
- :is-current="isCurrent"
+ :display="appButtonText"
:link="deploymentExternalUrl"
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
index 1550ec0f21e..c38c41f13b6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
@@ -1,18 +1,21 @@
<script>
-import { __ } from '~/locale';
+import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
cssClass: {
type: String,
required: true,
},
- isCurrent: {
- type: Boolean,
+ display: {
+ type: Object,
required: true,
},
link: {
@@ -20,15 +23,12 @@ export default {
required: true,
},
},
- computed: {
- linkText() {
- return this.isCurrent ? __('View app') : __('View previous app');
- },
- },
};
</script>
<template>
<a
+ v-gl-tooltip
+ :title="display.tooltip"
:href="link"
target="_blank"
rel="noopener noreferrer nofollow"
@@ -36,6 +36,6 @@ export default {
data-track-event="open_review_app"
data-track-label="review_app"
>
- {{ linkText }} <icon class="fgray" name="external-link" />
+ {{ display.text }} <icon class="fgray" name="external-link" />
</a>
</template>
diff --git a/app/controllers/projects/performance_monitoring/dashboards_controller.rb b/app/controllers/projects/performance_monitoring/dashboards_controller.rb
index c873fcd6c8a..2d872b78096 100644
--- a/app/controllers/projects/performance_monitoring/dashboards_controller.rb
+++ b/app/controllers/projects/performance_monitoring/dashboards_controller.rb
@@ -7,90 +7,53 @@ module Projects
before_action :check_repository_available!
before_action :validate_required_params!
- before_action :validate_dashboard_template!
- before_action :authorize_push!
- USER_DASHBOARDS_DIR = ::Metrics::Dashboard::ProjectDashboardService::DASHBOARD_ROOT
- DASHBOARD_TEMPLATES = {
- ::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH => ::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH
- }.freeze
+ rescue_from ActionController::ParameterMissing do |exception|
+ respond_error(http_status: :bad_request, message: _('Request parameter %{param} is missing.') % { param: exception.param })
+ end
def create
- result = ::Files::CreateService.new(project, current_user, dashboard_attrs).execute
+ result = ::Metrics::Dashboard::CloneDashboardService.new(project, current_user, dashboard_params).execute
if result[:status] == :success
- respond_success
+ respond_success(result)
else
- respond_error(result[:message])
+ respond_error(result)
end
end
private
- def respond_success
+ def respond_success(result)
+ set_web_ide_link_notice(result.dig(:dashboard, :path))
respond_to do |format|
- format.html { redirect_to ide_edit_path(project, redirect_safe_branch_name, new_dashboard_path) }
- format.json { render json: { redirect_to: ide_edit_path(project, redirect_safe_branch_name, new_dashboard_path) }, status: :created }
+ format.json { render status: result.delete(:http_status), json: result }
end
end
- def respond_error(message)
- flash[:alert] = message
-
+ def respond_error(result)
respond_to do |format|
- format.html { redirect_back_or_default(default: namespace_project_environments_path) }
- format.json { render json: { error: message }, status: :bad_request }
+ format.json { render json: { error: result[:message] }, status: result[:http_status] }
end
end
- def authorize_push!
- access_denied!(%q(You can't commit to this project)) unless user_access(project).can_push_to_branch?(params[:branch])
+ def set_web_ide_link_notice(new_dashboard_path)
+ web_ide_link_start = "<a href=\"#{ide_edit_path(project, redirect_safe_branch_name, new_dashboard_path)}\">"
+ message = _("Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}.") % { web_ide_link_start: web_ide_link_start, web_ide_link_end: "</a>" }
+ flash[:notice] = message.html_safe
end
def validate_required_params!
- params.require(%i(branch file_name dashboard))
- end
-
- def validate_dashboard_template!
- access_denied! unless dashboard_template
- end
-
- def dashboard_attrs
- {
- commit_message: commit_message,
- file_path: new_dashboard_path,
- file_content: new_dashboard_content,
- encoding: 'text',
- branch_name: params[:branch],
- start_branch: repository.branch_exists?(params[:branch]) ? params[:branch] : project.default_branch
- }
- end
-
- def commit_message
- params[:commit_message] || "Create custom dashboard #{params[:file_name]}"
- end
-
- def new_dashboard_path
- File.join(USER_DASHBOARDS_DIR, params[:file_name])
- end
-
- def new_dashboard_content
- File.read(Rails.root.join(dashboard_template))
- end
-
- def dashboard_template
- dashboard_templates[params[:dashboard]]
- end
-
- def dashboard_templates
- DASHBOARD_TEMPLATES
+ params.require(%i(branch file_name dashboard commit_message))
end
def redirect_safe_branch_name
repository.find_branch(params[:branch]).name
end
+
+ def dashboard_params
+ params.permit(%i(branch file_name dashboard commit_message)).to_h
+ end
end
end
end
-
-Projects::PerformanceMonitoring::DashboardsController.prepend_if_ee('EE::Projects::PerformanceMonitoring::DashboardsController')
diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb
new file mode 100644
index 00000000000..868abef98eb
--- /dev/null
+++ b/app/graphql/resolvers/environments_resolver.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class EnvironmentsResolver < BaseResolver
+ argument :name, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Name of the environment'
+
+ argument :search, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Search query'
+
+ type Types::EnvironmentType, null: true
+
+ alias_method :project, :object
+
+ def resolve(**args)
+ return unless project.present?
+
+ EnvironmentsFinder.new(project, context[:current_user], args).find
+ end
+ end
+end
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
new file mode 100644
index 00000000000..ad65caa24a6
--- /dev/null
+++ b/app/graphql/types/environment_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ class EnvironmentType < BaseObject
+ graphql_name 'Environment'
+ description 'Describes where code is deployed for a project'
+
+ authorize :read_environment
+
+ field :name, GraphQL::STRING_TYPE, null: false,
+ description: 'Human-readable name of the environment'
+
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the environment'
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 31cde7b6d48..5ece4926951 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -138,6 +138,12 @@ module Types
description: 'Issues of the project',
resolver: Resolvers::IssuesResolver
+ field :environments,
+ Types::EnvironmentType.connection_type,
+ null: true,
+ description: 'Environments of the project',
+ resolver: Resolvers::EnvironmentsResolver
+
field :issue,
Types::IssueType,
null: true,
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 59972118ae3..993c18f9229 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -29,8 +29,10 @@ module EnvironmentsHelper
"empty-no-data-small-svg-path" => image_path('illustrations/chart-empty-state-small.svg'),
"empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
+ "dashboards-endpoint" => project_performance_monitoring_dashboards_path(project, format: :json),
"dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json),
"deployments-endpoint" => project_environment_deployments_path(project, environment, format: :json),
+ "default-branch" => project.default_branch,
"environments-endpoint": project_environments_path(project, format: :json),
"project-path" => project_path(project),
"tags-path" => project_tags_path(project),
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 663389050d1..3943d991c87 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -197,6 +197,10 @@ module Ci
AutoMergeProcessWorker.perform_async(merge_request.id)
end
+
+ if pipeline.auto_devops_source?
+ self.class.auto_devops_pipelines_completed_total.increment(status: pipeline.status)
+ end
end
end
@@ -330,6 +334,10 @@ module Ci
::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource preparing pending]
end
+ def self.auto_devops_pipelines_completed_total
+ @auto_devops_pipelines_completed_total ||= Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines')
+ end
+
def stages_count
statuses.select(:stage).distinct.count
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 6442e74bbe3..4bba4d47b8f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -307,6 +307,8 @@ class User < ApplicationRecord
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal }
+ scope :active_without_ghosts, -> { with_state(:active).without_ghosts }
+ scope :without_ghosts, -> { where('ghost IS NOT TRUE') }
scope :deactivated, -> { with_state(:deactivated).non_internal }
scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
@@ -470,7 +472,7 @@ class User < ApplicationRecord
when 'deactivated'
deactivated
else
- active
+ active_without_ghosts
end
end
@@ -614,7 +616,7 @@ class User < ApplicationRecord
end
def self.non_internal
- where('ghost IS NOT TRUE')
+ without_ghosts
end
#
diff --git a/app/services/metrics/dashboard/clone_dashboard_service.rb b/app/services/metrics/dashboard/clone_dashboard_service.rb
new file mode 100644
index 00000000000..b2ec44cb814
--- /dev/null
+++ b/app/services/metrics/dashboard/clone_dashboard_service.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+# Copies system dashboard definition in .yml file into designated
+# .yml file inside `.gitlab/dashboards`
+module Metrics
+ module Dashboard
+ class CloneDashboardService < ::BaseService
+ ALLOWED_FILE_TYPE = '.yml'
+ USER_DASHBOARDS_DIR = ::Metrics::Dashboard::ProjectDashboardService::DASHBOARD_ROOT
+
+ def self.allowed_dashboard_templates
+ @allowed_dashboard_templates ||= Set[::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH].freeze
+ end
+
+ def execute
+ catch(:error) do
+ throw(:error, error(_(%q(You can't commit to this project)), :forbidden)) unless push_authorized?
+
+ result = ::Files::CreateService.new(project, current_user, dashboard_attrs).execute
+ throw(:error, wrap_error(result)) unless result[:status] == :success
+
+ repository.refresh_method_caches([:metrics_dashboard])
+ success(result.merge(http_status: :created, dashboard: dashboard_details))
+ end
+ end
+
+ private
+
+ def dashboard_attrs
+ {
+ commit_message: params[:commit_message],
+ file_path: new_dashboard_path,
+ file_content: new_dashboard_content,
+ encoding: 'text',
+ branch_name: branch,
+ start_branch: repository.branch_exists?(branch) ? branch : project.default_branch
+ }
+ end
+
+ def dashboard_details
+ {
+ path: new_dashboard_path,
+ display_name: ::Metrics::Dashboard::ProjectDashboardService.name_for_path(new_dashboard_path),
+ default: false,
+ system_dashboard: false
+ }
+ end
+
+ def push_authorized?
+ Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch)
+ end
+
+ def dashboard_template
+ @dashboard_template ||= begin
+ throw(:error, error(_('Not found.'), :not_found)) unless self.class.allowed_dashboard_templates.include?(params[:dashboard])
+
+ params[:dashboard]
+ end
+ end
+
+ def branch
+ @branch ||= begin
+ throw(:error, error(_('There was an error creating the dashboard, branch name is invalid.'), :bad_request)) unless valid_branch_name?
+ throw(:error, error(_('There was an error creating the dashboard, branch named: %{branch} already exists.') % { branch: params[:branch] }, :bad_request)) unless new_or_default_branch? # temporary validation for first UI iteration
+
+ params[:branch]
+ end
+ end
+
+ def new_or_default_branch?
+ !repository.branch_exists?(params[:branch]) || project.default_branch == params[:branch]
+ end
+
+ def valid_branch_name?
+ Gitlab::GitRefValidator.validate(params[:branch])
+ end
+
+ def new_dashboard_path
+ @new_dashboard_path ||= File.join(USER_DASHBOARDS_DIR, file_name)
+ end
+
+ def file_name
+ @file_name ||= begin
+ throw(:error, error(_('The file name should have a .yml extension'), :bad_request)) unless target_file_type_valid?
+
+ File.basename(params[:file_name])
+ end
+ end
+
+ def target_file_type_valid?
+ File.extname(params[:file_name]) == ALLOWED_FILE_TYPE
+ end
+
+ def new_dashboard_content
+ File.read(Rails.root.join(dashboard_template))
+ end
+
+ def repository
+ @repository ||= project.repository
+ end
+
+ def wrap_error(result)
+ if result[:message] == 'A file with this name already exists'
+ error(_("A file with '%{file_name}' already exists in %{branch} branch") % { file_name: file_name, branch: branch }, :bad_request)
+ else
+ result
+ end
+ end
+ end
+ end
+end
+
+Metrics::Dashboard::CloneDashboardService.prepend_if_ee('EE::Metrics::Dashboard::CloneDashboardService')
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 3c6ad899d1e..ecbabab3e7f 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -9,7 +9,7 @@
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do
= s_('AdminUsers|Active')
- %small.badge.badge-pill= limited_counter_with_delimiter(User.active)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.active_without_ghosts)
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do
= s_('AdminUsers|Admins')
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 81bd15ed287..8c9b859e127 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -44,8 +44,10 @@
= expanded ? _('Collapse') : _('Expand')
%p
- auto_devops_url = help_page_path('topics/autodevops/index')
+ - quickstart_url = help_page_path('topics/autodevops/quick_start_guide')
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
- = s_('GroupSettings|Auto DevOps will automatically build, test and deploy your application based on a predefined Continuous Integration and Delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe }
+ - quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url }
+ = s_('AutoDevOps|Auto DevOps can automatically build, test, and deploy applications based on predefined continuous integration and delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end} or use our %{quickstart_start}quick start guide%{quickstart_end} to get started right away.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
.settings-content
= render 'groups/settings/ci_cd/auto_devops_form', group: @group
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index a027dca1b56..88bb0a97487 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -44,7 +44,7 @@
- if group_sidebar_link?(:contribution_analytics)
= nav_link(path: 'analytics#show') do
- = link_to group_analytics_path(@group), title: _('Contribution Analytics'), data: { placement: 'right', qa_selector: 'contribution_analytics_link' } do
+ = link_to group_contribution_analytics_path(@group), title: _('Contribution Analytics'), data: { placement: 'right', qa_selector: 'contribution_analytics_link' } do
%span
= _('Contribution Analytics')
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 5a6c8079543..a65afeecc17 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -23,8 +23,11 @@
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
- = link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
+ - auto_devops_url = help_page_path('topics/autodevops/index')
+ - quickstart_url = help_page_path('topics/autodevops/quick_start_guide')
+ - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
+ - quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url }
+ = s_('AutoDevOps|Auto DevOps can automatically build, test, and deploy applications based on predefined continuous integration and delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end} or use our %{quickstart_start}quick start guide%{quickstart_end} to get started right away.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
.settings-content
= render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled?