Commit 594cf561 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'master' into 'jivanvl-add-text-variable-input-from-variables'

# Conflicts:
#   app/assets/javascripts/monitoring/stores/actions.js
#   app/assets/javascripts/monitoring/stores/mutation_types.js
#   app/assets/javascripts/monitoring/stores/mutations.js
#   app/assets/javascripts/monitoring/stores/state.js
#   app/assets/javascripts/monitoring/utils.js
#   changelogs/unreleased/jivanvl-use-metrics-url-query-param.yml
#   spec/frontend/monitoring/store/mutations_spec.js
parents 42b11db5 a096aef7
......@@ -857,7 +857,7 @@ GEM
re2 (1.2.0)
recaptcha (4.13.1)
json
recursive-open-struct (1.1.0)
recursive-open-struct (1.1.1)
redis (4.1.3)
redis-actionpack (5.2.0)
actionpack (>= 5, < 7)
......
<script>
import { GlNewDropdown, GlNewDropdownItem, GlTabs, GlTab } from '@gitlab/ui';
import { GlNewDropdown, GlNewDropdownItem, GlTabs, GlTab, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import query from '../graphql/queries/details.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
statuses: {
......@@ -19,7 +20,9 @@ export default {
GlNewDropdownItem,
GlTab,
GlTabs,
GlButton,
},
mixins: [glFeatureFlagsMixin()],
props: {
alertId: {
type: String,
......@@ -29,6 +32,10 @@ export default {
type: String,
required: true,
},
newIssuePath: {
type: String,
required: true,
},
},
apollo: {
alert: {
......@@ -52,9 +59,22 @@ export default {
</script>
<template>
<div>
<div v-if="alert" class="d-flex justify-content-between border-bottom pb-2 pt-1">
<div></div>
<gl-new-dropdown class="align-self-center" right>
<div
v-if="alert"
class="gl-display-flex justify-content-end gl-border-b-1 gl-border-b-gray-200 gl-border-b-solid gl-p-4"
>
<gl-button
v-if="glFeatures.createIssueFromAlertEnabled"
data-testid="createIssueBtn"
:href="newIssuePath"
category="primary"
variant="success"
>
{{ s__('AlertManagement|Create issue') }}
</gl-button>
</div>
<div class="gl-display-flex justify-content-end">
<gl-new-dropdown right>
<gl-new-dropdown-item
v-for="(label, field) in $options.statuses"
:key="field"
......
......@@ -7,7 +7,7 @@ Vue.use(VueApollo);
export default selector => {
const domEl = document.querySelector(selector);
const { alertId, projectPath } = domEl.dataset;
const { alertId, projectPath, newIssuePath } = domEl.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
......@@ -25,6 +25,7 @@ export default selector => {
props: {
alertId,
projectPath,
newIssuePath,
},
});
},
......
......@@ -23,6 +23,8 @@ const Api = {
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
projectRunnersPath: '/api/:version/projects/:id/runners',
projectProtectedBranchesPath: '/api/:version/projects/:id/protected_branches',
projectSearchPath: '/api/:version/projects/:id/search',
projectMilestonesPath: '/api/:version/projects/:id/milestones',
mergeRequestsPath: '/api/:version/merge_requests',
groupLabelsPath: '/groups/:namespace_path/-/labels',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
......@@ -75,13 +77,11 @@ const Api = {
const url = Api.buildUrl(Api.groupsPath);
return axios
.get(url, {
params: Object.assign(
{
search: query,
per_page: DEFAULT_PER_PAGE,
},
options,
),
params: {
search: query,
per_page: DEFAULT_PER_PAGE,
...options,
},
})
.then(({ data }) => {
callback(data);
......@@ -248,6 +248,23 @@ const Api = {
.then(({ data }) => data);
},
projectSearch(id, options = {}) {
const url = Api.buildUrl(Api.projectSearchPath).replace(':id', encodeURIComponent(id));
return axios.get(url, {
params: {
search: options.search,
scope: options.scope,
},
});
},
projectMilestones(id) {
const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id));
return axios.get(url);
},
mergeRequests(params = {}) {
const url = Api.buildUrl(Api.mergeRequestsPath);
......@@ -282,7 +299,7 @@ const Api = {
};
return axios
.get(url, {
params: Object.assign({}, defaults, options),
params: { ...defaults, ...options },
})
.then(({ data }) => callback(data))
.catch(() => flash(__('Something went wrong while fetching projects')));
......@@ -365,13 +382,11 @@ const Api = {
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
params: Object.assign(
{
search: query,
per_page: DEFAULT_PER_PAGE,
},
options,
),
params: {
search: query,
per_page: DEFAULT_PER_PAGE,
...options,
},
});
},
......@@ -402,7 +417,7 @@ const Api = {
};
return axios
.get(url, {
params: Object.assign({}, defaults, options),
params: { ...defaults, ...options },
})
.then(({ data }) => callback(data))
.catch(() => flash(__('Something went wrong while fetching projects')));
......
......@@ -22,7 +22,7 @@ function eventHasModifierKeys(event) {
export default class ShortcutsBlob extends Shortcuts {
constructor(opts) {
const options = Object.assign({}, defaults, opts);
const options = { ...defaults, ...opts };
super(options.skipResetBindings);
this.options = options;
......
......@@ -17,7 +17,7 @@ const defaults = {
class BlobForkSuggestion {
constructor(options) {
this.elementMap = Object.assign({}, defaults, options);
this.elementMap = { ...defaults, ...options };
this.onOpenButtonClick = this.onOpenButtonClick.bind(this);
this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
}
......
......@@ -19,14 +19,15 @@ export function getBoardSortableDefaultOptions(obj) {
const touchEnabled =
'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
const defaultSortOptions = Object.assign({}, sortableConfig, {
const defaultSortOptions = {
...sortableConfig,
filter: '.no-drag',
delay: touchEnabled ? 100 : 0,
scrollSensitivity: touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: sortableStart,
onEnd: sortableEnd,
});
};
Object.keys(obj).forEach(key => {
defaultSortOptions[key] = obj[key];
......
......@@ -2,7 +2,7 @@ import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
const InputSetter = { ...ISetter };
class CloseReopenReportToggle {
constructor(opts = {}) {
......
......@@ -325,7 +325,7 @@ export default class Clusters {
handleClusterStatusSuccess(data) {
const prevStatus = this.store.state.status;
const prevApplicationMap = Object.assign({}, this.store.state.applications);
const prevApplicationMap = { ...this.store.state.applications };
this.store.updateStateFromServer(data.data);
......
......@@ -2,7 +2,7 @@ import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
const InputSetter = { ...ISetter };
class CommentTypeToggle {
constructor(opts = {}) {
......
......@@ -13,7 +13,7 @@ import {
import confidentialMergeRequestState from './confidential_merge_request/state';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
const InputSetter = { ...ISetter };
const CREATE_MERGE_REQUEST = 'create-mr';
const CREATE_BRANCH = 'create-branch';
......
......@@ -84,7 +84,7 @@ export default {
events.forEach(item => {
if (!item) return;
const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
const eventItem = { ...DEFAULT_EVENT_OBJECTS[stage.slug], ...item };
eventItem.totalTime = eventItem.total_time;
......
......@@ -28,9 +28,7 @@ export default {
return this.label === null;
},
pinStyle() {
return this.repositioning
? Object.assign({}, this.position, { cursor: 'move' })
: this.position;
return this.repositioning ? { ...this.position, cursor: 'move' } : this.position;
},
pinLabel() {
return this.isNewNote
......
......@@ -35,6 +35,7 @@ import {
UPDATE_NOTE_ERROR,
designDeletionError,
} from '../../utils/error_messages';
import { trackDesignDetailView } from '../../utils/tracking';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
export default {
......@@ -257,8 +258,21 @@ export default {
query: this.$route.query,
});
},
trackEvent() {
trackDesignDetailView(
'issue-design-collection',
this.$route.query.version || this.latestVersionId,
this.isLatestVersion,
);
},
},
beforeRouteEnter(to, from, next) {
next(vm => {
vm.trackEvent();
});
},
beforeRouteUpdate(to, from, next) {
this.trackEvent();
this.closeCommentForm();
next();
},
......
import Tracking from '~/tracking';
function assembleDesignPayload(payloadArr) {
return {
value: {
'internal-object-refrerer': payloadArr[0],
'version-number': payloadArr[1],
'current-version': payloadArr[2],
},
};
}
// Tracking Constants
const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design';
// eslint-disable-next-line import/prefer-default-export
export function trackDesignDetailView(refrerer = '', designVersion = 1, latestVersion = false) {
Tracking.event(DESIGN_TRACKING_PAGE_NAME, 'design_viewed', {
label: 'design_viewed',
...assembleDesignPayload([refrerer, designVersion, latestVersion]),
});
}
......@@ -233,7 +233,7 @@ export function trimFirstCharOfLineContent(line = {}) {
// eslint-disable-next-line no-param-reassign
delete line.text;
const parsedLine = Object.assign({}, line);
const parsedLine = { ...line };
if (line.rich_text) {
const firstChar = parsedLine.rich_text.charAt(0);
......
......@@ -58,13 +58,14 @@ export default class EnvironmentsStore {
let filtered = {};
if (env.size > 1) {
filtered = Object.assign({}, env, {
filtered = {
...env,
isFolder: true,
isLoadingFolderContent: oldEnvironmentState.isLoading || false,
folderName: env.name,
isOpen: oldEnvironmentState.isOpen || false,
children: oldEnvironmentState.children || [],
});
};
}
if (env.latest) {
......@@ -166,7 +167,7 @@ export default class EnvironmentsStore {
let updated = env;
if (env.latest) {
updated = Object.assign({}, env, env.latest);
updated = { ...env, ...env.latest };
delete updated.latest;
} else {
updated = env;
......@@ -192,7 +193,7 @@ export default class EnvironmentsStore {
const { environments } = this.state;
const updatedEnvironments = environments.map(env => {
const updateEnv = Object.assign({}, env);
const updateEnv = { ...env };
if (env.id === environment.id) {
updateEnv[prop] = newValue;
}
......
......@@ -120,7 +120,7 @@ export default class FilteredSearchDropdownManager {
filter: key,
};
const extraArguments = mappingKey.extraArguments || {};
const glArguments = Object.assign({}, defaultArguments, extraArguments);
const glArguments = { ...defaultArguments, ...extraArguments };
// Passing glArguments to `new glClass(<arguments>)`
mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
......
......@@ -2,14 +2,12 @@ import { uniq } from 'lodash';
class RecentSearchesStore {
constructor(initialState = {}, allowedKeys) {
this.state = Object.assign(
{
isLocalStorageAvailable: true,
recentSearches: [],
allowedKeys,
},
initialState,
);
this.state = {
isLocalStorageAvailable: true,
recentSearches: [],
allowedKeys,
...initialState,
};
}
addRecentSearch(newSearch) {
......
......@@ -595,13 +595,14 @@ class GitLabDropdown {
return renderItem({
instance: this,
options: Object.assign({}, this.options, {
options: {
...this.options,
icon: this.icon,
highlight: this.highlight,
highlightText: text => this.highlightTextMatches(text, this.filterInput.val()),
highlightTemplate: this.highlightTemplate.bind(this),
parent,
}),
},
data,
group,
index,
......
......@@ -8,7 +8,7 @@ export default class GLForm {
constructor(form, enableGFM = {}) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = Object.assign({}, defaultAutocompleteConfig, enableGFM);
this.enableGFM = { ...defaultAutocompleteConfig, ...enableGFM };
// Disable autocomplete for keywords which do not have dataSources available
const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
Object.keys(this.enableGFM).forEach(item => {
......
......@@ -2,7 +2,7 @@ import { visitUrl } from '../lib/utils/url_utility';
import DropLab from '../droplab/drop_lab';
import ISetter from '../droplab/plugins/input_setter';
const InputSetter = Object.assign({}, ISetter);
const InputSetter = { ...ISetter };
const NEW_PROJECT = 'new-project';
const NEW_SUBGROUP = 'new-subgroup';
......
<script>
import $ from 'jquery';
import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import { GlModal } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
......@@ -11,7 +10,7 @@ export default {
components: {
Icon,
ListItem,
GlModal: DeprecatedModal2,
GlModal,
},
directives: {
tooltip,
......@@ -58,7 +57,7 @@ export default {
methods: {
...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']),
openDiscardModal() {
$('#discard-all-changes').modal('show');
this.$refs.discardAllModal.show();
},
unstageAndDiscardAllChanges() {
this.unstageAllChanges();
......@@ -114,11 +113,12 @@ export default {
</p>
<gl-modal
v-if="!stagedList"
id="discard-all-changes"
:footer-primary-button-text="__('Discard all changes')"
:header-title-text="__('Discard all changes?')"
footer-primary-button-variant="danger"
@submit="unstageAndDiscardAllChanges"
ref="discardAllModal"
ok-variant="danger"
modal-id="discard-all-changes"
:ok-title="__('Discard all changes')"
:title="__('Discard all changes?')"
@ok="unstageAndDiscardAllChanges"
>
{{ $options.discardModalText }}
</gl-modal>
......
......@@ -3,6 +3,7 @@ import Vue from 'vue';
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { modalTypes } from '../constants';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue';
......@@ -67,7 +68,7 @@ export default {
document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`);
},
methods: {
...mapActions(['toggleFileFinder', 'openNewEntryModal']),
...mapActions(['toggleFileFinder']),
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
......@@ -81,6 +82,9 @@ export default {
openFile(file) {
this.$router.push(`/project${file.url}`);
},
createNewFile() {
this.$refs.newModal.open(modalTypes.blob);
},
},
};
</script>
......@@ -137,7 +141,7 @@ export default {
variant="success"
:title="__('New file')"
:aria-label="__('New file')"
@click="openNewEntryModal({ type: 'blob' })"
@click="createNewFile()"
>
{{ __('New file') }}
</gl-deprecated-button>
......@@ -159,6 +163,6 @@ export default {
<component :is="rightPaneComponent" v-if="currentProjectId" />
</div>
<ide-status-bar />
<new-modal />
<new-modal ref="newModal" />
</article>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { modalTypes } from '../constants';
import IdeTreeList from './ide_tree_list.vue';
import Upload from './new_dropdown/upload.vue';
import NewEntryButton from './new_dropdown/button.vue';
import NewModal from './new_dropdown/modal.vue';
export default {
components: {
Upload,
IdeTreeList,
NewEntryButton,
NewModal,
},
computed: {
...mapState(['currentBranchId']),
......@@ -26,7 +29,13 @@ export default {
}
},
methods: {
...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry', 'resetOpenFiles']),
...mapActions(['updateViewer', 'createTempEntry', 'resetOpenFiles']),
createNewFile() {
this.$refs.newModal.open(modalTypes.blob);
},
createNewFolder() {
this.$refs.newModal.open(modalTypes.tree);
},
},
};
</script>
......@@ -41,7 +50,7 @@ export default {
:show-label="false"
class="d-flex border-0 p-0 mr-3 qa-new-file"
icon="doc-new"
@click="openNewEntryModal({ type: 'blob' })"
@click="createNewFile()"
/>
<upload
:show-label="false"
......@@ -54,9 +63,10 @@ export default {
:show-label="false"
class="d-flex border-0 p-0"
icon="folder-new"
@click="openNewEntryModal({ type: 'tree' })"
@click="createNewFolder()"
/>
</div>
<new-modal ref="newModal" />
</template>
</ide-tree-list>
</template>
......@@ -25,13 +25,13 @@ export default {
<div class="ide-nav-form p-0">
<tabs v-if="showMergeRequests" stop-propagation>
<tab active>
<template slot="title">
<template #title>
{{ __('Branches') }}
</template>
<branches-search-list />
</tab>
<tab>
<template slot="title">
<template #title>
{{ __('Merge Requests') }}
</template>
<merge-request-search-list />
......
......@@ -4,12 +4,14 @@ import icon from '~/vue_shared/components/icon.vue';
import upload from './upload.vue';
import ItemButton from './button.vue';
import { modalTypes } from '../../constants';
import NewModal from '../new_dropdown/modal.vue';
export default {
components: {
icon,
upload,
ItemButton,
NewModal,
},
props: {
type: {
......@@ -37,9 +39,9 @@ export default {
},
},
methods: {
...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']),
...mapActions(['createTempEntry', 'deleteEntry']),
createNewItem(type) {
this.openNewEntryModal({ type, path: this.path });
this.$refs.newModal.open(type, this.path);
this.$emit('toggle', false);
},
openDropdown() {
......@@ -109,5 +111,6 @@ export default {
</li>
</ul>
</div>
<new-modal ref="newModal" />
</div>
</template>
<script>
import $ from 'jquery';
import { mapActions, mapState, mapGetters } from 'vuex';
import flash from '~/flash';
import { __, sprintf, s__ } from '~/locale';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { GlModal } from '@gitlab/ui';
import { modalTypes } from '../../constants';
export default {
components: {
GlModal: DeprecatedModal2,
GlModal,
},
data() {
return {
name: '',
type: modalTypes.blob,
path: '',
};
},
computed: {
...mapState(['entries', 'entryModal']),
...mapState(['entries']),
...mapGetters('fileTemplates', ['templateTypes']),
entryName: {
get() {
const entryPath = this.entryModal.entry.path;
if (this.entryModal.type === modalTypes.rename) {
return this.name || entryPath;
if (this.type === modalTypes.rename) {
return this.name || this.path;
}
return this.name || (entryPath ? `${entryPath}/` : '');
return this.name || (this.path ? `${this.path}/` : '');
},
set(val) {
this.name = val.trim();
},
},
modalTitle() {
if (this.entryModal.type === modalTypes.tree) {
const entry = this.entries[this.path];
if (this.type === modalTypes.tree) {
return __('Create new directory');
} else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree
? __('Rename folder')
: __('Rename file');
} else if (this.type === modalTypes.rename) {
return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
return __('Create new file');
},
buttonLabel() {
if (this.entryModal.type === modalTypes.tree) {
const entry = this.entries[this.path];
if (this.type === modalTypes.tree) {
return __('Create directory');
} else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree
? __('Rename folder')
: __('Rename file');
} else if (this.type === modalTypes.rename) {
return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
return __('Create file');
},
isCreatingNewFile() {
return this.entryModal.type === 'blob';
return this.type === modalTypes.blob;
},
placeholder() {
return this.isCreatingNewFile ? 'dir/file_name' : 'dir/';
......@@ -64,7 +63,7 @@ export default {
methods: {
...mapActions(['createTempEntry', 'renameEntry']),
submitForm() {
if (this.entryModal.type === modalTypes.rename) {
if (this.type === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
flash(
sprintf(s__('The name "%{name}" is already taken in this directory.'), {
......@@ -82,7 +81,7 @@ export default {
parentPath = parentPath.join('/');
this.renameEntry({
path: this.entryModal.entry.path,
path: this.path,
name: entryName,
parentPath,
});
......@@ -90,17 +89,17 @@ export default {
} else {
this.createTempEntry({
name: this.name,
type: this.entryModal.type,
type: this.type,
});
}
},
createFromTemplate(template) {
this.createTempEntry({
name: template.name,
type: this.entryModal.type,
type: this.type,
});
$('#ide-new-entry').modal('toggle');
this.$refs.modal.toggle();
},
focusInput() {
const name = this.entries[this.entryName] ? this.entries[this.entryName].name : null;
......@@ -112,8 +111,23 @@ export default {
this.$refs.fieldName.setSelectionRange(inputValue.indexOf(name), inputValue.length);
}
},
closedModal() {
resetData() {
this.name = '';
this.path = '';
this.type = modalTypes.blob;
},
open(type = modalTypes.blob, path = '') {
this.type = type;
this.path = path;
this.$refs.modal.show();
// wait for modal to show first
this.$nextTick(() => {
this.focusInput();
});
},
close() {
this.$refs.modal.hide();
},
},
};
......@@ -121,15 +135,15 @@ export default {
<template>
<gl-modal
id="ide-new-entry"
class="qa-new-file-modal"
:header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success"
modal-size="lg"
@submit="submitForm"
@open="focusInput"
@closed="closedModal"
ref="modal"
modal-id="ide-new-entry"
modal-class="qa-new-file-modal"
:title="modalTitle"
:ok-title="buttonLabel"
ok-variant="success"
size="lg"
@ok="submitForm"
@hide="resetData"
>
<div class="form-group row">
<label class="label-bold col-form-label col-sm-2"> {{ __('Name') }} </label>
......
......@@ -78,6 +78,7 @@ export const commitItemIconMap = {
export const modalTypes = {
rename: 'rename',
tree: 'tree',
blob: 'blob',
};
export const commitActionTypes = {
......
......@@ -14,13 +14,12 @@ export const computeDiff = (originalContent, newContent) => {
endLineNumber: lineNumber + change.count - 1,
});
} else if ('added' in change || 'removed' in change) {
acc.push(
Object.assign({}, change, {
lineNumber,
modified: undefined,
endLineNumber: lineNumber + change.count - 1,
}),
);
acc.push({
...change,
lineNumber,
modified: undefined,
endLineNumber: lineNumber + change.count - 1,
});
}
if (!change.removed) {
......
import $ from 'jquery';
import Vue from 'vue';
import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
......@@ -176,13 +175,6 @@ export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
export const setErrorMessage = ({ commit }, errorMessage) =>
commit(types.SET_ERROR_MESSAGE, errorMessage);
export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
commit(types.OPEN_NEW_ENTRY_MODAL, { type, path });
// open the modal manually so we don't mess around with dropdown/rows
$('#ide-new-entry').modal('show');
};
export const deleteEntry = ({ commit, dispatch, state }, path) => {
const entry = state.entries[path];
const { prevPath, prevName, prevParentPath } = entry;
......
......@@ -73,7 +73,6 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
export const DELETE_ENTRY = 'DELETE_ENTRY';
export const RENAME_ENTRY = 'RENAME_ENTRY';
export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY';
......
......@@ -192,15 +192,6 @@ export default {
[types.SET_ERROR_MESSAGE](state, errorMessage) {
Object.assign(state, { errorMessage });
},
[types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) {
Object.assign(state, {
entryModal: {
type,
path,
entry: { ...state.entries[path] },
},
});
},
[types.DELETE_ENTRY](state, path) {
const entry = state.entries[path];
const { tempFile = false } = entry;
......
......@@ -16,9 +16,7 @@ export default {
});
Object.assign(state, {
projects: Object.assign({}, state.projects, {
[projectPath]: project,
}),
projects: { ...state.projects, [projectPath]: project },
});
},
[types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) {
......
......@@ -14,12 +14,13 @@ export default {
},
[types.CREATE_TREE](state, { treePath }) {
Object.assign(state, {
trees: Object.assign({}, state.trees, {
trees: {
...state.trees,
[treePath]: {
tree: [],
loading: true,
},
}),
},
});
},
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
......
......@@ -32,9 +32,7 @@ export function removeCommentIndicator(imageFrameEl) {
commentIndicatorEl.remove();
}
return Object.assign({}, meta, {
removed: willRemove,
});
return { ...meta, removed: willRemove };
}
export function showCommentIndicator(imageFrameEl, coordinate) {
......
......@@ -4,12 +4,7 @@ export function setPositionDataAttribute(el, options) {
const { x, y, width, height } = options;
const { position } = el.dataset;
const positionObject = Object.assign({}, JSON.parse(position), {
x,
y,
width,
height,
});
const positionObject = { ...JSON.parse(position), x, y, width, height };
el.setAttribute('data-position', JSON.stringify(positionObject));
}
......
......@@ -75,9 +75,7 @@ export default class ImageDiff {
if (this.renderCommentBadge) {
imageDiffHelper.addImageCommentBadge(this.imageFrameEl, options);
} else {
const numberBadgeOptions = Object.assign({}, options, {
badgeText: index + 1,
});
const numberBadgeOptions = { ...options, badgeText: index + 1 };
imageDiffHelper.addImageBadge(this.imageFrameEl, numberBadgeOptions);
}
......
......@@ -220,7 +220,7 @@ export const fetchJobsForStage = ({ dispatch }, stage = {}) => {
},
})
.then(({ data }) => {
const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true }));
const retriedJobs = data.retried.map(job => ({ ...job, retried: true }));
const jobs = data.latest_statuses.concat(retriedJobs);
dispatch('receiveJobsForStageSuccess', jobs);
......@@ -236,7 +236,7 @@ export const receiveJobsForStageError = ({ commit }) => {
export const triggerManualJob = ({ state }, variables) => {
const parsedVariables = variables.map(variable => {
const copyVar = Object.assign({}, variable);
const copyVar = { ...variable };
delete copyVar.id;
return copyVar;
});
......
......@@ -270,8 +270,10 @@ export function getWebSocketUrl(path) {
export function queryToObject(query) {
const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query;
return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => {
const p = curr.split('=');
accumulator[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
const [key, value] = curr.split('=');
if (value !== undefined) {
accumulator[decodeURIComponent(key)] = decodeURIComponent(value);
}
return accumulator;
}, {});
}
......
<script>
import {
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownHeader,
GlNewDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIcon,
} from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import Api from '~/api';
import createFlash from '~/flash';
import { intersection, debounce } from 'lodash';
export default {
components: {
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownHeader,
GlNewDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIcon,
},
model: {
prop: 'preselectedMilestones',
event: 'change',
},
props: {
projectId: {
type: String,
required: true,
},
preselectedMilestones: {
type: Array,
default: () => [],
required: false,
},
extraLinks: {
type: Array,
default: () => [],
required: false,
},
},
data() {
return {
searchQuery: '',
projectMilestones: [],
searchResults: [],
selectedMilestones: [],
requestCount: 0,
};
},
translations: {
milestone: __('Milestone'),
selectMilestone: __('Select milestone'),
noMilestone: __('No milestone'),
noResultsLabel: __('No matching results'),
searchMilestones: __('Search Milestones'),
},
computed: {
selectedMilestonesLabel() {
if (this.milestoneTitles.length === 1) {
return this.milestoneTitles[0];
}
if (this.milestoneTitles.length > 1) {
const firstMilestoneName = this.milestoneTitles[0];
const numberOfOtherMilestones = this.milestoneTitles.length - 1;
return sprintf(__('%{firstMilestoneName} + %{numberOfOtherMilestones} more'), {
firstMilestoneName,
numberOfOtherMilestones,
});
}
return this.$options.translations.noMilestone;
},
milestoneTitles() {
return this.preselectedMilestones.map(milestone => milestone.title);
},
dropdownItems() {
return this.searchResults.length ? this.searchResults : this.projectMilestones;
},
noResults() {
return this.searchQuery.length > 2 && this.searchResults.length === 0;
},
isLoading() {
return this.requestCount !== 0;
},
},
mounted() {
this.fetchMilestones();
},
methods: {
fetchMilestones() {
this.requestCount += 1;
Api.projectMilestones(this.projectId)
.then(({ data }) => {
this.projectMilestones = this.getTitles(data);
this.selectedMilestones = intersection(this.projectMilestones, this.milestoneTitles);
})
.catch(() => {
createFlash(__('An error occurred while loading milestones'));
})
.finally(() => {
this.requestCount -= 1;
});
},
searchMilestones: debounce(function searchMilestones() {
this.requestCount += 1;
const options = {
search: this.searchQuery,
scope: 'milestones',
};
if (this.searchQuery.length < 3) {
this.requestCount -= 1;
this.searchResults = [];
return;
}
Api.projectSearch(this.projectId, options)
.then(({ data }) => {
const searchResults = this.getTitles(data);
this.searchResults = searchResults.length ? searchResults : [];
})
.catch(() => {
createFlash(__('An error occurred while searching for milestones'));
})
.finally(() => {
this.requestCount -= 1;
});
}, 100),
toggleMilestoneSelection(clickedMilestone) {
if (!clickedMilestone) return [];
let milestones = [...this.preselectedMilestones];
const hasMilestone = this.milestoneTitles.includes(clickedMilestone);
if (hasMilestone) {
milestones = milestones.filter(({ title }) => title !== clickedMilestone);
} else {
milestones.push({ title: clickedMilestone });
}
return milestones;
},
onMilestoneClicked(clickedMilestone) {
const milestones = this.toggleMilestoneSelection(clickedMilestone);
this.$emit('change', milestones);
this.selectedMilestones = intersection(
this.projectMilestones,
milestones.map(milestone => milestone.title),
);
},
isSelectedMilestone(milestoneTitle) {
return this.selectedMilestones.includes(milestoneTitle);
},
getTitles(milestones) {
return milestones.filter(({ state }) => state === 'active').map(({ title }) => title);
},
},
};
</script>
<template>
<gl-new-dropdown>
<template slot="button-content">
<span ref="buttonText" class="flex-grow-1 ml-1 text-muted">{{
selectedMilestonesLabel
}}</span>
<gl-icon name="chevron-down" />
</template>
<gl-new-dropdown-header>
<span class="text-center d-block">{{ $options.translations.selectMilestone }}</span>
</gl-new-dropdown-header>
<gl-new-dropdown-divider />
<gl-search-box-by-type
v-model.trim="searchQuery"
class="m-2"
:placeholder="this.$options.translations.searchMilestones"
@input="searchMilestones"
/>
<gl-new-dropdown-item @click="onMilestoneClicked(null)">
<span :class="{ 'pl-4': true, 'selected-item': selectedMilestones.length === 0 }">
{{ $options.translations.noMilestone }}
</span>
</gl-new-dropdown-item>
<gl-new-dropdown-divider />
<template v-if="isLoading">
<gl-loading-icon />
<gl-new-dropdown-divider />
</template>
<template v-else-if="noResults">
<div class="dropdown-item-space">
<span ref="noResults" class="pl-4">{{ $options.translations.noResultsLabel }}</span>
</div>
<gl-new-dropdown-divider />
</template>
<template v-else-if="dropdownItems.length">
<gl-new-dropdown-item
v-for="item in dropdownItems"
:key="item"
role="milestone option"
@click="onMilestoneClicked(item)"
>
<span :class="{ 'pl-4': true, 'selected-item': isSelectedMilestone(item) }">
{{ item }}
</span>
</gl-new-dropdown-item>
<gl-new-dropdown-divider />
</template>
<gl-new-dropdown-item v-for="(item, idx) in extraLinks" :key="idx" :href="item.url">
<span class="pl-4">{{ item.text }}</span>
</gl-new-dropdown-item>
</gl-new-dropdown>
</template>
......@@ -266,8 +266,17 @@ export default {
);
}
},
expandedPanel: {
handler({ group, panel }) {
const dashboardPath = this.currentDashboard || this.firstDashboard.path;
updateHistory({
url: panelToUrl(dashboardPath, group, panel),
title: document.title,
});
},
deep: true,
},
},
created() {
this.setInitialState({
metricsEndpoint: this.metricsEndpoint,
......
......@@ -198,3 +198,21 @@ export const OPERATORS = {
equalTo: '==',
lessThan: '<',
};
/**
* Dashboard yml files support custom user-defined variables that
* are rendered as input elements in the monitoring dashboard.
* These values can be edited by the user and are passed on to the
* the backend and eventually to Prometheus API proxy.
*
* As of 13.0, the supported types are:
* simple custom -> dropdown elements
* advanced custom -> dropdown elements
* text -> text input elements
*
* Custom variables have a simple and a advanced variant.
*/
export const VARIABLE_TYPES = {
custom: 'custom',
text: 'text',
};
......@@ -51,6 +51,18 @@ const emptyStateFromError = error => {
return metricStates.UNKNOWN_ERROR;
};
/**
* Maps an variables object to an array
* @returns {Array} The custom variables array to be send to the API
* in the format of [variable1, variable1_value]
* @param {Object} variables - Custom variables provided by the user
*/
const transformVariablesObjectArray = variables =>
Object.entries(variables)
.flat()
.map(encodeURIComponent);
export default {
/**
* Dashboard panels structure and global state
......
import { VARIABLE_TYPES } from '../constants';
/**
* This file exclusively deals with parsing user-defined variables
* in dashboard yml file.
*
* As of 13.0, simple custom and advanced custom variables are supported.
*
* In the future iterations, text and query variables will be
* supported
*
*/
/**
* Utility method to determine if a custom variable is
* simple or not. If its not simple, it is advanced.
*
* @param {Array|Object} customVar Array if simple, object if advanced
* @returns {Boolean} true if simple, false if advanced
*/
const isSimpleCustomVariable = customVar => Array.isArray(customVar);
/**
* Normalize simple and advanced custom variable options to a standard
* format
* @param {Object} custom variable option
* @returns {Object} normalized custom variable options
*/
const normalizeDropdownOptions = ({ default: defaultOpt = false, text, value }) => ({
default: defaultOpt,
text,
value,
});
/**
* Simple custom variables have an array of values.
* This method parses such variables options to a standard format.
*
* @param {String} opt option from simple custom variable
*/
const parseSimpleDropdownOptions = opt => ({ text: opt, value: opt });
/**
* Custom advanced variables are rendered as dropdown elements in the dashboard
* header. This method parses advanced custom variables.
*
* @param {Object} advVariable advance custom variable
* @returns {Object}
*/
const customAdvancedVariableParser = advVariable => {
const options = advVariable?.options?.values ?? [];
return {
type: VARIABLE_TYPES.custom,
label: advVariable.label,
options: options.map(normalizeDropdownOptions),
};
};
/**
* Custom simple variables are rendered as dropdown elements in the dashboard
* header. This method parses simple custom variables.
*
* Simple custom variables do not have labels so its set to null here.
*
* @param {Array} customVariable array of options
* @returns {Object}
*/
const customSimpleVariableParser = simpleVar => {
const options = (simpleVar || []).map(parseSimpleDropdownOptions);
return {
type: VARIABLE_TYPES.custom,
label: null,
options: options.map(normalizeDropdownOptions),
};
};
/**
* This method returns a parser based on the type of the variable.
* Currently, the supported variables are simple custom and
* advanced custom only. In the future, this method will support
* text and query variables.
*
* @param {Array|Object} variable
* @return {Function} parser method
*/
const getVariableParser = variable => {
if (isSimpleCustomVariable(variable)) {
return customSimpleVariableParser;
} else if (variable.type === VARIABLE_TYPES.custom) {
return customAdvancedVariableParser;
}
return () => null;
};
/**
* This method parses the templating property in the dashboard yml file.
* The templating property has variables that are rendered as input elements
* for the user to edit. The values from input elements are relayed to
* backend and eventually Prometheus API.
*
* This method currently is not used anywhere. Once the issue
* https://gitlab.com/gitlab-org/gitlab/-/issues/214536 is completed,
* this method will have been used by the monitoring dashboard.
*
* @param {Object} templating templating variables from the dashboard yml file
* @returns {Object} a map of processed templating variables
*/
export const parseTemplatingVariables = ({ variables = {} } = {}) =>
Object.entries(variables).reduce((acc, [key, variable]) => {
// get the parser
const parser = getVariableParser(variable);
// parse the variable
const parsedVar = parser(variable);
// for simple custom variable label is null and it should be
// replace with key instead
if (parsedVar) {
acc[key] = {
...parsedVar,
label: parsedVar.label || key,
};
}
return acc;
}, {});
export default {};
import { omit, pickBy } from 'lodash';
import { omit } from 'lodash';
import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
import {
timeRangeParamNames,
......@@ -196,25 +196,30 @@ export const expandedPanelPayloadFromUrl = (dashboard, search = window.location.
* Convert panel information to a URL for the user to
* bookmark or share highlighting a specific panel.
*
* @param {String} dashboardPath - Dashboard path used as identifier
* @param {String} group - Group Identifier
* If no group/panel is set, the dashboard URL is returned.
*
* @param {?String} dashboard - Dashboard path, used as identifier for a dashboard
* @param {?String} group - Group Identifier
* @param {?Object} panel - Panel object from the dashboard
* @param {?String} url - Base URL including current search params
* @returns Dashboard URL which expands a panel (chart)
*/
export const panelToUrl = (dashboardPath, group, panel, url = window.location.href) => {
if (!group || !panel) {
return null;
export const panelToUrl = (dashboard = null, group, panel, url = window.location.href) => {
const params = {
dashboard,
};
if (group && panel) {
params.group = group;
params.title = panel.title;
params.y_label = panel.y_label;
} else {
// Remove existing parameters if any
params.group = null;
params.title = null;
params.y_label = null;
}
const params = pickBy(
{
dashboard: dashboardPath,
group,
title: panel.title,
y_label: panel.y_label,
},
value => value != null,
);
return mergeUrlParams(params, url);
};
......
......@@ -15,6 +15,19 @@ export default () => {
notesApp,
},
store,
data() {
const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
noteableData.targetType = notesDataset.targetType;
return {
noteableData,
currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: JSON.parse(notesDataset.notesData),
helpPagePath: notesDataset.helpPagePath,
};
},
computed: {
...mapGetters(['discussionTabCounter']),
...mapState({
......@@ -54,19 +67,6 @@ export default () => {
updateDiscussionTabCounter() {
this.notesCountBadge.text(this.discussionTabCounter);
},
dataset() {
const data = this.$el.dataset;
const noteableData = JSON.parse(data.noteableData);
noteableData.noteableType = data.noteableType;
noteableData.targetType = data.targetType;
return {
noteableData,
notesData: JSON.parse(data.notesData),
userData: JSON.parse(data.currentUserData),
helpPagePath: data.helpPagePath,
};
},
},
render(createElement) {
// NOTE: Even though `discussionKeyboardNavigator` is added to the `notes-app`,
......@@ -76,8 +76,11 @@ export default () => {
return createElement(discussionKeyboardNavigator, [
createElement('notes-app', {
props: {
...this.dataset(),
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
shouldShow: this.isShowTabActive,
helpPagePath: this.helpPagePath,
},
}),
]);
......
......@@ -3,7 +3,15 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { isEmpty } from 'lodash';
import Autosize from 'autosize';
import { GlAlert, GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui';
import {
GlAlert,
GlFormCheckbox,
GlIcon,
GlIntersperse,
GlLink,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
......@@ -24,6 +32,7 @@ import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'CommentForm',
......@@ -36,11 +45,16 @@ export default {
loadingButton,
TimelineEntryItem,
GlAlert,
GlFormCheckbox,
GlIcon,
GlIntersperse,
GlLink,
GlSprintf,
},
mixins: [issuableStateMixin],
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [issuableStateMixin, glFeatureFlagsMixin()],
props: {
noteableType: {
type: String,
......@@ -51,6 +65,7 @@ export default {
return {
note: '',
noteType: constants.COMMENT,
noteIsConfidential: false,
isSubmitting: false,
isSubmitButtonDisabled: true,
};
......@@ -138,6 +153,9 @@ export default {
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
},
confidentialNotesEnabled() {
return Boolean(this.glFeatures.confidentialNotes);
},
},
watch: {
note(newNote) {
......@@ -185,6 +203,7 @@ export default {
note: {
noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
confidential: this.noteIsConfidential,
note: this.note,
},
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
......@@ -285,6 +304,7 @@ export default {
if (shouldClear) {
this.note = '';
this.noteIsConfidential = false;
this.resizeTextarea();
this.$refs.markdownField.previewMarkdown = false;
}
......@@ -411,6 +431,19 @@ js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
</p>
</gl-alert>
<div class="note-form-actions">
<div v-if="confidentialNotesEnabled" class="js-confidential-note-toggle mb-4">
<gl-form-checkbox v-model="noteIsConfidential">
<gl-icon name="eye-slash" :size="12" />
{{ __('Mark this comment as private') }}
<gl-icon
v-gl-tooltip:tooltipcontainer.bottom
name="question"
:size="12"
:title="__('Private comments are accessible by internal staff only')"
class="gl-text-gray-800"
/>
</gl-form-checkbox>
</div>
<div
class="float-left btn-group
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
......
<script>
import { mapActions } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
......@@ -7,6 +8,10 @@ export default {
components: {
timeAgoTooltip,
GitlabTeamMemberBadge,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
author: {
......@@ -44,6 +49,11 @@ export default {
required: false,
default: true,
},
isConfidential: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -160,7 +170,7 @@ export default {
</span>
</template>
<span v-else>{{ __('A deleted user') }}</span>
<span class="note-headline-light note-headline-meta">
<span class="note-headline-light note-headline-meta d-inline-flex align-items-center">
<span class="system-note-message"> <slot></slot> </span>
<template v-if="createdAt">
<span ref="actionText" class="system-note-separator">
......@@ -177,6 +187,15 @@ export default {
</a>
<time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" />
</template>
<gl-icon
v-if="isConfidential"
ref="confidentialIndicator"
v-gl-tooltip:tooltipcontainer.bottom
name="eye-slash"
:size="14"
:title="__('Private comments are accessible by internal staff only')"
class="ml-1 gl-text-gray-800"
/>
<slot name="extra-controls"></slot>
<i
v-if="showSpinner"
......
......@@ -255,10 +255,16 @@ export default {
</div>
<div class="timeline-content">
<div class="note-header">
<note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id">
<note-header
v-once
:author="author"
:created-at="note.created_at"
:note-id="note.id"
:is-confidential="note.confidential"
>
<slot slot="note-header-info" name="note-header-info"></slot>
<span v-if="commit" v-html="actionText"></span>
<span v-else class="d-none d-sm-inline">&middot;</span>
<span v-else class="d-none d-sm-inline mr-1">&middot;</span>
</note-header>
<note-actions
:author-id="author.id"
......
......@@ -230,10 +230,11 @@ export default {
const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') };
if (doesHashExistInUrl(constants.NOTE_UNDERSCORE)) {
return Object.assign({}, defaultConfig, {
return {
...defaultConfig,
filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
persistFilter: false,
});
};
}
return defaultConfig;
},
......
......@@ -2,11 +2,9 @@ import Vue from 'vue';
import notesApp from './components/notes_app.vue';
import initDiscussionFilters from './discussion_filters';
import initSortDiscussions from './sort_discussions';
import createStore from './stores';
import { store } from './stores';
document.addEventListener('DOMContentLoaded', () => {
const store = createStore();
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-notes',
......@@ -14,36 +12,38 @@ document.addEventListener('DOMContentLoaded', () => {
notesApp,
},
store,
methods: {
setData() {
const notesDataset = this.$el.dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData);
let currentUserData = {};
noteableData.noteableType = notesDataset.noteableType;
noteableData.targetType = notesDataset.targetType;
data() {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData);
let currentUserData = {};
if (parsedUserData) {
currentUserData = {
id: parsedUserData.id,
name: parsedUserData.name,
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
};
}
noteableData.noteableType = notesDataset.noteableType;
noteableData.targetType = notesDataset.targetType;
return {
noteableData,
userData: currentUserData,
notesData: JSON.parse(notesDataset.notesData),
if (parsedUserData) {
currentUserData = {
id: parsedUserData.id,
name: parsedUserData.name,
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
};
},
}
return {
noteableData,
currentUserData,
notesData: JSON.parse(notesDataset.notesData),
};
},
render(createElement) {
return createElement('notes-app', {
props: { ...this.setData() },
props: {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
},
});
},
});
......
......@@ -248,7 +248,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const hasQuickActions = utils.hasQuickActions(placeholderText);
const replyId = noteData.data.in_reply_to_discussion_id;
let methodToDispatch;
const postData = Object.assign({}, noteData);
const postData = { ...noteData };
if (postData.isDraft === true) {
methodToDispatch = replyId
? 'batchComments/addDraftToDiscussion'
......
......@@ -4,4 +4,8 @@ import notesModule from './modules';
Vue.use(Vuex);
export default () => new Vuex.Store(notesModule());
// NOTE: Giving the option to either use a singleton or new instance of notes.
const notesStore = () => new Vuex.Store(notesModule());
export default notesStore;
export const store = notesStore();
......@@ -25,6 +25,7 @@ export default () => ({
},
userData: {},
noteableData: {
confidential: false, // TODO: Move data like this to Issue Store, should not be apart of notes.
current_user: {},
preview_note_path: 'path/to/preview',
},
......
......@@ -10,7 +10,6 @@ import { __ } from '~/locale';
import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate';
import initVueAlerts from '../../../../vue_alerts';
Vue.use(Translate);
Vue.use(GlToast);
......@@ -57,5 +56,3 @@ document.addEventListener(
},
}),
);
document.addEventListener('DOMContentLoaded', initVueAlerts);
......@@ -12,10 +12,8 @@ import initReadMore from '~/read_more';
import leaveByUrl from '~/namespaces/leave_by_url';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
import initVueAlerts from '../../../vue_alerts';
document.addEventListener('DOMContentLoaded', () => {
initVueAlerts();
initReadMore();
new Star(); // eslint-disable-line no-new
notificationsDropdown();
......
......@@ -99,9 +99,10 @@ export default {
// 3. If GitLab user does not have avatar, they might have a Gravatar
} else if (this.pipeline.commit.author_gravatar_url) {
commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
commitAuthorInformation = {
...this.pipeline.commit.author,
avatar_url: this.pipeline.commit.author_gravatar_url,
});
};
}
// 4. If committer is not a GitLab User, they can have a Gravatar
} else {
......
......@@ -29,7 +29,14 @@ export default {
successPercentage() {
// Returns a full number when the decimals equal .00.
// Otherwise returns a float to two decimal points
return Number(((this.report.success_count / this.report.total_count) * 100 || 0).toFixed(2));
// Do not include skipped tests as part of the total when doing success calculations.
const totalCompletedCount = this.report.total_count - this.report.skipped_count;
if (totalCompletedCount > 0) {
return Number(((this.report.success_count / totalCompletedCount) * 100 || 0).toFixed(2));
}
return 0;
},
formattedDuration() {
return formatTime(secondsToMilliseconds(this.report.total_time));
......
......@@ -15,7 +15,7 @@ export default class PipelineStore {
* @param {Object} pipeline
*/
storePipeline(pipeline = {}) {
const pipelineCopy = Object.assign({}, pipeline);
const pipelineCopy = { ...pipeline };
if (pipelineCopy.triggered_by) {
pipelineCopy.triggered_by = [pipelineCopy.triggered_by];
......
......@@ -21,7 +21,7 @@ export default {
state.original = Object.freeze(settings);
},
[types.RESET_SETTINGS](state) {
state.settings = Object.assign({}, state.original);
state.settings = { ...state.original };
},
[types.TOGGLE_LOADING](state) {
state.isLoading = !state.isLoading;
......
......@@ -41,5 +41,5 @@ export const NAME_REGEX_KEEP_LABEL = s__(
);
export const NAME_REGEX_KEEP_PLACEHOLDER = '';
export const NAME_REGEX_KEEP_DESCRIPTION = s__(
'ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported',
'ContainerRegistry|Regular expressions such as %{codeStart}.*-master%{codeEnd} or %{codeStart}release-.*%{codeEnd} are supported',
);
......@@ -9,6 +9,7 @@ import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import AssetLinksForm from './asset_links_form.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
export default {
name: 'ReleaseEditApp',
......@@ -18,6 +19,7 @@ export default {
GlButton,
MarkdownField,
AssetLinksForm,
MilestoneCombobox,
},
directives: {
autofocusonshow,
......@@ -32,6 +34,10 @@ export default {
'markdownPreviewPath',
'releasesPagePath',
'updateReleaseApiDocsPath',
'release',
'newMilestonePath',
'manageMilestonesPath',
'projectId',
]),
...mapGetters('detail', ['isValid']),
showForm() {
......@@ -82,6 +88,14 @@ export default {
this.updateReleaseNotes(notes);
},
},
releaseMilestones: {
get() {
return this.$store.state.detail.release.milestones;
},
set(milestones) {
this.updateReleaseMilestones(milestones);
},
},
cancelPath() {
return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
},
......@@ -91,6 +105,18 @@ export default {
isSaveChangesDisabled() {
return this.isUpdatingRelease || !this.isValid;
},
milestoneComboboxExtraLinks() {
return [
{
text: __('Create new'),
url: this.newMilestonePath,
},
{
text: __('Manage milestones'),
url: this.manageMilestonesPath,
},
];
},
},
created() {
this.fetchRelease();
......@@ -101,6 +127,7 @@ export default {
'updateRelease',
'updateReleaseTitle',
'updateReleaseNotes',
'updateReleaseMilestones',
]),
},
};
......@@ -137,6 +164,16 @@ export default {
class="form-control"
/>
</gl-form-group>
<gl-form-group class="w-50">
<label>{{ __('Milestones') }}</label>
<div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
<milestone-combobox
v-model="releaseMilestones"
:project-id="projectId"
:extra-links="milestoneComboboxExtraLinks"
/>
</div>
</gl-form-group>
<gl-form-group>
<label for="release-notes">{{ __('Release notes') }}</label>
<div class="bordered-box pr-3 pl-3">
......@@ -158,8 +195,7 @@ export default {
:placeholder="__('Write your release notes or drag your files here…')"
@keydown.meta.enter="updateRelease()"
@keydown.ctrl.enter="updateRelease()"
>
</textarea>
></textarea>
</markdown-field>
</div>
</gl-form-group>
......@@ -174,12 +210,9 @@ export default {
type="submit"
:aria-label="__('Save changes')"
:disabled="isSaveChangesDisabled"
>{{ __('Save changes') }}</gl-button
>
{{ __('Save changes') }}
</gl-button>
<gl-button :href="cancelPath" class="js-cancel-button">
{{ __('Cancel') }}
</gl-button>
<gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button>
</div>
</form>
</div>
......
<script>
import { mapState, mapActions } from 'vuex';
import { GlSkeletonLoading, GlEmptyState, GlLink } from '@gitlab/ui';
import { GlSkeletonLoading, GlEmptyState, GlLink, GlButton } from '@gitlab/ui';
import {
getParameterByName,
historyPushState,
......@@ -18,6 +18,7 @@ export default {
ReleaseBlock,
TablePagination,
GlLink,
GlButton,
},
props: {
projectId: {
......@@ -69,14 +70,16 @@ export default {
</script>
<template>
<div class="flex flex-column mt-2">
<gl-link
<gl-button
v-if="newReleasePath"
:href="newReleasePath"
:aria-describedby="shouldRenderEmptyState && 'releases-description'"
class="btn btn-success align-self-end mb-2 js-new-release-btn"
category="primary"
variant="success"
class="align-self-end mb-2 js-new-release-btn"
>
{{ __('New release') }}
</gl-link>
</gl-button>
<gl-skeleton-loading v-if="isLoading" class="js-loading" />
......
<script>
import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import { setUrlParams } from '~/lib/utils/url_utility';
......@@ -10,6 +10,7 @@ export default {
GlLink,
GlBadge,
Icon,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -50,14 +51,16 @@ export default {
__('Upcoming Release')
}}</gl-badge>
</h2>
<gl-link
<gl-button
v-if="editLink"
v-gl-tooltip
class="btn btn-default append-right-10 js-edit-button ml-2"
category="primary"
variant="default"
class="append-right-10 js-edit-button ml-2 pb-2"
:title="__('Edit this release')"
:href="editLink"
>
<icon name="pencil" />
</gl-link>
</gl-button>
</div>
</template>
......@@ -18,7 +18,12 @@ export const fetchRelease = ({ dispatch, state }) => {
return api
.release(state.projectId, state.tagName)
.then(({ data: release }) => {
.then(({ data }) => {
const release = {
...data,
milestones: data.milestones || [],
};
dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true }));
})
.catch(error => {
......@@ -28,6 +33,8 @@ export const fetchRelease = ({ dispatch, state }) => {
export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
export const updateReleaseMilestones = ({ commit }, milestones) =>
commit(types.UPDATE_RELEASE_MILESTONES, milestones);
export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => {
......@@ -45,12 +52,14 @@ export const updateRelease = ({ dispatch, state, getters }) => {
dispatch('requestUpdateRelease');
const { release } = state;
const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : [];
return (
api
.updateRelease(state.projectId, state.tagName, {
name: release.name,
description: release.description,
milestones,
})
/**
......
......@@ -4,6 +4,7 @@ export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES';
export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
......
......@@ -28,6 +28,10 @@ export default {
state.release.description = notes;
},
[types.UPDATE_RELEASE_MILESTONES](state, milestones) {
state.release.milestones = milestones;
},
[types.REQUEST_UPDATE_RELEASE](state) {
state.isUpdatingRelease = true;
},
......
......@@ -6,6 +6,8 @@ export default ({
markdownPreviewPath,
updateReleaseApiDocsPath,
releaseAssetsDocsPath,
manageMilestonesPath,
newMilestonePath,
}) => ({
projectId,
tagName,
......@@ -14,6 +16,8 @@ export default ({
markdownPreviewPath,
updateReleaseApiDocsPath,
releaseAssetsDocsPath,
manageMilestonesPath,
newMilestonePath,
/** The Release object */
release: null,
......
......@@ -4,6 +4,7 @@ import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
import actionCable from '~/actioncable_consumer';
export default {
subscription: null,
name: 'AssigneesRealtime',
props: {
mediator: {
......@@ -36,6 +37,9 @@ export default {
mounted() {
this.initActionCablePolling();
},
beforeDestroy() {
this.$options.subscription.unsubscribe();
},
methods: {
received(data) {
if (data.event === 'updated') {
......@@ -43,7 +47,7 @@ export default {
}
},
initActionCablePolling() {
actionCable.subscriptions.create(
this.$options.subscription = actionCable.subscriptions.create(
{
channel: 'IssuesChannel',
project_path: this.projectPath,
......
<script>
import { mapState } from 'vuex';
import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
import EditForm from './edit_form.vue';
import recaptchaModalImplementor from '~/vue_shared/mixins/recaptcha_modal_implementor';
export default {
components: {
editForm,
EditForm,
Icon,
},
directives: {
......@@ -17,10 +18,6 @@ export default {
},
mixins: [recaptchaModalImplementor],
props: {
isConfidential: {
required: true,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
......@@ -36,11 +33,12 @@ export default {
};
},
computed: {
...mapState({ confidential: ({ noteableData }) => noteableData.confidential }),
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
return this.confidential ? 'eye-slash' : 'eye';
},
tooltipLabel() {
return this.isConfidential ? __('Confidential') : __('Not confidential');
return this.confidential ? __('Confidential') : __('Not confidential');
},
},
created() {
......@@ -95,17 +93,16 @@ export default {
data-track-label="right_sidebar"
data-track-property="confidentiality"
@click.prevent="toggleForm"
>{{ __('Edit') }}</a
>
{{ __('Edit') }}
</a>
</div>
<div class="value sidebar-item-value hide-collapsed">
<editForm
<edit-form
v-if="edit"
:is-confidential="isConfidential"
:is-confidential="confidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
<div v-if="!isConfidential" class="no-value sidebar-item-value">
<div v-if="!confidential" class="no-value sidebar-item-value">
<icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" />
{{ __('Not confidential') }}
</div>
......
......@@ -10,6 +10,7 @@ import sidebarParticipants from './components/participants/sidebar_participants.
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
import { store } from '~/notes/stores';
Vue.use(Translate);
Vue.use(VueApollo);
......@@ -59,8 +60,8 @@ function mountConfidentialComponent(mediator) {
const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
new ConfidentialComp({
store,
propsData: {
isConfidential: initialData.is_confidential,
isEditable: initialData.is_editable,
service: mediator.service,
},
......
extend type Query {
isSupportedContent: Boolean!
projectId: String!
project: String!
returnUrl: String
sourcePath: String!
username: String!
......
......@@ -6,7 +6,14 @@ import createRouter from './router';
import createApolloProvider from './graphql';
const initStaticSiteEditor = el => {
const { isSupportedContent, projectId, path: sourcePath, baseUrl } = el.dataset;
const {
isSupportedContent,
projectId,
path: sourcePath,
baseUrl,
namespace,
project,
} = el.dataset;
const { current_username: username } = window.gon;
const returnUrl = el.dataset.returnUrl || null;
......@@ -22,7 +29,7 @@ const initStaticSiteEditor = el => {
const router = createRouter(baseUrl);
const apolloProvider = createApolloProvider({
isSupportedContent: parseBoolean(isSupportedContent),
projectId,
project: `${namespace}/${project}`,
returnUrl,
sourcePath,
username,
......
......@@ -11,6 +11,8 @@ import PublishToolbar from '../components/publish_toolbar.vue';
import InvalidContentMessage from '../components/invalid_content_message.vue';
import SubmitChangesError from '../components/submit_changes_error.vue';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
export default {
components: {
RichContentEditor,
......@@ -23,13 +25,17 @@ export default {
SubmitChangesError,
},
mixins: [glFeatureFlagsMixin()],
apollo: {
isSupportedContent: {
query: appDataQuery,
},
},
computed: {
...mapState([
'content',
'isLoadingContent',
'isSavingChanges',
'isContentLoaded',
'isSupportedContent',
'returnUrl',
'title',
'submitChangesError',
......
......@@ -6,7 +6,6 @@ const createState = (initialState = {}) => ({
isLoadingContent: false,
isSavingChanges: false,
isSupportedContent: false,
isContentLoaded: false,
......
......@@ -13,14 +13,11 @@ Terminal.applyAddon(webLinks);
export default class GLTerminal {
constructor(element, options = {}) {
this.options = Object.assign(
{},
{
cursorBlink: true,
screenKeys: true,
},
options,
);
this.options = {
cursorBlink: true,
screenKeys: true,
...options,
};
this.container = element;
this.onDispose = [];
......
......@@ -164,7 +164,11 @@ export default {
'js-dropdown-button',
'js-btn-cancel-create',
'js-sidebar-dropdown-toggle',
].some(className => target?.classList.contains(className));
].some(
className =>
target?.classList.contains(className) ||
target?.parentElement.classList.contains(className),
);
const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
className => $(target).parents(className).length,
......
.selected-item::before {
content: '\f00c';
color: $green-500;
position: absolute;
left: 16px;
top: 16px;
transform: translateY(-50%);
font: 14px FontAwesome;
}
.dropdown-item-space {
padding: 8px 12px;
}
......@@ -40,7 +40,7 @@
h1,
h2,
h3,
h4:not(.modal-title),
h4,
h5,
h6,
code,
......@@ -80,10 +80,6 @@
background-color: $dropdown-hover-background;
}
.modal-body {
color: $gl-text-color;
}
.dropdown-menu-toggle svg,
.dropdown-menu-toggle svg:hover,
.ide-tree-header:not(.ide-pipeline-header) svg,
......
......@@ -660,10 +660,6 @@ $note-form-margin-left: 72px;
padding-bottom: 0;
}
.note-headline-light {
display: inline;
}
.note-headline-light,
.discussion-headline-light {
color: $gl-text-color-secondary;
......
......@@ -3,8 +3,10 @@
class Projects::AlertManagementController < Projects::ApplicationController
before_action :ensure_list_feature_enabled, only: :index
before_action :ensure_detail_feature_enabled, only: :details
before_action :authorize_read_alert_management_alert!
before_action do
push_frontend_feature_flag(:alert_list_status_filtering_enabled)
push_frontend_feature_flag(:create_issue_from_alert_enabled)
end
def index
......
......@@ -52,6 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action only: :show do
push_frontend_feature_flag(:real_time_issue_sidebar, @project)
push_frontend_feature_flag(:confidential_notes, @project)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
......@@ -85,11 +86,13 @@ class Projects::IssuesController < Projects::ApplicationController
)
build_params = issue_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve]
discussion_to_resolve: params[:discussion_to_resolve],
confidential: !!Gitlab::Utils.to_boolean(params[:issue][:confidential])
)
service = Issues::BuildService.new(project, current_user, build_params)
@issue = @noteable = service.execute
@merge_request_to_resolve_discussions_of = service.merge_request_to_resolve_discussions_of
@discussion_to_resolve = service.discussions_to_resolve.first if params[:discussion_to_resolve]
......
......@@ -7,6 +7,8 @@ class Projects::PagesDomainsController < Projects::ApplicationController
before_action :authorize_update_pages!
before_action :domain, except: [:new, :create]
helper_method :domain_presenter
def show
end
......@@ -27,7 +29,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end
def retry_auto_ssl
PagesDomains::RetryAcmeOrderService.new(@domain.pages_domain).execute
PagesDomains::RetryAcmeOrderService.new(@domain).execute
redirect_to project_pages_domain_path(@project, @domain)
end
......@@ -88,6 +90,10 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end
def domain
@domain ||= @project.pages_domains.find_by_domain!(params[:id].to_s).present(current_user: current_user)
@domain ||= @project.pages_domains.find_by_domain!(params[:id].to_s)
end
def domain_presenter
@domain_presenter ||= domain.present(current_user: current_user)
end
end
......@@ -31,7 +31,7 @@ module AlertManagement
end
def authorized?
Ability.allowed?(current_user, :read_alert_management_alerts, project)
Ability.allowed?(current_user, :read_alert_management_alert, project)
end
end
end
......@@ -18,7 +18,7 @@ module Mutations
null: true,
description: "The alert after mutation"
authorize :update_alert_management_alerts
authorize :update_alert_management_alert
private
......
......@@ -6,7 +6,7 @@ module Types
graphql_name 'AlertManagementAlert'
description "Describes an alert from the project's Alert Management"
authorize :read_alert_management_alerts
authorize :read_alert_management_alert
field :iid,
GraphQL::ID_TYPE,
......
......@@ -4,17 +4,18 @@ module Projects::AlertManagementHelper
def alert_management_data(current_user, project)
{
'project-path' => project.full_path,
'enable-alert-management-path' => project_settings_operations_path(project),
'enable-alert-management-path' => edit_project_service_path(project, AlertsService),
'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => 'false',
'alert-management-enabled' => Feature.enabled?(:alert_management_minimal, project).to_s
'user-can-enable-alert-management' => can?(current_user, :admin_project, project).to_s,
'alert-management-enabled' => (!!project.alerts_service_activated?).to_s
}
end
def alert_management_detail_data(project_path, alert_id)
def alert_management_detail_data(project, alert_id)
{
'alert-id' => alert_id,
'project-path' => project_path
'project-path' => project.full_path,
'new-issue-path' => new_project_issue_path(project)
}
end
end
......@@ -448,7 +448,7 @@ module ProjectsHelper
clusters: :read_cluster,
serverless: :read_cluster,
error_tracking: :read_sentry_issue,
alert_management: :read_alert_management,
alert_management: :read_alert_management_alert,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
......
......@@ -30,7 +30,9 @@ module ReleasesHelper
markdown_docs_path: help_page_path('user/markdown'),
releases_page_path: project_releases_path(@project, anchor: @release.tag),
update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'),
release_assets_docs_path: help_page(anchor: 'release-assets')
release_assets_docs_path: help_page(anchor: 'release-assets'),
manage_milestones_path: project_milestones_path(@project),
new_milestone_path: new_project_milestone_url(@project)
}
end
end
......@@ -115,8 +115,11 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
transition [:created, :waiting_for_resource, :preparing, :skipped, :scheduled] => :pending
transition [:created, :manual, :waiting_for_resource, :preparing, :skipped, :scheduled] => :pending
transition [:success, :failed, :canceled] => :running
# this is needed to ensure tests to be covered
transition [:running] => :running
end
event :request_resource do
......@@ -683,6 +686,8 @@ module Ci
variables.concat(merge_request.predefined_variables)
end
variables.append(key: 'CI_KUBERNETES_ACTIVE', value: 'true') if has_kubernetes_active?
if external_pull_request_event? && external_pull_request
variables.concat(external_pull_request.predefined_variables)
end
......
......@@ -42,8 +42,7 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
transition [:created, :waiting_for_resource, :preparing] => :pending
transition [:success, :failed, :canceled, :skipped] => :running
transition any - [:pending] => :pending
end
event :request_resource do
......
......@@ -5,7 +5,7 @@ module Clusters
class Knative < ApplicationRecord
VERSION = '0.9.0'
REPOSITORY = 'https://charts.gitlab.io'
METRICS_CONFIG = 'https://gitlab.com/gitlab-org/charts/knative/-/blob/v0.9.0/vendor/istio-metrics.yml'
METRICS_CONFIG = 'https://gitlab.com/gitlab-org/charts/knative/-/raw/v0.9.0/vendor/istio-metrics.yml'
FETCH_IP_ADDRESS_DELAY = 30.seconds
API_GROUPS_PATH = 'config/knative/api_groups.yml'
......
......@@ -1436,20 +1436,12 @@ class Project < ApplicationRecord
# Expires various caches before a project is renamed.
def expire_caches_before_rename(old_path)
repo = Repository.new(old_path, self, shard: repository_storage)
wiki = Repository.new("#{old_path}.wiki", self, shard: repository_storage, repo_type: Gitlab::GlRepository::WIKI)
design = Repository.new("#{old_path}#{Gitlab::GlRepository::DESIGN.path_suffix}", self, shard: repository_storage, repo_type: Gitlab::GlRepository::DESIGN)
project_repo = Repository.new(old_path, self, shard: repository_storage)
wiki_repo = Repository.new("#{old_path}#{Gitlab::GlRepository::WIKI.path_suffix}", self, shard: repository_storage, repo_type: Gitlab::GlRepository::WIKI)
design_repo = Repository.new("#{old_path}#{Gitlab::GlRepository::DESIGN.path_suffix}", self, shard: repository_storage, repo_type: Gitlab::GlRepository::DESIGN)
if repo.exists?
repo.before_delete
end
if wiki.exists?
wiki.before_delete
end
if design.exists?
design.before_delete
[project_repo, wiki_repo, design_repo].each do |repo|
repo.before_delete if repo.exists?
end
end
......
......@@ -236,11 +236,8 @@ class ProjectPolicy < BasePolicy
enable :read_merge_request
enable :read_sentry_issue
enable :update_sentry_issue
enable :read_alert_management
enable :read_prometheus
enable :read_metrics_dashboard_annotation
enable :read_alert_management_alerts
enable :update_alert_management_alerts
enable :metrics_dashboard
end
......@@ -306,6 +303,8 @@ class ProjectPolicy < BasePolicy
enable :create_metrics_dashboard_annotation
enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation
enable :read_alert_management_alert
enable :update_alert_management_alert
enable :create_design
enable :destroy_design
end
......
......@@ -7,6 +7,7 @@ module Projects
GENERIC_ALERT_SUMMARY_ANNOTATIONS = %w(monitoring_tool service hosts).freeze
MARKDOWN_LINE_BREAK = " \n".freeze
INCIDENT_LABEL_NAME = IncidentManagement::CreateIssueService::INCIDENT_LABEL[:title].freeze
METRIC_TIME_WINDOW = 30.minutes
def full_title
[environment_name, alert_title].compact.join(': ')
......@@ -119,9 +120,63 @@ module Projects
Array(hosts.value).join(' ')
end
def metric_embed_for_alert; end
def metric_embed_for_alert
url = embed_url_for_gitlab_alert || embed_url_for_self_managed_alert
"\n[](#{url})" if url
end
def embed_url_for_gitlab_alert
return unless gitlab_alert
metrics_dashboard_project_prometheus_alert_url(
project,
gitlab_alert.prometheus_metric_id,
environment_id: environment.id,
**alert_embed_window_params(embed_time)
)
end
def embed_url_for_self_managed_alert
return unless environment && full_query && title
metrics_dashboard_project_environment_url(
project,
environment,
embed_json: dashboard_for_self_managed_alert.to_json,
**alert_embed_window_params(embed_time)
)
end
def embed_time
starts_at ? Time.rfc3339(starts_at) : Time.current
end
def alert_embed_window_params(time)
{
start: format_embed_timestamp(time - METRIC_TIME_WINDOW),
end: format_embed_timestamp(time + METRIC_TIME_WINDOW)
}
end
def format_embed_timestamp(time)
time.utc.strftime('%FT%TZ')
end
def dashboard_for_self_managed_alert
{
panel_groups: [{
panels: [{
type: 'line-graph',
title: title,
y_label: y_label,
metrics: [{
query_range: full_query
}]
}]
}]
}
end
end
end
end
Projects::Prometheus::AlertPresenter.prepend_if_ee('EE::Projects::Prometheus::AlertPresenter')
# frozen_string_literal: true
class AccessibilityErrorEntity < Grape::Entity
expose :code
expose :type
expose :typeCode, as: :type_code
expose :message
expose :context
expose :selector
expose :runner
expose :runnerExtras, as: :runner_extras
end
# frozen_string_literal: true
class AccessibilityReportsComparerEntity < Grape::Entity
expose :status
expose :new_errors, using: AccessibilityErrorEntity
expose :resolved_errors, using: AccessibilityErrorEntity
expose :existing_errors, using: AccessibilityErrorEntity
expose :summary do
expose :total_count, as: :total
expose :resolved_count, as: :resolved
expose :errors_count, as: :errored
end
end
# frozen_string_literal: true
class AccessibilityReportsComparerSerializer < BaseSerializer
entity AccessibilityReportsComparerEntity
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment