Commit 438a7f52 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent ac0a6366d831
......@@ -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
......
<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>
<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>
......@@ -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 () => {};
......@@ -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;
},
......
<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"
......
......@@ -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"
/>
......
<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>
......@@ -7,90 +7,53 @@ class DashboardsController < ::Projects::ApplicationController
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