Commit 552786bc authored by Shinya Maeda's avatar Shinya Maeda

Merge branch '263484-integration-descriptions-should-be-less-project-level-specific' into 'master'

Update integration descriptions to not be project-specific

See merge request gitlab-org/gitlab!44893
parents 6190e2e5 2cb69ca5
...@@ -290,7 +290,7 @@ gem 'gitlab_chronic_duration', '~> 0.10.6.2' ...@@ -290,7 +290,7 @@ gem 'gitlab_chronic_duration', '~> 0.10.6.2'
gem 'rack-proxy', '~> 0.6.0' gem 'rack-proxy', '~> 0.6.0'
gem 'sassc-rails', '~> 2.1.0' gem 'sassc-rails', '~> 2.1.0'
gem 'terser', '~> 1.0' gem 'gitlab-terser', '1.0.1.1'
gem 'addressable', '~> 2.7' gem 'addressable', '~> 2.7'
gem 'font-awesome-rails', '~> 4.7' gem 'font-awesome-rails', '~> 4.7'
......
...@@ -452,6 +452,8 @@ GEM ...@@ -452,6 +452,8 @@ GEM
rubocop-performance (~> 1.5.2) rubocop-performance (~> 1.5.2)
rubocop-rails (~> 2.5) rubocop-rails (~> 2.5)
rubocop-rspec (~> 1.36) rubocop-rspec (~> 1.36)
gitlab-terser (1.0.1.1)
execjs (>= 0.3.0, < 3)
gitlab_chronic_duration (0.10.6.2) gitlab_chronic_duration (0.10.6.2)
numerizer (~> 0.2) numerizer (~> 0.2)
gitlab_omniauth-ldap (2.1.1) gitlab_omniauth-ldap (2.1.1)
...@@ -1130,8 +1132,6 @@ GEM ...@@ -1130,8 +1132,6 @@ GEM
temple (0.8.2) temple (0.8.2)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
terser (1.0.1)
execjs (>= 0.3.0, < 3)
test-prof (0.12.0) test-prof (0.12.0)
text (1.3.1) text (1.3.1)
thin (1.7.2) thin (1.7.2)
...@@ -1333,6 +1333,7 @@ DEPENDENCIES ...@@ -1333,6 +1333,7 @@ DEPENDENCIES
gitlab-puma_worker_killer (~> 0.1.1.gitlab.1) gitlab-puma_worker_killer (~> 0.1.1.gitlab.1)
gitlab-sidekiq-fetcher (= 0.5.2) gitlab-sidekiq-fetcher (= 0.5.2)
gitlab-styles (~> 4.3.0) gitlab-styles (~> 4.3.0)
gitlab-terser (= 1.0.1.1)
gitlab_chronic_duration (~> 0.10.6.2) gitlab_chronic_duration (~> 0.10.6.2)
gitlab_omniauth-ldap (~> 2.1.1) gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2) gon (~> 6.2)
...@@ -1482,7 +1483,6 @@ DEPENDENCIES ...@@ -1482,7 +1483,6 @@ DEPENDENCIES
stackprof (~> 0.2.15) stackprof (~> 0.2.15)
state_machines-activerecord (~> 0.6.0) state_machines-activerecord (~> 0.6.0)
sys-filesystem (~> 1.1.6) sys-filesystem (~> 1.1.6)
terser (~> 1.0)
test-prof (~> 0.12.0) test-prof (~> 0.12.0)
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.9.1) timecop (~> 0.9.1)
......
<script> <script>
import * as Sentry from '@sentry/browser';
import { import {
GlAlert, GlAlert,
GlBadge, GlBadge,
...@@ -12,6 +11,7 @@ import { ...@@ -12,6 +11,7 @@ import {
GlButton, GlButton,
GlSafeHtmlDirective, GlSafeHtmlDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import alertQuery from '../graphql/queries/details.query.graphql'; import alertQuery from '../graphql/queries/details.query.graphql';
import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql'; import sidebarStatusQuery from '../graphql/queries/sidebar_status.query.graphql';
...@@ -30,6 +30,7 @@ import AlertSidebar from './alert_sidebar.vue'; ...@@ -30,6 +30,7 @@ import AlertSidebar from './alert_sidebar.vue';
import AlertMetrics from './alert_metrics.vue'; import AlertMetrics from './alert_metrics.vue';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import AlertSummaryRow from './alert_summary_row.vue'; import AlertSummaryRow from './alert_summary_row.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const containerEl = document.querySelector('.page-with-contextual-sidebar'); const containerEl = document.querySelector('.page-with-contextual-sidebar');
...@@ -76,6 +77,7 @@ export default { ...@@ -76,6 +77,7 @@ export default {
SystemNote, SystemNote,
AlertMetrics, AlertMetrics,
}, },
mixins: [glFeatureFlagsMixin()],
inject: { inject: {
projectPath: { projectPath: {
default: '', default: '',
...@@ -147,6 +149,15 @@ export default { ...@@ -147,6 +149,15 @@ export default {
this.$router.replace({ name: 'tab', params: { tabId } }); this.$router.replace({ name: 'tab', params: { tabId } });
}, },
}, },
environmentName() {
return this.shouldDisplayEnvironment && this.alert?.environment?.name;
},
environmentPath() {
return this.shouldDisplayEnvironment && this.alert?.environment?.path;
},
shouldDisplayEnvironment() {
return this.glFeatures.exposeEnvironmentPathInAlertDetails;
},
}, },
mounted() { mounted() {
this.trackPageViews(); this.trackPageViews();
...@@ -299,19 +310,18 @@ export default { ...@@ -299,19 +310,18 @@ export default {
</span> </span>
</alert-summary-row> </alert-summary-row>
<alert-summary-row <alert-summary-row
v-if="alert.environment" v-if="environmentName"
:label="`${s__('AlertManagement|Environment')}:`" :label="`${s__('AlertManagement|Environment')}:`"
> >
<gl-link <gl-link
v-if="alert.environmentUrl" v-if="environmentPath"
class="gl-display-inline-block" class="gl-display-inline-block"
data-testid="environmentUrl" data-testid="environmentPath"
:href="alert.environmentUrl" :href="environmentPath"
target="_blank"
> >
{{ alert.environment }} {{ environmentName }}
</gl-link> </gl-link>
<span v-else data-testid="environment">{{ alert.environment }}</span> <span v-else data-testid="environmentName">{{ environmentName }}</span>
</alert-summary-row> </alert-summary-row>
<alert-summary-row <alert-summary-row
v-if="alert.startedAt" v-if="alert.startedAt"
......
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as Sentry from '@sentry/browser'; import * as Sentry from '~/sentry/wrapper';
Vue.use(Vuex); Vue.use(Vuex);
......
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import produce from 'immer';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import produce from 'immer';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import createRouter from './router';
import AlertDetails from './components/alert_details.vue'; import AlertDetails from './components/alert_details.vue';
import sidebarStatusQuery from './graphql/queries/sidebar_status.query.graphql'; import sidebarStatusQuery from './graphql/queries/sidebar_status.query.graphql';
import createRouter from './router';
Vue.use(VueApollo); Vue.use(VueApollo);
......
...@@ -11,6 +11,10 @@ fragment AlertDetailItem on AlertManagementAlert { ...@@ -11,6 +11,10 @@ fragment AlertDetailItem on AlertManagementAlert {
updatedAt updatedAt
endedAt endedAt
hosts hosts
environment {
name
path
}
details details
runbook runbook
todos { todos {
......
<script> <script>
import * as Sentry from '@sentry/browser'; import * as Sentry from '~/sentry/wrapper';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format'; import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
......
import { masks } from 'dateformat';
import { formatDate } from '~/lib/utils/datetime_utility';
const { isoDate } = masks;
/**
* Takes an array of items and returns one item per month with the average of the `count`s from that month
* @param {Array} items
* @param {Number} items[index].count value to be averaged
* @param {String} items[index].recordedAt item dateTime time stamp to be collected into a month
* @param {Object} options
* @param {Object} options.shouldRound an option to specify whether the retuned averages should be rounded
* @return {Array} items collected into [month, average],
* where month is a dateTime string representing the first of the given month
* and average is the average of the count
*/
export function getAverageByMonth(items = [], options = {}) {
const { shouldRound = false } = options;
const itemsMap = items.reduce((memo, item) => {
const { count, recordedAt } = item;
const date = new Date(recordedAt);
const month = formatDate(new Date(date.getFullYear(), date.getMonth(), 1), isoDate);
if (memo[month]) {
const { sum, recordCount } = memo[month];
return { ...memo, [month]: { sum: sum + count, recordCount: recordCount + 1 } };
}
return { ...memo, [month]: { sum: count, recordCount: 1 } };
}, {});
return Object.keys(itemsMap).map(month => {
const { sum, recordCount } = itemsMap[month];
const avg = sum / recordCount;
if (shouldRound) {
return [month, Math.round(avg)];
}
return [month, avg];
});
}
...@@ -572,7 +572,7 @@ export class AwardsHandler { ...@@ -572,7 +572,7 @@ export class AwardsHandler {
} }
findMatchingEmojiElements(query) { findMatchingEmojiElements(query) {
const emojiMatches = this.emoji.searchEmoji(query).map(({ name }) => name); const emojiMatches = this.emoji.searchEmoji(query, { match: 'fuzzy' }).map(({ name }) => name);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]'); const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements.filter( const $matchingElements = $emojiElements.filter(
(i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0, (i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
......
...@@ -24,10 +24,10 @@ export default { ...@@ -24,10 +24,10 @@ export default {
}, },
inject: { inject: {
autoDevopsHelpPath: { autoDevopsHelpPath: {
type: String, default: '',
}, },
externalEndpointHelpPath: { externalEndpointHelpPath: {
type: String, default: '',
}, },
}, },
data() { data() {
......
import * as Sentry from '@sentry/browser'; import * as Sentry from '~/sentry/wrapper';
import Poll from '~/lib/utils/poll'; import Poll from '~/lib/utils/poll';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash'; import { deprecatedCreateFlash as flash } from '~/flash';
......
...@@ -122,67 +122,20 @@ export default { ...@@ -122,67 +122,20 @@ export default {
</script> </script>
<template> <template>
<li :class="{ 'js-toggle-container': collapsible }" class="commit flex-row"> <li :class="{ 'js-toggle-container': collapsible }" class="commit">
<div class="d-flex align-items-center align-self-start"> <div
<input class="d-block d-sm-flex flex-row-reverse justify-content-between align-items-start flex-lg-row-reverse"
v-if="isSelectable" >
class="mr-2" <div
type="checkbox" class="commit-actions flex-row d-none d-sm-flex align-items-start flex-wrap justify-content-end"
:checked="checked" >
@change="$emit('handleCheckboxChange', $event.target.checked)"
/>
<user-avatar-link
:link-href="authorUrl"
:img-src="authorAvatar"
:img-alt="authorName"
:img-size="40"
class="avatar-cell d-none d-sm-block"
/>
</div>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<a
:href="commit.commit_url"
class="commit-row-message item-title"
v-html="commit.title_html"
></a>
<span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
<gl-button
v-if="commit.description_html && collapsible"
class="js-toggle-button"
size="small"
icon="ellipsis_h"
:aria-label="__('Toggle commit description')"
/>
<div class="committer">
<a
:href="authorUrl"
:class="authorClass"
:data-user-id="authorId"
v-text="authorName"
></a>
{{ s__('CommitWidget|authored') }}
<time-ago-tooltip :time="commit.authored_date" />
</div>
<pre
v-if="commit.description_html"
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
class="commit-row-description gl-mb-3 text-dark"
v-html="commitDescription"
></pre>
</div>
<div class="commit-actions flex-row d-none d-sm-flex">
<div v-if="commit.signature_html" v-html="commit.signature_html"></div> <div v-if="commit.signature_html" v-html="commit.signature_html"></div>
<commit-pipeline-status <commit-pipeline-status
v-if="commit.pipeline_status_path" v-if="commit.pipeline_status_path"
:endpoint="commit.pipeline_status_path" :endpoint="commit.pipeline_status_path"
class="d-inline-flex" class="d-inline-flex mb-2"
/> />
<gl-button-group class="gl-ml-4" data-testid="commit-sha-group"> <gl-button-group class="gl-ml-4 gl-mb-4" data-testid="commit-sha-group">
<gl-button label class="gl-font-monospace" v-text="commit.short_id" /> <gl-button label class="gl-font-monospace" v-text="commit.short_id" />
<clipboard-button <clipboard-button
:text="commit.id" :text="commit.id"
...@@ -226,6 +179,62 @@ export default { ...@@ -226,6 +179,62 @@ export default {
</gl-button-group> </gl-button-group>
</div> </div>
</div> </div>
<div>
<div class="d-flex float-left align-items-center align-self-start">
<input
v-if="isSelectable"
class="mr-2"
type="checkbox"
:checked="checked"
@change="$emit('handleCheckboxChange', $event.target.checked)"
/>
<user-avatar-link
:link-href="authorUrl"
:img-src="authorAvatar"
:img-alt="authorName"
:img-size="40"
class="avatar-cell d-none d-sm-block"
/>
</div>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<a
:href="commit.commit_url"
class="commit-row-message item-title"
v-html="commit.title_html"
></a>
<span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
<gl-button
v-if="commit.description_html && collapsible"
class="js-toggle-button"
size="small"
icon="ellipsis_h"
:aria-label="__('Toggle commit description')"
/>
<div class="committer">
<a
:href="authorUrl"
:class="authorClass"
:data-user-id="authorId"
v-text="authorName"
></a>
{{ s__('CommitWidget|authored') }}
<time-ago-tooltip :time="commit.authored_date" />
</div>
</div>
</div>
</div>
</div>
<div>
<pre
v-if="commit.description_html"
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
class="commit-row-description gl-mb-3 text-dark"
v-html="commitDescription"
></pre>
</div> </div>
</li> </li>
</template> </template>
import { uniq } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import emojiAliases from 'emojis/aliases.json'; import emojiAliases from 'emojis/aliases.json';
import axios from '../lib/utils/axios_utils'; import axios from '../lib/utils/axios_utils';
...@@ -67,49 +66,111 @@ export function isEmojiNameValid(name) { ...@@ -67,49 +66,111 @@ export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0; return validEmojiNames.indexOf(name) >= 0;
} }
export function getValidEmojiUnicodeValues() {
return Object.values(emojiMap).map(({ e }) => e);
}
export function getValidEmojiDescriptions() {
return Object.values(emojiMap).map(({ d }) => d);
}
/** /**
* Search emoji by name or alias. Returns a normalized, deduplicated list of * Retrieves an emoji by name or alias.
* names.
* *
* Calling with an empty filter returns an empty array. * Note: `initEmojiMap` must have been called and completed before this method
* can safely be called.
* *
* @param {String} * @param {String} query The emoji name
* @returns {Array} * @param {Boolean} fallback If true, a fallback emoji will be returned if the
* named emoji does not exist. Defaults to false.
* @returns {Object} The matching emoji.
*/ */
export function queryEmojiNames(filter) { export function getEmoji(query, fallback = false) {
const matches = fuzzaldrinPlus.filter(validEmojiNames, filter); if (!emojiMap) {
return uniq(matches.map(name => normalizeEmojiName(name))); // eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('The emoji map is uninitialized or initialization has not completed');
}
const lowercaseQuery = query.toLowerCase();
const name = normalizeEmojiName(lowercaseQuery);
if (name in emojiMap) {
return emojiMap[name];
}
if (fallback) {
return emojiMap.grey_question;
}
return null;
} }
const searchMatchers = {
fuzzy: (value, query) => fuzzaldrinPlus.score(value, query) > 0, // Fuzzy matching compares using a fuzzy matching library
contains: (value, query) => value.indexOf(query.toLowerCase()) >= 0, // Contains matching compares by indexOf
exact: (value, query) => value === query.toLowerCase(), // Exact matching compares by equality
};
const searchPredicates = {
name: (matcher, query) => emoji => matcher(emoji.name, query), // Search by name
alias: (matcher, query) => emoji => emoji.aliases.some(v => matcher(v, query)), // Search by alias
description: (matcher, query) => emoji => matcher(emoji.d, query), // Search by description
unicode: (matcher, query) => emoji => emoji.e === query, // Search by unicode value (always exact)
};
/** /**
* Searches emoji by name, alias, description, and unicode value and returns an * Searches emoji by name, aliases, description, and unicode value and returns
* array of matches. * an array of matches.
*
* Behavior is undefined if `opts.fields` is empty or if `opts.match` is fuzzy
* and the query is empty.
* *
* Note: `initEmojiMap` must have been called and completed before this method * Note: `initEmojiMap` must have been called and completed before this method
* can safely be called. * can safely be called.
* *
* @param {String} query The search query * @param {String} query Search query.
* @returns {Object[]} A list of emoji that match the query * @param {Object} opts Search options (optional).
* @param {String[]} opts.fields Fields to search. Choices are 'name', 'alias',
* 'description', and 'unicode' (value). Default is all (four) fields.
* @param {String} opts.match Search method to use. Choices are 'exact',
* 'contains', or 'fuzzy'. All methods are case-insensitive. Exact matching (the
* default) compares by equality. Contains matching compares by indexOf. Fuzzy
* matching compares using a fuzzy matching library.
* @param {Boolean} opts.fallback If true, a fallback emoji will be returned if
* the result set is empty. Defaults to false.
* @returns {Object[]} A list of emoji that match the query.
*/ */
export function searchEmoji(query) { export function searchEmoji(query, opts) {
if (!emojiMap) if (!emojiMap) {
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('The emoji map is uninitialized or initialization has not completed'); throw new Error('The emoji map is uninitialized or initialization has not completed');
}
const {
fields = ['name', 'alias', 'description', 'unicode'],
match = 'exact',
fallback = false,
} = opts || {};
const matches = s => fuzzaldrinPlus.score(s, query) > 0; // optimization for an exact match in name and alias
if (match === 'exact' && new Set([...fields, 'name', 'alias']).size === 2) {
// Search emoji const emoji = getEmoji(query, fallback);
return Object.values(emojiMap).filter( return emoji ? [emoji] : [];
emoji => }
// by name
matches(emoji.name) || const matcher = searchMatchers[match] || searchMatchers.exact;
// by alias const predicates = fields.map(f => searchPredicates[f](matcher, query));
emoji.aliases.some(matches) ||
// by description const results = Object.values(emojiMap).filter(emoji =>
matches(emoji.d) || predicates.some(predicate => predicate(emoji)),
// by unicode value
query === emoji.e,
); );
// Fallback to question mark for unknown emojis
if (fallback && results.length === 0) {
return [emojiMap.grey_question];
}
return results;
} }
let emojiCategoryMap; let emojiCategoryMap;
...@@ -136,16 +197,10 @@ export function getEmojiCategoryMap() { ...@@ -136,16 +197,10 @@ export function getEmojiCategoryMap() {
} }
export function getEmojiInfo(query) { export function getEmojiInfo(query) {
let name = normalizeEmojiName(query); return searchEmoji(query, {
let emojiInfo = emojiMap[name]; fields: ['name', 'alias'],
fallback: true,
// Fallback to question mark for unknown emojis })[0];
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
return { ...emojiInfo, name };
} }
export function emojiFallbackImageSrc(inputName) { export function emojiFallbackImageSrc(inputName) {
......
import * as Sentry from '@sentry/browser';
import { escape } from 'lodash'; import { escape } from 'lodash';
import * as Sentry from '~/sentry/wrapper';
import { spriteIcon } from './lib/utils/common_utils'; import { spriteIcon } from './lib/utils/common_utils';
const FLASH_TYPES = { const FLASH_TYPES = {
......
...@@ -191,8 +191,7 @@ class GfmAutoComplete { ...@@ -191,8 +191,7 @@ class GfmAutoComplete {
} }
return tmpl; return tmpl;
}, },
// eslint-disable-next-line no-template-curly-in-string insertTpl: GfmAutoComplete.Emoji.insertTemplateFunction,
insertTpl: ':${name}:',
skipSpecialCharacterTest: true, skipSpecialCharacterTest: true,
data: GfmAutoComplete.defaultLoadingData, data: GfmAutoComplete.defaultLoadingData,
callbacks: { callbacks: {
...@@ -612,12 +611,7 @@ class GfmAutoComplete { ...@@ -612,12 +611,7 @@ class GfmAutoComplete {
} else if (this.cachedData[at]) { } else if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
Emoji.initEmojiMap() this.loadEmojiData($input, at).catch(() => {});
.then(() => {
this.loadData($input, at, Emoji.getValidEmojiNames());
GfmAutoComplete.glEmojiTag = Emoji.glEmojiTag;
})
.catch(() => {});
} else if (dataSource) { } else if (dataSource) {
AjaxCache.retrieve(dataSource, true) AjaxCache.retrieve(dataSource, true)
.then(data => { .then(data => {
...@@ -640,6 +634,18 @@ class GfmAutoComplete { ...@@ -640,6 +634,18 @@ class GfmAutoComplete {
return $input.trigger('keyup'); return $input.trigger('keyup');
} }
async loadEmojiData($input, at) {
await Emoji.initEmojiMap();
this.loadData($input, at, [
...Emoji.getValidEmojiNames(),
...Emoji.getValidEmojiDescriptions(),
...Emoji.getValidEmojiUnicodeValues(),
]);
GfmAutoComplete.glEmojiTag = Emoji.glEmojiTag;
}
clearCache() { clearCache() {
this.cachedData = {}; this.cachedData = {};
} }
...@@ -708,12 +714,16 @@ GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities']; ...@@ -708,12 +714,16 @@ GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities'];
// Emoji // Emoji
GfmAutoComplete.glEmojiTag = null; GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = { GfmAutoComplete.Emoji = {
insertTemplateFunction(value) {
const { name = value.name } = Emoji.searchEmoji(value.name, { match: 'contains' })[0] || {};
return `:${name}:`;
},
templateFunction(name) { templateFunction(name) {
// glEmojiTag helper is loaded on-demand in fetchData() // glEmojiTag helper is loaded on-demand in fetchData()
if (GfmAutoComplete.glEmojiTag) { if (!GfmAutoComplete.glEmojiTag) return `<li>${name}</li>`;
return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
} const emoji = Emoji.searchEmoji(name, { match: 'contains' })[0];
return `<li>${name}</li>`; return `<li>${name} ${GfmAutoComplete.glEmojiTag(emoji?.name || name)}</li>`;
}, },
}; };
// Team Members // Team Members
......
...@@ -7,7 +7,6 @@ import CommitMessageField from './message_field.vue'; ...@@ -7,7 +7,6 @@ import CommitMessageField from './message_field.vue';
import Actions from './actions.vue'; import Actions from './actions.vue';
import SuccessMessage from './success_message.vue'; import SuccessMessage from './success_message.vue';
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
import consts from '../../stores/modules/commit/constants';
import { createUnexpectedCommitError } from '../../lib/errors'; import { createUnexpectedCommitError } from '../../lib/errors';
export default { export default {
...@@ -45,12 +44,11 @@ export default { ...@@ -45,12 +44,11 @@ export default {
return this.currentActivityView === leftSidebarViews.commit.name; return this.currentActivityView === leftSidebarViews.commit.name;
}, },
commitErrorPrimaryAction() { commitErrorPrimaryAction() {
if (!this.lastCommitError?.canCreateBranch) { const { primaryAction } = this.lastCommitError || {};
return undefined;
}
return { return {
text: __('Create new branch'), button: primaryAction ? { text: primaryAction.text } : undefined,
callback: primaryAction?.callback?.bind(this, this.$store) || (() => {}),
}; };
}, },
}, },
...@@ -78,9 +76,6 @@ export default { ...@@ -78,9 +76,6 @@ export default {
commit() { commit() {
return this.commitChanges(); return this.commitChanges();
}, },
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit());
},
handleCompactState() { handleCompactState() {
if (this.lastCommitMsg) { if (this.lastCommitMsg) {
this.isCompact = false; this.isCompact = false;
...@@ -188,9 +183,9 @@ export default { ...@@ -188,9 +183,9 @@ export default {
ref="commitErrorModal" ref="commitErrorModal"
modal-id="ide-commit-error-modal" modal-id="ide-commit-error-modal"
:title="lastCommitError.title" :title="lastCommitError.title"
:action-primary="commitErrorPrimaryAction" :action-primary="commitErrorPrimaryAction.button"
:action-cancel="{ text: __('Cancel') }" :action-cancel="{ text: __('Cancel') }"
@ok="forceCreateNewBranch" @ok="commitErrorPrimaryAction.callback"
> >
<div v-safe-html="lastCommitError.messageHTML"></div> <div v-safe-html="lastCommitError.messageHTML"></div>
</gl-modal> </gl-modal>
......
import { escape } from 'lodash'; import { escape } from 'lodash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import consts from '../stores/modules/commit/constants';
const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/; const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/;
const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/; const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/;
const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/;
export const createUnexpectedCommitError = () => ({ const createNewBranchAndCommit = store =>
store
.dispatch('commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH)
.then(() => store.dispatch('commit/commitChanges'));
export const createUnexpectedCommitError = message => ({
title: __('Unexpected error'), title: __('Unexpected error'),
messageHTML: __('Could not commit. An unexpected error occurred.'), messageHTML: escape(message) || __('Could not commit. An unexpected error occurred.'),
canCreateBranch: false,
}); });
export const createCodeownersCommitError = message => ({ export const createCodeownersCommitError = message => ({
title: __('CODEOWNERS rule violation'), title: __('CODEOWNERS rule violation'),
messageHTML: escape(message), messageHTML: escape(message),
canCreateBranch: true, primaryAction: {
text: __('Create new branch'),
callback: createNewBranchAndCommit,
},
}); });
export const createBranchChangedCommitError = message => ({ export const createBranchChangedCommitError = message => ({
title: __('Branch changed'), title: __('Branch changed'),
messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`, messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`,
canCreateBranch: true, primaryAction: {
text: __('Create new branch'),
callback: createNewBranchAndCommit,
},
});
export const branchAlreadyExistsCommitError = message => ({
title: __('Branch already exists'),
messageHTML: `${escape(message)}<br/><br/>${__(
'Would you like to try auto-generating a branch name?',
)}`,
primaryAction: {
text: __('Create new branch'),
callback: store =>
store.dispatch('commit/addSuffixToBranchName').then(() => createNewBranchAndCommit(store)),
},
}); });
export const parseCommitError = e => { export const parseCommitError = e => {
...@@ -33,7 +57,9 @@ export const parseCommitError = e => { ...@@ -33,7 +57,9 @@ export const parseCommitError = e => {
return createCodeownersCommitError(message); return createCodeownersCommitError(message);
} else if (BRANCH_CHANGED_REGEX.test(message)) { } else if (BRANCH_CHANGED_REGEX.test(message)) {
return createBranchChangedCommitError(message); return createBranchChangedCommitError(message);
} else if (BRANCH_ALREADY_EXISTS.test(message)) {
return branchAlreadyExistsCommitError(message);
} }
return createUnexpectedCommitError(); return createUnexpectedCommitError(message);
}; };
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
PERMISSION_CREATE_MR, PERMISSION_CREATE_MR,
PERMISSION_PUSH_CODE, PERMISSION_PUSH_CODE,
} from '../constants'; } from '../constants';
import { addNumericSuffix } from '~/ide/utils';
import Api from '~/api'; import Api from '~/api';
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state => state.openFiles.find(file => file.active) || null;
...@@ -167,10 +168,7 @@ export const getAvailableFileName = (state, getters) => path => { ...@@ -167,10 +168,7 @@ export const getAvailableFileName = (state, getters) => path => {
let newPath = path; let newPath = path;
while (getters.entryExists(newPath)) { while (getters.entryExists(newPath)) {
newPath = newPath.replace( newPath = addNumericSuffix(newPath);
/([ _-]?)(\d*)(\..+?$|$)/,
(_, before, number, after) => `${before || '_'}${Number(number) + 1}${after}`,
);
} }
return newPath; return newPath;
......
...@@ -8,6 +8,7 @@ import consts from './constants'; ...@@ -8,6 +8,7 @@ import consts from './constants';
import { leftSidebarViews } from '../../../constants'; import { leftSidebarViews } from '../../../constants';
import eventHub from '../../../eventhub'; import eventHub from '../../../eventhub';
import { parseCommitError } from '../../../lib/errors'; import { parseCommitError } from '../../../lib/errors';
import { addNumericSuffix } from '~/ide/utils';
export const updateCommitMessage = ({ commit }, message) => { export const updateCommitMessage = ({ commit }, message) => {
commit(types.UPDATE_COMMIT_MESSAGE, message); commit(types.UPDATE_COMMIT_MESSAGE, message);
...@@ -17,11 +18,8 @@ export const discardDraft = ({ commit }) => { ...@@ -17,11 +18,8 @@ export const discardDraft = ({ commit }) => {
commit(types.UPDATE_COMMIT_MESSAGE, ''); commit(types.UPDATE_COMMIT_MESSAGE, '');
}; };
export const updateCommitAction = ({ commit, getters }, commitAction) => { export const updateCommitAction = ({ commit }, commitAction) => {
commit(types.UPDATE_COMMIT_ACTION, { commit(types.UPDATE_COMMIT_ACTION, { commitAction });
commitAction,
});
commit(types.TOGGLE_SHOULD_CREATE_MR, !getters.shouldHideNewMrOption);
}; };
export const toggleShouldCreateMR = ({ commit }) => { export const toggleShouldCreateMR = ({ commit }) => {
...@@ -32,6 +30,12 @@ export const updateBranchName = ({ commit }, branchName) => { ...@@ -32,6 +30,12 @@ export const updateBranchName = ({ commit }, branchName) => {
commit(types.UPDATE_NEW_BRANCH_NAME, branchName); commit(types.UPDATE_NEW_BRANCH_NAME, branchName);
}; };
export const addSuffixToBranchName = ({ commit, state }) => {
const newBranchName = addNumericSuffix(state.newBranchName, true);
commit(types.UPDATE_NEW_BRANCH_NAME, newBranchName);
};
export const setLastCommitMessage = ({ commit, rootGetters }, data) => { export const setLastCommitMessage = ({ commit, rootGetters }, data) => {
const { currentProject } = rootGetters; const { currentProject } = rootGetters;
const commitStats = data.stats const commitStats = data.stats
...@@ -107,7 +111,7 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter ...@@ -107,7 +111,7 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => { export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
// Pull commit options out because they could change // Pull commit options out because they could change
// During some of the pre and post commit processing // During some of the pre and post commit processing
const { shouldCreateMR, isCreatingNewBranch, branchName } = getters; const { shouldCreateMR, shouldHideNewMrOption, isCreatingNewBranch, branchName } = getters;
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const stageFilesPromise = rootState.stagedFiles.length const stageFilesPromise = rootState.stagedFiles.length
? Promise.resolve() ? Promise.resolve()
...@@ -167,7 +171,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -167,7 +171,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000); }, 5000);
if (shouldCreateMR) { if (shouldCreateMR && !shouldHideNewMrOption) {
const { currentProject } = rootGetters; const { currentProject } = rootGetters;
const targetBranch = isCreatingNewBranch const targetBranch = isCreatingNewBranch
? rootState.currentBranchId ? rootState.currentBranchId
......
...@@ -10,9 +10,7 @@ export default { ...@@ -10,9 +10,7 @@ export default {
Object.assign(state, { commitAction }); Object.assign(state, { commitAction });
}, },
[types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) { [types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) {
Object.assign(state, { Object.assign(state, { newBranchName });
newBranchName,
});
}, },
[types.UPDATE_LOADING](state, submitCommitLoading) { [types.UPDATE_LOADING](state, submitCommitLoading) {
Object.assign(state, { Object.assign(state, {
......
...@@ -139,6 +139,34 @@ export function getFileEOL(content = '') { ...@@ -139,6 +139,34 @@ export function getFileEOL(content = '') {
return content.includes('\r\n') ? 'CRLF' : 'LF'; return content.includes('\r\n') ? 'CRLF' : 'LF';
} }
/**
* Adds or increments the numeric suffix to a filename/branch name.
* Retains underscore or dash before the numeric suffix if it already exists.
*
* Examples:
* hello -> hello-1
* hello-2425 -> hello-2425
* hello.md -> hello-1.md
* hello_2.md -> hello_3.md
* hello_ -> hello_1
* master-patch-22432 -> master-patch-22433
* patch_332 -> patch_333
*
* @param {string} filename File name or branch name
* @param {number} [randomize] Should randomize the numeric suffix instead of auto-incrementing?
*/
export function addNumericSuffix(filename, randomize = false) {
return filename.replace(/([ _-]?)(\d*)(\..+?$|$)/, (_, before, number, after) => {
const n = randomize
? Math.random()
.toString()
.substring(2, 7)
.slice(-5)
: Number(number) + 1;
return `${before || '-'}${n}${after}`;
});
}
export const measurePerformance = ( export const measurePerformance = (
mark, mark,
measureName, measureName,
......
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
GlEmptyState, GlEmptyState,
} from '@gitlab/ui'; } from '@gitlab/ui';
import Api from '~/api'; import Api from '~/api';
import Tracking from '~/tracking';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
...@@ -41,6 +42,7 @@ import { ...@@ -41,6 +42,7 @@ import {
TH_SEVERITY_TEST_ID, TH_SEVERITY_TEST_ID,
TH_PUBLISHED_TEST_ID, TH_PUBLISHED_TEST_ID,
INCIDENT_DETAILS_PATH, INCIDENT_DETAILS_PATH,
trackIncidentCreateNewOptions,
} from '../constants'; } from '../constants';
const tdClass = const tdClass =
...@@ -58,6 +60,7 @@ const initialPaginationState = { ...@@ -58,6 +60,7 @@ const initialPaginationState = {
}; };
export default { export default {
trackIncidentCreateNewOptions,
i18n: I18N, i18n: I18N,
statusTabs: INCIDENT_STATUS_TABS, statusTabs: INCIDENT_STATUS_TABS,
fields: [ fields: [
...@@ -335,6 +338,11 @@ export default { ...@@ -335,6 +338,11 @@ export default {
navigateToIncidentDetails({ iid }) { navigateToIncidentDetails({ iid }) {
return visitUrl(joinPaths(this.issuePath, INCIDENT_DETAILS_PATH, iid)); return visitUrl(joinPaths(this.issuePath, INCIDENT_DETAILS_PATH, iid));
}, },
navigateToCreateNewIncident() {
const { category, action } = this.$options.trackIncidentCreateNewOptions;
Tracking.event(category, action);
this.redirecting = true;
},
handlePageChange(page) { handlePageChange(page) {
const { startCursor, endCursor } = this.incidents.pageInfo; const { startCursor, endCursor } = this.incidents.pageInfo;
...@@ -458,7 +466,7 @@ export default { ...@@ -458,7 +466,7 @@ export default {
category="primary" category="primary"
variant="success" variant="success"
:href="newIncidentPath" :href="newIncidentPath"
@click="redirecting = true" @click="navigateToCreateNewIncident"
> >
{{ $options.i18n.createIncidentBtnLabel }} {{ $options.i18n.createIncidentBtnLabel }}
</gl-button> </gl-button>
......
/* eslint-disable @gitlab/require-i18n-strings */
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
export const I18N = { export const I18N = {
...@@ -34,6 +35,14 @@ export const INCIDENT_STATUS_TABS = [ ...@@ -34,6 +35,14 @@ export const INCIDENT_STATUS_TABS = [
}, },
]; ];
/**
* Tracks snowplow event when user clicks create new incident
*/
export const trackIncidentCreateNewOptions = {
category: 'Incident Management',
action: 'create_incident_button_clicks',
};
export const DEFAULT_PAGE_SIZE = 20; export const DEFAULT_PAGE_SIZE = 20;
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' }; export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' }; export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };
......
...@@ -11,6 +11,8 @@ export default { ...@@ -11,6 +11,8 @@ export default {
GlTab, GlTab,
AlertsSettingsForm, AlertsSettingsForm,
PagerDutySettingsForm, PagerDutySettingsForm,
ServiceLevelAgreementForm: () =>
import('ee_component/incidents_settings/components/service_level_agreement_form.vue'),
}, },
tabs: INTEGRATION_TABS_CONFIG, tabs: INTEGRATION_TABS_CONFIG,
i18n: I18N_INTEGRATION_TABS, i18n: I18N_INTEGRATION_TABS,
...@@ -45,6 +47,7 @@ export default { ...@@ -45,6 +47,7 @@ export default {
> >
<component :is="tab.component" class="gl-pt-3" :data-testid="`${tab.component}-tab`" /> <component :is="tab.component" class="gl-pt-3" :data-testid="`${tab.component}-tab`" />
</gl-tab> </gl-tab>
<service-level-agreement-form />
</gl-tabs> </gl-tabs>
</div> </div>
</section> </section>
......
...@@ -21,6 +21,9 @@ export default () => { ...@@ -21,6 +21,9 @@ export default () => {
pagerdutyWebhookUrl, pagerdutyWebhookUrl,
pagerdutyResetKeyPath, pagerdutyResetKeyPath,
autoCloseIncident, autoCloseIncident,
slaActive,
slaMinutes,
slaFeatureAvailable,
}, },
} = el; } = el;
...@@ -40,6 +43,11 @@ export default () => { ...@@ -40,6 +43,11 @@ export default () => {
active: parseBoolean(pagerdutyActive), active: parseBoolean(pagerdutyActive),
webhookUrl: pagerdutyWebhookUrl, webhookUrl: pagerdutyWebhookUrl,
}, },
serviceLevelAgreementSettings: {
active: parseBoolean(slaActive),
minutes: slaMinutes,
available: parseBoolean(slaFeatureAvailable),
},
}, },
render(createElement) { render(createElement) {
return createElement(SettingsTabs); return createElement(SettingsTabs);
......
<script>
import $ from 'jquery';
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import '~/behaviors/markdown/render_gfm';
export default {
directives: {
SafeHtml,
},
props: {
issuable: {
type: Object,
required: true,
},
},
mounted() {
this.renderGFM();
},
methods: {
renderGFM() {
$(this.$refs.gfmContainer).renderGFM();
},
},
};
</script>
<template>
<div class="description">
<div ref="gfmContainer" v-safe-html="issuable.descriptionHtml" class="md"></div>
</div>
</template>
<script>
import $ from 'jquery';
import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import Autosave from '~/autosave';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import eventHub from '../event_hub';
export default {
components: {
GlForm,
GlFormGroup,
GlFormInput,
MarkdownField,
},
props: {
issuable: {
type: Object,
required: true,
},
enableAutocomplete: {
type: Boolean,
required: true,
},
descriptionPreviewPath: {
type: String,
required: true,
},
descriptionHelpPath: {
type: String,
required: true,
},
},
data() {
const { title, description } = this.issuable;
return {
title,
description,
};
},
created() {
eventHub.$on('update.issuable', this.resetAutosave);
eventHub.$on('close.form', this.resetAutosave);
},
mounted() {
this.initAutosave();
},
beforeDestroy() {
eventHub.$off('update.issuable', this.resetAutosave);
eventHub.$off('close.form', this.resetAutosave);
},
methods: {
initAutosave() {
const { titleInput, descriptionInput } = this.$refs;
if (!titleInput || !descriptionInput) return;
this.autosaveTitle = new Autosave($(titleInput.$el), [
document.location.pathname,
document.location.search,
'title',
]);
this.autosaveDescription = new Autosave($(descriptionInput.$el), [
document.location.pathname,
document.location.search,
'description',
]);
},
resetAutosave() {
this.autosaveTitle.reset();
this.autosaveDescription.reset();
},
},
};
</script>
<template>
<gl-form>
<gl-form-group
data-testid="title"
:label="__('Title')"
:label-sr-only="true"
label-for="issuable-title"
class="col-12"
>
<gl-form-input
id="issuable-title"
ref="titleInput"
v-model.trim="title"
:placeholder="__('Title')"
:aria-label="__('Title')"
:autofocus="true"
class="qa-title-input"
/>
</gl-form-group>
<gl-form-group
data-testid="description"
:label="__('Description')"
:label-sr-only="true"
label-for="issuable-description"
class="col-12 common-note-form"
>
<markdown-field
:markdown-preview-path="descriptionPreviewPath"
:markdown-docs-path="descriptionHelpPath"
:enable-autocomplete="enableAutocomplete"
:textarea-value="description"
>
<template #textarea>
<textarea
id="issuable-description"
ref="descriptionInput"
v-model="description"
:data-supports-quick-actions="enableAutocomplete"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
class="note-textarea js-gfm-input js-autosize markdown-area
qa-description-textarea"
dir="auto"
></textarea>
</template>
</markdown-field>
</gl-form-group>
<div data-testid="actions" class="col-12 gl-mt-3 gl-mb-3 clearfix">
<slot
name="edit-form-actions"
:issuable-title="title"
:issuable-description="description"
></slot>
</div>
</gl-form>
</template>
<script>
import {
GlIcon,
GlButton,
GlIntersectionObserver,
GlTooltipDirective,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
export default {
components: {
GlIcon,
GlButton,
GlIntersectionObserver,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
props: {
issuable: {
type: Object,
required: true,
},
statusBadgeClass: {
type: String,
required: true,
},
statusIcon: {
type: String,
required: true,
},
enableEdit: {
type: Boolean,
required: true,
},
},
data() {
return {
stickyTitleVisible: false,
};
},
methods: {
handleTitleAppear() {
this.stickyTitleVisible = false;
},
handleTitleDisappear() {
this.stickyTitleVisible = true;
},
},
};
</script>
<template>
<div>
<div class="title-container">
<h2 v-safe-html="issuable.titleHtml" class="title qa-title" dir="auto"></h2>
<gl-button
v-if="enableEdit"
v-gl-tooltip.bottom
:title="__('Edit title and description')"
icon="pencil"
class="btn-edit js-issuable-edit qa-edit-button"
@click="$emit('edit-issuable', $event)"
/>
</div>
<gl-intersection-observer @appear="handleTitleAppear" @disappear="handleTitleDisappear">
<transition name="issuable-header-slide">
<div
v-if="stickyTitleVisible"
class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
data-testid="header"
>
<div
class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
>
<p
data-testid="status"
class="issuable-status-box status-box gl-my-0"
:class="statusBadgeClass"
>
<gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" />
<span class="gl-display-none d-sm-block"><slot name="status-badge"></slot></span>
</p>
<p
class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0"
:title="issuable.title"
>
{{ issuable.title }}
</p>
</div>
</div>
</transition>
</gl-intersection-observer>
</div>
</template>
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
import * as Sentry from '@sentry/browser'; import * as Sentry from '~/sentry/wrapper';
import { sanitize } from '~/lib/dompurify'; import { sanitize } from '~/lib/dompurify';
// We currently load + parse the data from the issue app and related merge request // We currently load + parse the data from the issue app and related merge request
......
export const BYTES_IN_KIB = 1024; export const BYTES_IN_KIB = 1024;
export const BYTES_IN_KB = 1000;
export const HIDDEN_CLASS = 'hidden'; export const HIDDEN_CLASS = 'hidden';
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80; export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12; export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
......
import { BYTES_IN_KIB } from './constants'; import { BYTES_IN_KIB, BYTES_IN_KB } from './constants';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
/** /**
...@@ -34,6 +34,18 @@ export function formatRelevantDigits(number) { ...@@ -34,6 +34,18 @@ export function formatRelevantDigits(number) {
return formattedNumber; return formattedNumber;
} }
/**
* Utility function that calculates KB of the given bytes.
* Note: This method calculates KiloBytes as opposed to
* Kibibytes. For Kibibytes, bytesToKiB should be used.
*
* @param {Number} number bytes
* @return {Number} KiB
*/
export function bytesToKB(number) {
return number / BYTES_IN_KB;
}
/** /**
* Utility function that calculates KiB of the given bytes. * Utility function that calculates KiB of the given bytes.
* *
......
import * as Sentry from '@sentry/browser'; import * as Sentry from '~/sentry/wrapper';
import * as types from './mutation_types'; import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
......
...@@ -14,12 +14,12 @@ export default { ...@@ -14,12 +14,12 @@ export default {
}, },
computed: { computed: {
...mapState(['composerHelpPath']), ...mapState(['composerHelpPath']),
...mapGetters(['composerRegistryInclude', 'composerPackageInclude']), ...mapGetters(['composerRegistryInclude', 'composerPackageInclude', 'groupExists']),
}, },
i18n: { i18n: {
registryInclude: s__('PackageRegistry|composer.json registry include'), registryInclude: s__('PackageRegistry|Add composer registry'),
copyRegistryInclude: s__('PackageRegistry|Copy registry include'), copyRegistryInclude: s__('PackageRegistry|Copy registry include'),
packageInclude: s__('PackageRegistry|composer.json require package include'), packageInclude: s__('PackageRegistry|Install package version'),
copyPackageInclude: s__('PackageRegistry|Copy require package include'), copyPackageInclude: s__('PackageRegistry|Copy require package include'),
infoLine: s__( infoLine: s__(
'PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}', 'PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}',
...@@ -32,31 +32,33 @@ export default { ...@@ -32,31 +32,33 @@ export default {
<template> <template>
<div> <div>
<h3 class="gl-font-lg">{{ __('Installation') }}</h3> <div v-if="groupExists">
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
<code-instruction <code-instruction
:label="$options.i18n.registryInclude" :label="$options.i18n.registryInclude"
:instruction="composerRegistryInclude" :instruction="composerRegistryInclude"
:copy-text="$options.i18n.copyRegistryInclude" :copy-text="$options.i18n.copyRegistryInclude"
:tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND" :tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND"
:tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
data-testid="registry-include" data-testid="registry-include"
/> />
<code-instruction <code-instruction
:label="$options.i18n.packageInclude" :label="$options.i18n.packageInclude"
:instruction="composerPackageInclude" :instruction="composerPackageInclude"
:copy-text="$options.i18n.copyPackageInclude" :copy-text="$options.i18n.copyPackageInclude"
:tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND" :tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND"
:tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
data-testid="package-include" data-testid="package-include"
/> />
<span data-testid="help-text"> <span data-testid="help-text">
<gl-sprintf :message="$options.i18n.infoLine"> <gl-sprintf :message="$options.i18n.infoLine">
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link> <gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</span> </span>
</div>
</div> </div>
</template> </template>
...@@ -102,11 +102,12 @@ repository = ${pypiSetupPath} ...@@ -102,11 +102,12 @@ repository = ${pypiSetupPath}
username = __token__ username = __token__
password = <your personal access token>`; password = <your personal access token>`;
export const composerRegistryInclude = ({ composerPath }) => { export const composerRegistryInclude = ({ composerPath, composerConfigRepositoryName }) =>
const base = { type: 'composer', url: composerPath }; // eslint-disable-next-line @gitlab/require-i18n-strings
return JSON.stringify(base); `composer config repositories.${composerConfigRepositoryName} '{"type": "composer", "url": "${composerPath}"}'`;
};
export const composerPackageInclude = ({ packageEntity }) => { export const composerPackageInclude = ({ packageEntity }) =>
const base = { [packageEntity.name]: packageEntity.version }; // eslint-disable-next-line @gitlab/require-i18n-strings
return JSON.stringify(base); `composer req ${[packageEntity.name]}:${packageEntity.version}`;
};
export const groupExists = ({ groupListUrl }) => groupListUrl.length > 0;
...@@ -70,14 +70,18 @@ export default { ...@@ -70,14 +70,18 @@ export default {
</script> </script>
<template> <template>
<div :data-for="name" class="project-feature-controls"> <div
:data-for="name"
class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0"
>
<input v-if="name" :name="name" :value="value" type="hidden" /> <input v-if="name" :name="name" :value="value" type="hidden" />
<project-feature-toggle <project-feature-toggle
class="gl-flex-grow-0 gl-mr-3"
:value="featureEnabled" :value="featureEnabled"
:disabled-input="disabledInput" :disabled-input="disabledInput"
@change="toggleFeature" @change="toggleFeature"
/> />
<div class="select-wrapper"> <div class="select-wrapper gl-flex-fill-1">
<select <select
:disabled="displaySelectInput" :disabled="displaySelectInput"
class="form-control project-repo-select select-control" class="form-control project-repo-select select-control"
......
...@@ -292,14 +292,16 @@ export default { ...@@ -292,14 +292,16 @@ export default {
<template> <template>
<div> <div>
<div class="project-visibility-setting"> <div
class="project-visibility-setting gl-border-1 gl-border-solid gl-border-gray-100 gl-py-3 gl-px-7 gl-sm-pr-5 gl-sm-pl-5"
>
<project-setting-row <project-setting-row
ref="project-visibility-settings" ref="project-visibility-settings"
:help-path="visibilityHelpPath" :help-path="visibilityHelpPath"
:label="s__('ProjectSettings|Project visibility')" :label="s__('ProjectSettings|Project visibility')"
> >
<div class="project-feature-controls"> <div class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0">
<div class="select-wrapper"> <div class="select-wrapper gl-flex-fill-1">
<select <select
v-model="visibilityLevel" v-model="visibilityLevel"
:disabled="!canChangeVisibilityLevel" :disabled="!canChangeVisibilityLevel"
...@@ -327,7 +329,7 @@ export default { ...@@ -327,7 +329,7 @@ export default {
</div> </div>
</div> </div>
<span class="form-text text-muted">{{ visibilityLevelDescription }}</span> <span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
<label v-if="visibilityLevel !== visibilityOptions.PRIVATE" class="request-access"> <label v-if="visibilityLevel !== visibilityOptions.PRIVATE" class="gl-line-height-28">
<input <input
:value="requestAccessEnabled" :value="requestAccessEnabled"
type="hidden" type="hidden"
...@@ -338,7 +340,10 @@ export default { ...@@ -338,7 +340,10 @@ export default {
</label> </label>
</project-setting-row> </project-setting-row>
</div> </div>
<div :class="{ 'highlight-changes': highlightChangesClass }" class="project-feature-settings"> <div
:class="{ 'highlight-changes': highlightChangesClass }"
class="gl-border-1 gl-border-solid gl-border-t-none gl-border-gray-100 gl-mb-5 gl-py-3 gl-px-7 gl-sm-pr-5 gl-sm-pl-5 gl-bg-gray-10"
>
<project-setting-row <project-setting-row
ref="issues-settings" ref="issues-settings"
:label="s__('ProjectSettings|Issues')" :label="s__('ProjectSettings|Issues')"
...@@ -361,7 +366,7 @@ export default { ...@@ -361,7 +366,7 @@ export default {
name="project[project_feature_attributes][repository_access_level]" name="project[project_feature_attributes][repository_access_level]"
/> />
</project-setting-row> </project-setting-row>
<div class="project-feature-setting-group"> <div class="project-feature-setting-group gl-pl-7 gl-sm-pl-5">
<project-setting-row <project-setting-row
ref="merge-request-settings" ref="merge-request-settings"
:label="s__('ProjectSettings|Merge requests')" :label="s__('ProjectSettings|Merge requests')"
...@@ -516,8 +521,8 @@ export default { ...@@ -516,8 +521,8 @@ export default {
) )
" "
> >
<div class="project-feature-controls"> <div class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0">
<div class="select-wrapper"> <div class="select-wrapper gl-flex-fill-1">
<select <select
v-model="metricsDashboardAccessLevel" v-model="metricsDashboardAccessLevel"
:disabled="metricsOptionsDropdownEnabled" :disabled="metricsOptionsDropdownEnabled"
......
...@@ -126,7 +126,7 @@ export default { ...@@ -126,7 +126,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="ci-job-component"> <div class="ci-job-component" data-qa-selector="job_item_container">
<gl-link <gl-link
v-if="status.has_details" v-if="status.has_details"
v-gl-tooltip="{ boundary, placement: 'bottom' }" v-gl-tooltip="{ boundary, placement: 'bottom' }"
...@@ -156,6 +156,7 @@ export default { ...@@ -156,6 +156,7 @@ export default {
:tooltip-text="status.action.title" :tooltip-text="status.action.title"
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
data-qa-selector="action_button"
@pipelineActionRequestComplete="pipelineActionRequestComplete" @pipelineActionRequestComplete="pipelineActionRequestComplete"
/> />
</div> </div>
......
import * as Sentry from '@sentry/browser'; import * as Sentry from '~/sentry/wrapper';
import * as types from './mutation_types'; import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
......
<script> <script>
import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
name: 'ServiceDeskSetting', name: 'ServiceDeskSetting',
directives: {
tooltip,
},
components: { components: {
ClipboardButton, ClipboardButton,
GlButton, GlButton,
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* eslint-disable vue/no-v-html */ /* eslint-disable vue/no-v-html */
import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui'; import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png'; import defaultAvatarUrl from 'images/no_avatar.png';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
...@@ -9,7 +10,6 @@ import CiIcon from '../../vue_shared/components/ci_icon.vue'; ...@@ -9,7 +10,6 @@ import CiIcon from '../../vue_shared/components/ci_icon.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import getRefMixin from '../mixins/get_ref'; import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql'; import projectPathQuery from '../queries/project_path.query.graphql';
import pathLastCommitQuery from '../queries/path_last_commit.query.graphql';
export default { export default {
components: { components: {
......
import Vue from 'vue'; import Vue from 'vue';
import PathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { escapeFileUrl, joinPaths, webIDEUrl } from '../lib/utils/url_utility'; import { escapeFileUrl, joinPaths, webIDEUrl } from '../lib/utils/url_utility';
import createRouter from './router'; import createRouter from './router';
import App from './components/app.vue'; import App from './components/app.vue';
...@@ -18,6 +19,10 @@ export default function setupVueRepositoryList() { ...@@ -18,6 +19,10 @@ export default function setupVueRepositoryList() {
const { dataset } = el; const { dataset } = el;
const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset; const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
const router = createRouter(projectPath, escapedRef); const router = createRouter(projectPath, escapedRef);
const pathRegex = /-\/tree\/[^/]+\/(.+$)/;
const matches = window.location.href.match(pathRegex);
const currentRoutePath = matches ? matches[1] : '';
apolloProvider.clients.defaultClient.cache.writeData({ apolloProvider.clients.defaultClient.cache.writeData({
data: { data: {
...@@ -29,6 +34,43 @@ export default function setupVueRepositoryList() { ...@@ -29,6 +34,43 @@ export default function setupVueRepositoryList() {
}, },
}); });
const initLastCommitApp = () =>
new Vue({
el: document.getElementById('js-last-commit'),
router,
apolloProvider,
render(h) {
return h(LastCommit, {
props: {
currentPath: this.$route.params.path,
},
});
},
});
if (window.gl.startup_graphql_calls) {
const query = window.gl.startup_graphql_calls.find(
call => call.operationName === 'pathLastCommit',
);
query.fetchCall
.then(res => res.json())
.then(res => {
apolloProvider.clients.defaultClient.writeQuery({
query: PathLastCommitQuery,
data: res.data,
variables: {
projectPath,
ref,
path: currentRoutePath,
},
});
})
.catch(() => {})
.finally(() => initLastCommitApp());
} else {
initLastCommitApp();
}
router.afterEach(({ params: { path } }) => { router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName); setTitle(path, ref, fullName);
}); });
...@@ -77,20 +119,6 @@ export default function setupVueRepositoryList() { ...@@ -77,20 +119,6 @@ export default function setupVueRepositoryList() {
}); });
} }
// eslint-disable-next-line no-new
new Vue({
el: document.getElementById('js-last-commit'),
router,
apolloProvider,
render(h) {
return h(LastCommit, {
props: {
currentPath: this.$route.params.path,
},
});
},
});
const treeHistoryLinkEl = document.getElementById('js-tree-history-link'); const treeHistoryLinkEl = document.getElementById('js-tree-history-link');
const { historyLink } = treeHistoryLinkEl.dataset; const { historyLink } = treeHistoryLinkEl.dataset;
......
import * as Sentry from '@sentry/browser';
import $ from 'jquery'; import $ from 'jquery';
import * as Sentry from '~/sentry/wrapper';
import { __ } from '~/locale'; import { __ } from '~/locale';
const IGNORE_ERRORS = [ const IGNORE_ERRORS = [
......
// Temporarily commented out to investigate performance: https://gitlab.com/gitlab-org/gitlab/-/issues/251179
// export * from '@sentry/browser';
export function init(...args) {
return args;
}
export function setUser(...args) {
return args;
}
export function captureException(...args) {
return args;
}
export function captureMessage(...args) {
return args;
}
export function withScope(fn) {
fn({
setTag(...args) {
return args;
},
});
}
<script> <script>
import { GlIcon } from '@gitlab/ui'; import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'TimeTrackingCollapsedState', name: 'TimeTrackingCollapsedState',
...@@ -9,7 +8,7 @@ export default { ...@@ -9,7 +8,7 @@ export default {
GlIcon, GlIcon,
}, },
directives: { directives: {
tooltip, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
showComparisonState: { showComparisonState: {
...@@ -97,14 +96,7 @@ export default { ...@@ -97,14 +96,7 @@ export default {
</script> </script>
<template> <template>
<div <div v-gl-tooltip:body.viewport.left :title="tooltipText" class="sidebar-collapsed-icon">
v-tooltip
:title="tooltipText"
class="sidebar-collapsed-icon"
data-container="body"
data-placement="left"
data-boundary="viewport"
>
<gl-icon name="timer" /> <gl-icon name="timer" />
<div class="time-tracking-collapsed-summary"> <div class="time-tracking-collapsed-summary">
<div :class="divClass"> <div :class="divClass">
......
...@@ -4,6 +4,7 @@ import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_ ...@@ -4,6 +4,7 @@ import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service'; import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue'; import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps'; import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import { GlSafeHtmlDirective } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale'; import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project'; import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval'; import SmartInterval from '~/smart_interval';
...@@ -52,6 +53,9 @@ export default { ...@@ -52,6 +53,9 @@ export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25 // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
name: 'MRWidget', name: 'MRWidget',
directives: {
SafeHtml: GlSafeHtmlDirective,
},
components: { components: {
Loading, Loading,
'mr-widget-header': WidgetHeader, 'mr-widget-header': WidgetHeader,
...@@ -510,7 +514,7 @@ export default { ...@@ -510,7 +514,7 @@ export default {
</mr-widget-alert-message> </mr-widget-alert-message>
<mr-widget-alert-message v-if="mr.mergeError" type="danger"> <mr-widget-alert-message v-if="mr.mergeError" type="danger">
{{ mergeError }} <span v-safe-html="mergeError"></span>
</mr-widget-alert-message> </mr-widget-alert-message>
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" /> <source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
......
...@@ -7,9 +7,11 @@ import { ...@@ -7,9 +7,11 @@ import {
convertToSentenceCase, convertToSentenceCase,
splitCamelCase, splitCamelCase,
} from '~/lib/utils/text_utility'; } from '~/lib/utils/text_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!'; const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!';
const tdClass = 'gl-border-gray-100! gl-p-5!'; const tdClass = 'gl-border-gray-100! gl-p-5!';
const allowedFields = [ const allowedFields = [
'iid', 'iid',
'title', 'title',
...@@ -22,17 +24,15 @@ const allowedFields = [ ...@@ -22,17 +24,15 @@ const allowedFields = [
'description', 'description',
'endedAt', 'endedAt',
'details', 'details',
'environment',
'hosts', 'hosts',
]; ];
const isAllowed = fieldName => allowedFields.includes(fieldName);
export default { export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
GlTable, GlTable,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
alert: { alert: {
type: Object, type: Object,
...@@ -60,14 +60,23 @@ export default { ...@@ -60,14 +60,23 @@ export default {
}, },
], ],
computed: { computed: {
flaggedAllowedFields() {
return this.shouldDisplayEnvironment ? [...allowedFields, 'environment'] : allowedFields;
},
items() { items() {
if (!this.alert) { if (!this.alert) {
return []; return [];
} }
return reduce( return reduce(
this.alert, this.alert,
(allowedItems, value, fieldName) => { (allowedItems, fieldValue, fieldName) => {
if (isAllowed(fieldName)) { if (this.isAllowed(fieldName)) {
let value;
if (fieldName === 'environment') {
value = fieldValue?.name;
} else {
value = fieldValue;
}
return [...allowedItems, { fieldName, value }]; return [...allowedItems, { fieldName, value }];
} }
return allowedItems; return allowedItems;
...@@ -75,6 +84,14 @@ export default { ...@@ -75,6 +84,14 @@ export default {
[], [],
); );
}, },
shouldDisplayEnvironment() {
return this.glFeatures.exposeEnvironmentPathInAlertDetails;
},
},
methods: {
isAllowed(fieldName) {
return this.flaggedAllowedFields.includes(fieldName);
},
}, },
}; };
</script> </script>
......
...@@ -8,11 +8,13 @@ import { ...@@ -8,11 +8,13 @@ import {
import { generateBadges } from 'ee_else_ce/vue_shared/components/members/utils'; import { generateBadges } from 'ee_else_ce/vue_shared/components/members/utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { AVATAR_SIZE } from '../constants'; import { AVATAR_SIZE } from '../constants';
import { glEmojiTag } from '~/emoji';
export default { export default {
name: 'UserAvatar', name: 'UserAvatar',
avatarSize: AVATAR_SIZE, avatarSize: AVATAR_SIZE,
orphanedUserLabel: __('Orphaned member'), orphanedUserLabel: __('Orphaned member'),
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
components: { components: {
GlAvatarLink, GlAvatarLink,
GlAvatarLabeled, GlAvatarLabeled,
...@@ -38,6 +40,12 @@ export default { ...@@ -38,6 +40,12 @@ export default {
badges() { badges() {
return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show); return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show);
}, },
statusEmoji() {
return this.user?.status?.emoji;
},
},
methods: {
glEmojiTag,
}, },
}; };
</script> </script>
...@@ -60,6 +68,9 @@ export default { ...@@ -60,6 +68,9 @@ export default {
:entity-id="user.id" :entity-id="user.id"
> >
<template #meta> <template #meta>
<div v-if="statusEmoji" class="gl-p-1">
<span v-safe-html:[$options.safeHtmlConfig]="glEmojiTag(statusEmoji)"></span>
</div>
<div v-for="badge in badges" :key="badge.text" class="gl-p-1"> <div v-for="badge in badges" :key="badge.text" class="gl-p-1">
<gl-badge size="sm" :variant="badge.variant"> <gl-badge size="sm" :variant="badge.variant">
{{ badge.text }} {{ badge.text }}
......
...@@ -38,8 +38,8 @@ export const FIELDS = [ ...@@ -38,8 +38,8 @@ export const FIELDS = [
{ {
key: 'maxRole', key: 'maxRole',
label: __('Max role'), label: __('Max role'),
thClass: 'col-meta', thClass: 'col-max-role',
tdClass: 'col-meta', tdClass: 'col-max-role',
}, },
{ {
key: 'expiration', key: 'expiration',
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { GlTable } from '@gitlab/ui'; import { GlTable, GlBadge } from '@gitlab/ui';
import { FIELDS } from '../constants'; import { FIELDS } from '../constants';
import initUserPopovers from '~/user_popovers'; import initUserPopovers from '~/user_popovers';
import MemberAvatar from './member_avatar.vue'; import MemberAvatar from './member_avatar.vue';
...@@ -9,17 +9,20 @@ import CreatedAt from './created_at.vue'; ...@@ -9,17 +9,20 @@ import CreatedAt from './created_at.vue';
import ExpiresAt from './expires_at.vue'; import ExpiresAt from './expires_at.vue';
import MemberActionButtons from './member_action_buttons.vue'; import MemberActionButtons from './member_action_buttons.vue';
import MembersTableCell from './members_table_cell.vue'; import MembersTableCell from './members_table_cell.vue';
import RoleDropdown from './role_dropdown.vue';
export default { export default {
name: 'MembersTable', name: 'MembersTable',
components: { components: {
GlTable, GlTable,
GlBadge,
MemberAvatar, MemberAvatar,
CreatedAt, CreatedAt,
ExpiresAt, ExpiresAt,
MembersTableCell, MembersTableCell,
MemberSource, MemberSource,
MemberActionButtons, MemberActionButtons,
RoleDropdown,
}, },
computed: { computed: {
...mapState(['members', 'tableFields']), ...mapState(['members', 'tableFields']),
...@@ -77,6 +80,13 @@ export default { ...@@ -77,6 +80,13 @@ export default {
<expires-at :date="expiresAt" /> <expires-at :date="expiresAt" />
</template> </template>
<template #cell(maxRole)="{ item: member }">
<members-table-cell #default="{ permissions }" :member="member">
<role-dropdown v-if="permissions.canUpdate" :member="member" />
<gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge>
</members-table-cell>
</template>
<template #cell(actions)="{ item: member }"> <template #cell(actions)="{ item: member }">
<members-table-cell #default="{ memberType, isCurrentUser, permissions }" :member="member"> <members-table-cell #default="{ memberType, isCurrentUser, permissions }" :member="member">
<member-action-buttons <member-action-buttons
......
...@@ -33,7 +33,7 @@ export default { ...@@ -33,7 +33,7 @@ export default {
return MEMBER_TYPES.user; return MEMBER_TYPES.user;
}, },
isDirectMember() { isDirectMember() {
return this.member.source?.id === this.sourceId; return this.isGroup || this.member.source?.id === this.sourceId;
}, },
isCurrentUser() { isCurrentUser() {
return this.member.user?.id === this.currentUserId; return this.member.user?.id === this.currentUserId;
...@@ -44,6 +44,9 @@ export default { ...@@ -44,6 +44,9 @@ export default {
canResend() { canResend() {
return Boolean(this.member.invite?.canResend); return Boolean(this.member.invite?.canResend);
}, },
canUpdate() {
return !this.isCurrentUser && this.isDirectMember && this.member.canUpdate;
},
}, },
render() { render() {
return this.$scopedSlots.default({ return this.$scopedSlots.default({
...@@ -53,6 +56,7 @@ export default { ...@@ -53,6 +56,7 @@ export default {
permissions: { permissions: {
canRemove: this.canRemove, canRemove: this.canRemove,
canResend: this.canResend, canResend: this.canResend,
canUpdate: this.canUpdate,
}, },
}); });
}, },
......
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
export default {
name: 'RoleDropdown',
components: {
GlDropdown,
GlDropdownItem,
},
props: {
member: {
type: Object,
required: true,
},
},
data() {
return {
isDesktop: false,
};
},
mounted() {
this.isDesktop = bp.isDesktop();
},
methods: {
handleSelect() {
// Vuex action will be called here to make API request and update `member.accessLevel`
},
},
};
</script>
<template>
<gl-dropdown
:right="!isDesktop"
:text="member.accessLevel.stringValue"
:header-text="__('Change permissions')"
>
<gl-dropdown-item
v-for="(value, name) in member.validRoles"
:key="value"
is-check-item
:is-checked="value === member.accessLevel.integerValue"
@click="handleSelect"
>
{{ name }}
</gl-dropdown-item>
</gl-dropdown>
</template>
...@@ -32,8 +32,8 @@ export default { ...@@ -32,8 +32,8 @@ export default {
uploadImageTab: null, uploadImageTab: null,
}; };
}, },
modalTitle: __('Image Details'), modalTitle: __('Image details'),
okTitle: __('Insert'), okTitle: __('Insert image'),
urlTabTitle: __('By URL'), urlTabTitle: __('By URL'),
urlLabel: __('Image URL'), urlLabel: __('Image URL'),
descriptionLabel: __('Description'), descriptionLabel: __('Description'),
......
<script> <script>
import { isString } from 'lodash'; import { isString } from 'lodash';
import { import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui';
GlDeprecatedDropdown,
GlDeprecatedDropdownDivider,
GlDeprecatedDropdownItem,
} from '@gitlab/ui';
const isValidItem = item => const isValidItem = item =>
isString(item.eventName) && isString(item.title) && isString(item.description); isString(item.eventName) && isString(item.title) && isString(item.description);
export default { export default {
components: { components: {
GlDeprecatedDropdown, GlDropdown,
GlDeprecatedDropdownDivider, GlDropdownDivider,
GlDeprecatedDropdownItem, GlDropdownItem,
}, },
props: { props: {
...@@ -32,7 +28,7 @@ export default { ...@@ -32,7 +28,7 @@ export default {
variant: { variant: {
type: String, type: String,
required: false, required: false,
default: 'secondary', default: 'default',
}, },
}, },
...@@ -61,8 +57,8 @@ export default { ...@@ -61,8 +57,8 @@ export default {
</script> </script>
<template> <template>
<gl-deprecated-dropdown <gl-dropdown
:menu-class="`dropdown-menu-selectable ${menuClass}`" :menu-class="menuClass"
split split
:text="dropdownToggleText" :text="dropdownToggleText"
:variant="variant" :variant="variant"
...@@ -70,20 +66,20 @@ export default { ...@@ -70,20 +66,20 @@ export default {
@click="triggerEvent" @click="triggerEvent"
> >
<template v-for="(item, itemIndex) in actionItems"> <template v-for="(item, itemIndex) in actionItems">
<gl-deprecated-dropdown-item <gl-dropdown-item
:key="item.eventName" :key="item.eventName"
:active="selectedItem === item" :is-check-item="true"
active-class="is-active" :is-checked="selectedItem === item"
@click="changeSelectedItem(item)" @click="changeSelectedItem(item)"
> >
<strong>{{ item.title }}</strong> <strong>{{ item.title }}</strong>
<div>{{ item.description }}</div> <div>{{ item.description }}</div>
</gl-deprecated-dropdown-item> </gl-dropdown-item>
<gl-deprecated-dropdown-divider <gl-dropdown-divider
v-if="itemIndex < actionItems.length - 1" v-if="itemIndex < actionItems.length - 1"
:key="`${item.eventName}-divider`" :key="`${item.eventName}-divider`"
/> />
</template> </template>
</gl-deprecated-dropdown> </gl-dropdown>
</template> </template>
...@@ -59,7 +59,7 @@ export default { ...@@ -59,7 +59,7 @@ export default {
</script> </script>
<template> <template>
<label class="toggle-wrapper"> <label class="gl-mt-2">
<input v-if="name" :name="name" :value="value" type="hidden" /> <input v-if="name" :name="name" :value="value" type="hidden" />
<button <button
type="button" type="button"
......
...@@ -8,11 +8,8 @@ ...@@ -8,11 +8,8 @@
@import './pages/commits'; @import './pages/commits';
@import './pages/deploy_keys'; @import './pages/deploy_keys';
@import './pages/detail_page'; @import './pages/detail_page';
@import './pages/diff';
@import './pages/editor'; @import './pages/editor';
@import './pages/environment_logs'; @import './pages/environment_logs';
@import './pages/error_list';
@import './pages/error_tracking_list';
@import './pages/events'; @import './pages/events';
@import './pages/experience_level'; @import './pages/experience_level';
@import './pages/experimental_separate_sign_up'; @import './pages/experimental_separate_sign_up';
......
...@@ -70,3 +70,4 @@ ...@@ -70,3 +70,4 @@
@import 'framework/spinner'; @import 'framework/spinner';
@import 'framework/card'; @import 'framework/card';
@import 'framework/editor-lite'; @import 'framework/editor-lite';
@import 'framework/diffs';
...@@ -267,6 +267,7 @@ ...@@ -267,6 +267,7 @@
} }
} }
} }
//.view.swipe //.view.swipe
.view.onion-skin { .view.onion-skin {
.onion-skin-frame { .onion-skin-frame {
...@@ -335,6 +336,7 @@ ...@@ -335,6 +336,7 @@
} }
} }
} }
//.view.onion-skin //.view.onion-skin
} }
...@@ -961,15 +963,13 @@ table.code { ...@@ -961,15 +963,13 @@ table.code {
.frame.click-to-comment, .frame.click-to-comment,
.btn-transparent.image-diff-overlay-add-comment { .btn-transparent.image-diff-overlay-add-comment {
position: relative; position: relative;
cursor: image-url('illustrations/image_comment_light_cursor.svg') cursor: image-url('illustrations/image_comment_light_cursor.svg') $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
$image-comment-cursor-left-offset $image-comment-cursor-top-offset,
auto; auto;
// Retina cursor // Retina cursor
// scss-lint:disable DuplicateProperty // scss-lint:disable DuplicateProperty
cursor: image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x, cursor: image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x,
image-url('illustrations/image_comment_light_cursor@2x.svg') 2x) image-url('illustrations/image_comment_light_cursor@2x.svg') 2x) $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
$image-comment-cursor-left-offset $image-comment-cursor-top-offset,
auto; auto;
.comment-indicator { .comment-indicator {
...@@ -1078,85 +1078,6 @@ table.code { ...@@ -1078,85 +1078,6 @@ table.code {
position: relative; position: relative;
} }
.diff-tree-list {
position: -webkit-sticky;
position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
top: $top-pos;
max-height: calc(100vh - #{$top-pos});
z-index: 202;
.with-system-header & {
top: $top-pos + $system-header-height;
}
.with-system-header.with-performance-bar & {
top: $top-pos + $system-header-height + $performance-bar-height;
}
.with-performance-bar & {
$performance-bar-top-pos: $performance-bar-height + $top-pos;
top: $performance-bar-top-pos;
max-height: calc(100vh - #{$performance-bar-top-pos});
}
.drag-handle {
bottom: 16px;
transform: translateX(10px);
}
}
.diff-files-holder {
flex: 1;
min-width: 0;
z-index: 201;
}
.compare-versions-container {
min-width: 0;
}
.tree-list-holder {
height: 100%;
.file-row {
margin-left: 0;
margin-right: 0;
}
}
.tree-list-scroll {
max-height: 100%;
padding-bottom: $grid-size;
overflow-y: scroll;
overflow-x: auto;
}
.tree-list-search {
flex: 0 0 34px;
.form-control {
padding-left: 30px;
}
}
.tree-list-icon {
top: 50%;
left: 10px;
transform: translateY(-50%);
&,
svg {
fill: $gl-text-color-tertiary;
}
}
.tree-list-clear-icon {
right: 10px;
left: auto;
line-height: 0;
}
.discussion-collapsible { .discussion-collapsible {
margin: 0 $gl-padding $gl-padding 71px; margin: 0 $gl-padding $gl-padding 71px;
...@@ -1172,30 +1093,6 @@ table.code { ...@@ -1172,30 +1093,6 @@ table.code {
} }
} }
@media (max-width: map-get($grid-breakpoints, md)-1) {
.diffs .files {
@include fixed-width-container;
flex-direction: column;
.diff-tree-list {
position: relative;
top: 0;
// !important is required to override inline styles of resizable sidebar
width: 100% !important;
}
.tree-list-holder {
max-height: calc(50px + 50vh);
padding-right: 0;
}
}
.discussion-collapsible {
margin: $gl-padding;
margin-top: 0;
}
}
.image-diff-overlay, .image-diff-overlay,
.image-diff-overlay-add-comment { .image-diff-overlay-add-comment {
top: 0; top: 0;
...@@ -1218,3 +1115,15 @@ table.code { ...@@ -1218,3 +1115,15 @@ table.code {
display: none; display: none;
} }
} }
@media (max-width: map-get($grid-breakpoints, md)-1) {
.diffs .files {
@include fixed-width-container;
flex-direction: column;
}
.discussion-collapsible {
margin: $gl-padding;
margin-top: 0;
}
}
@import 'page_bundles/mixins_and_variables_and_functions';
.error-list { .error-list {
.dropdown {
min-width: auto;
}
.sort-control { .sort-control {
.btn { .btn {
padding-right: 2rem; padding-right: 2rem;
...@@ -17,7 +23,7 @@ ...@@ -17,7 +23,7 @@
min-height: 68px; min-height: 68px;
&:last-child { &:last-child {
background-color: $gray-10; background-color: var(--gray-10, $gray-10);
&::before { &::before {
content: none !important; content: none !important;
......
...@@ -226,6 +226,14 @@ $colors: ( ...@@ -226,6 +226,14 @@ $colors: (
.solarized-dark { .solarized-dark {
@include color-scheme('solarized-dark'); } @include color-scheme('solarized-dark'); }
.none {
.line_content.header {
button {
color: $gray-900;
}
}
}
.diff-wrap-lines .line_content { .diff-wrap-lines .line_content {
white-space: normal; white-space: normal;
min-height: 19px; min-height: 19px;
......
@import 'mixins_and_variables_and_functions';
.compare-versions-container {
min-width: 0;
}
.diff-files-holder {
flex: 1;
min-width: 0;
z-index: 201;
}
.diff-tree-list {
position: -webkit-sticky;
position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px;
top: $top-pos;
max-height: calc(100vh - #{$top-pos});
z-index: 202;
.with-system-header & {
top: $top-pos + $system-header-height;
}
.with-system-header.with-performance-bar & {
top: $top-pos + $system-header-height + $performance-bar-height;
}
.with-performance-bar & {
$performance-bar-top-pos: $performance-bar-height + $top-pos;
top: $performance-bar-top-pos;
max-height: calc(100vh - #{$performance-bar-top-pos});
}
.drag-handle {
bottom: 16px;
transform: translateX(10px);
}
}
.tree-list-holder {
height: 100%;
.file-row {
margin-left: 0;
margin-right: 0;
}
}
.tree-list-scroll {
max-height: 100%;
padding-bottom: $grid-size;
overflow-y: scroll;
overflow-x: auto;
}
.tree-list-search {
flex: 0 0 34px;
.form-control {
padding-left: 30px;
}
}
.tree-list-icon {
top: 50%;
left: 10px;
transform: translateY(-50%);
&,
svg {
fill: var(--gray-400, $gray-400);
}
}
.tree-list-clear-icon {
right: 10px;
left: auto;
line-height: 0;
}
@media (max-width: map-get($grid-breakpoints, md)-1) {
.diffs .files {
.diff-tree-list {
position: relative;
top: 0;
// !important is required to override inline styles of resizable sidebar
width: 100% !important;
}
.tree-list-holder {
max-height: calc(50px + 50vh);
padding-right: 0;
}
}
}
.error-list {
.dropdown {
min-width: auto;
}
}
...@@ -95,6 +95,78 @@ ...@@ -95,6 +95,78 @@
} }
} }
.group-home-panel {
margin-top: $gl-padding;
margin-bottom: $gl-padding;
.home-panel-avatar {
width: $home-panel-title-row-height;
height: $home-panel-title-row-height;
flex-shrink: 0;
flex-basis: $home-panel-title-row-height;
}
.home-panel-title {
font-size: 20px;
line-height: $gl-line-height-24;
font-weight: bold;
.icon {
vertical-align: -1px;
}
.home-panel-topic-list {
font-size: $gl-font-size;
font-weight: $gl-font-weight-normal;
.icon {
position: relative;
top: 3px;
margin-right: $gl-padding-4;
}
}
}
.home-panel-title-row {
@include media-breakpoint-down(sm) {
.home-panel-avatar {
width: $home-panel-avatar-mobile-size;
height: $home-panel-avatar-mobile-size;
flex-basis: $home-panel-avatar-mobile-size;
.avatar {
font-size: 20px;
line-height: 46px;
}
}
.home-panel-title {
margin-top: 4px;
margin-bottom: 2px;
font-size: $gl-font-size;
line-height: $gl-font-size-large;
}
.home-panel-topic-list,
.home-panel-metadata {
font-size: $gl-font-size-small;
}
}
}
.home-panel-metadata {
font-weight: normal;
font-size: 14px;
line-height: $gl-btn-line-height;
}
.home-panel-description {
@include media-breakpoint-up(md) {
font-size: $gl-font-size-large;
}
}
}
.home-panel-buttons { .home-panel-buttons {
.home-panel-action-button { .home-panel-action-button {
vertical-align: top; vertical-align: top;
......
...@@ -247,6 +247,7 @@ ...@@ -247,6 +247,7 @@
.label-badge { .label-badge {
color: $gray-900; color: $gray-900;
display: inline-block;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
padding: $gl-padding-4 $gl-padding-8; padding: $gl-padding-4 $gl-padding-8;
border-radius: $border-radius-default; border-radius: $border-radius-default;
......
...@@ -216,6 +216,10 @@ ...@@ -216,6 +216,10 @@
width: px-to-rem(150px); width: px-to-rem(150px);
} }
.col-max-role {
width: px-to-rem(175px);
}
.col-expiration { .col-expiration {
width: px-to-rem(200px); width: px-to-rem(200px);
} }
......
.alert_holder {
margin: -16px;
.alert-link {
font-weight: $gl-font-weight-normal;
}
}
.new_project, .new_project,
.edit-project, .edit-project,
.import-project { .import-project {
...@@ -67,38 +59,7 @@ ...@@ -67,38 +59,7 @@
} }
} }
.classification-label { // INFO Scoped to project_feature_setting and settings_panel components in app/assets/javascripts/pages/projects/shared/permissions/components
background-color: $red-500;
}
.toggle-wrapper {
margin-top: 5px;
}
.project-feature-row > .toggle-wrapper {
margin: 10px 0;
}
.project-visibility-setting,
.project-feature-settings {
border: 1px solid $border-color;
padding: 10px 32px;
@include media-breakpoint-down(xs) {
padding: 10px 20px;
}
}
.project-visibility-setting .request-access {
line-height: 2;
}
.project-feature-settings {
background: $gray-lighter;
border-top: 0;
margin-bottom: 16px;
}
.project-repo-select { .project-repo-select {
transition: background 2s ease-out; transition: background 2s ease-out;
...@@ -113,63 +74,31 @@ ...@@ -113,63 +74,31 @@
} }
} }
// INFO Scoped to project_feature_setting and settings_panel components in app/assets/javascripts/pages/projects/shared/permissions/components
.project-feature-controls { .project-feature-controls {
display: flex;
align-items: center;
margin: $gl-padding-8 0;
max-width: 432px; max-width: 432px;
.toggle-wrapper {
flex: 0;
margin-right: 10px;
}
.select-wrapper {
flex: 1;
}
} }
// INFO Scoped to settings_panel component in app/assets/javascripts/pages/projects/shared/permissions/components
.project-feature-setting-group { .project-feature-setting-group {
padding-left: 32px;
.project-feature-controls { .project-feature-controls {
max-width: 400px; max-width: 400px;
} }
@include media-breakpoint-down(xs) {
padding-left: 20px;
}
} }
.group-home-panel,
.project-home-panel { .project-home-panel {
margin-top: $gl-padding;
margin-bottom: $gl-padding;
.home-panel-avatar { .home-panel-avatar {
width: $home-panel-title-row-height;
height: $home-panel-title-row-height;
flex-shrink: 0;
flex-basis: $home-panel-title-row-height; flex-basis: $home-panel-title-row-height;
} }
.home-panel-title { .home-panel-title {
font-size: 20px;
line-height: $gl-line-height-24;
font-weight: bold;
.icon { .icon {
vertical-align: -1px; vertical-align: -1px;
} }
.home-panel-topic-list { .home-panel-topic-list {
font-size: $gl-font-size;
font-weight: $gl-font-weight-normal;
.icon { .icon {
position: relative;
top: 3px; top: 3px;
margin-right: $gl-padding-4;
} }
} }
} }
...@@ -201,24 +130,6 @@ ...@@ -201,24 +130,6 @@
} }
} }
.home-panel-metadata {
font-weight: normal;
font-size: 14px;
line-height: $gl-btn-line-height;
.home-panel-license {
.btn {
line-height: 0;
border-width: 0;
}
}
.access-request-link {
padding-left: $gl-padding-8;
border-left: 1px solid $gl-text-color-secondary;
}
}
.home-panel-description { .home-panel-description {
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
font-size: $gl-font-size-large; font-size: $gl-font-size-large;
......
...@@ -3,7 +3,7 @@ class Admin::InstanceReviewController < Admin::ApplicationController ...@@ -3,7 +3,7 @@ class Admin::InstanceReviewController < Admin::ApplicationController
feature_category :instance_statistics feature_category :instance_statistics
def index def index
redirect_to("#{EE::SUBSCRIPTIONS_URL}/instance_review?#{instance_review_params}") redirect_to("#{::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL}/instance_review?#{instance_review_params}")
end end
def instance_review_params def instance_review_params
......
...@@ -80,18 +80,17 @@ class HelpController < ApplicationController ...@@ -80,18 +80,17 @@ class HelpController < ApplicationController
def documentation_url def documentation_url
return unless documentation_base_url return unless documentation_base_url
@documentation_url ||= [ @documentation_url ||= Gitlab::Utils.append_path(documentation_base_url, documentation_file_path)
documentation_base_url.chomp('/'),
version_segment,
'ee',
"#{@path}.html"
].compact.join('/')
end end
def documentation_base_url def documentation_base_url
@documentation_base_url ||= Gitlab::CurrentSettings.current_application_settings.help_page_documentation_base_url.presence @documentation_base_url ||= Gitlab::CurrentSettings.current_application_settings.help_page_documentation_base_url.presence
end end
def documentation_file_path
@documentation_file_path ||= [version_segment, 'ee', "#{@path}.html"].compact.join('/')
end
def version_segment def version_segment
return if Gitlab.pre_release? return if Gitlab.pre_release?
......
...@@ -10,5 +10,6 @@ class Projects::AlertManagementController < Projects::ApplicationController ...@@ -10,5 +10,6 @@ class Projects::AlertManagementController < Projects::ApplicationController
def details def details
@alert_id = params[:id] @alert_id = params[:id]
push_frontend_feature_flag(:expose_environment_path_in_alert_details, @project)
end end
end end
...@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload include SendFileUpload
include ContinueParams include ContinueParams
before_action :build, except: [:index] before_action :find_job_as_build, except: [:index, :play]
before_action :find_job_as_processable, only: [:play]
before_action :authorize_read_build! before_action :authorize_read_build!
before_action :authorize_update_build!, before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :erase] except: [:index, :show, :status, :raw, :trace, :erase]
...@@ -44,10 +45,10 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -44,10 +45,10 @@ class Projects::JobsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def trace def trace
build.trace.read do |stream| @build.trace.read do |stream|
respond_to do |format| respond_to do |format|
format.json do format.json do
build.trace.being_watched! @build.trace.being_watched!
build_trace = Ci::BuildTrace.new( build_trace = Ci::BuildTrace.new(
build: @build, build: @build,
...@@ -72,8 +73,13 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -72,8 +73,13 @@ class Projects::JobsController < Projects::ApplicationController
def play def play
return respond_422 unless @build.playable? return respond_422 unless @build.playable?
build = @build.play(current_user, play_params[:job_variables_attributes]) job = @build.play(current_user, play_params[:job_variables_attributes])
redirect_to build_path(build)
if job.is_a?(Ci::Bridge)
redirect_to pipeline_path(job.pipeline)
else
redirect_to build_path(job)
end
end end
def cancel def cancel
...@@ -117,7 +123,7 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -117,7 +123,7 @@ class Projects::JobsController < Projects::ApplicationController
send_params: raw_send_params, send_params: raw_send_params,
redirect_params: raw_redirect_params) redirect_params: raw_redirect_params)
else else
build.trace.read do |stream| @build.trace.read do |stream|
if stream.file? if stream.file?
workhorse_set_content_type! workhorse_set_content_type!
send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline' send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
...@@ -149,19 +155,19 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -149,19 +155,19 @@ class Projects::JobsController < Projects::ApplicationController
private private
def authorize_update_build! def authorize_update_build!
return access_denied! unless can?(current_user, :update_build, build) return access_denied! unless can?(current_user, :update_build, @build)
end end
def authorize_erase_build! def authorize_erase_build!
return access_denied! unless can?(current_user, :erase_build, build) return access_denied! unless can?(current_user, :erase_build, @build)
end end
def authorize_use_build_terminal! def authorize_use_build_terminal!
return access_denied! unless can?(current_user, :create_build_terminal, build) return access_denied! unless can?(current_user, :create_build_terminal, @build)
end end
def authorize_create_proxy_build! def authorize_create_proxy_build!
return access_denied! unless can?(current_user, :create_build_service_proxy, build) return access_denied! unless can?(current_user, :create_build_service_proxy, @build)
end end
def verify_api_request! def verify_api_request!
...@@ -186,14 +192,22 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -186,14 +192,22 @@ class Projects::JobsController < Projects::ApplicationController
end end
def trace_artifact_file def trace_artifact_file
@trace_artifact_file ||= build.job_artifacts_trace&.file @trace_artifact_file ||= @build.job_artifacts_trace&.file
end end
def build def find_job_as_build
@build ||= project.builds.find(params[:id]) @build = project.builds.find(params[:id])
.present(current_user: current_user) .present(current_user: current_user)
end end
def find_job_as_processable
if ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
@build = project.processables.find(params[:id])
else
find_job_as_build
end
end
def build_path(build) def build_path(build)
project_job_path(build.project, build) project_job_path(build.project, build)
end end
...@@ -208,10 +222,10 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -208,10 +222,10 @@ class Projects::JobsController < Projects::ApplicationController
end end
def build_service_specification def build_service_specification
build.service_specification(service: params['service'], @build.service_specification(service: params['service'],
port: params['port'], port: params['port'],
path: params['path'], path: params['path'],
subprotocols: proxy_subprotocol) subprotocols: proxy_subprotocol)
end end
def proxy_subprotocol def proxy_subprotocol
......
query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
__typename
repository { repository {
__typename
tree(path: $path, ref: $ref) { tree(path: $path, ref: $ref) {
__typename
lastCommit { lastCommit {
__typename
sha sha
title title
titleHtml titleHtml
...@@ -13,15 +17,20 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { ...@@ -13,15 +17,20 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
authorName authorName
authorGravatar authorGravatar
author { author {
__typename
name name
avatarUrl avatarUrl
webPath webPath
} }
signatureHtml signatureHtml
pipelines(ref: $ref, first: 1) { pipelines(ref: $ref, first: 1) {
__typename
edges { edges {
__typename
node { node {
__typename
detailedStatus { detailedStatus {
__typename
detailsPath detailsPath
icon icon
tooltip tooltip
......
...@@ -68,6 +68,11 @@ module Types ...@@ -68,6 +68,11 @@ module Types
null: true, null: true,
description: 'Timestamp the alert ended' description: 'Timestamp the alert ended'
field :environment,
Types::EnvironmentType,
null: true,
description: 'Environment for the alert'
field :event_count, field :event_count,
GraphQL::INT_TYPE, GraphQL::INT_TYPE,
null: true, null: true,
......
...@@ -5,6 +5,8 @@ module Types ...@@ -5,6 +5,8 @@ module Types
graphql_name 'Environment' graphql_name 'Environment'
description 'Describes where code is deployed for a project' description 'Describes where code is deployed for a project'
present_using ::EnvironmentPresenter
authorize :read_environment authorize :read_environment
field :name, GraphQL::STRING_TYPE, null: false, field :name, GraphQL::STRING_TYPE, null: false,
...@@ -16,6 +18,10 @@ module Types ...@@ -16,6 +18,10 @@ module Types
field :state, GraphQL::STRING_TYPE, null: false, field :state, GraphQL::STRING_TYPE, null: false,
description: 'State of the environment, for example: available/stopped' description: 'State of the environment, for example: available/stopped'
field :path, GraphQL::STRING_TYPE, null: true,
description: 'The path to the environment. Will always return null ' \
'if `expose_environment_path_in_alert_details` feature flag is disabled'
field :metrics_dashboard, Types::Metrics::DashboardType, null: true, field :metrics_dashboard, Types::Metrics::DashboardType, null: true,
description: 'Metrics dashboard schema for the environment', description: 'Metrics dashboard schema for the environment',
resolver: Resolvers::Metrics::DashboardResolver resolver: Resolvers::Metrics::DashboardResolver
...@@ -23,6 +29,6 @@ module Types ...@@ -23,6 +29,6 @@ module Types
field :latest_opened_most_severe_alert, field :latest_opened_most_severe_alert,
Types::AlertManagement::AlertType, Types::AlertManagement::AlertType,
null: true, null: true,
description: 'The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.' description: 'The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned'
end end
end end
...@@ -212,6 +212,10 @@ module ApplicationHelper ...@@ -212,6 +212,10 @@ module ApplicationHelper
Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/' Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end end
def instance_review_permitted?
::Gitlab::CurrentSettings.instance_review_permitted? && current_user&.admin?
end
def static_objects_external_storage_enabled? def static_objects_external_storage_enabled?
Gitlab::CurrentSettings.static_objects_external_storage_enabled? Gitlab::CurrentSettings.static_objects_external_storage_enabled?
end end
......
...@@ -34,6 +34,10 @@ module PackagesHelper ...@@ -34,6 +34,10 @@ module PackagesHelper
expose_url(api_v4_group___packages_composer_packages_path(id: group_id, format: '.json')) expose_url(api_v4_group___packages_composer_packages_path(id: group_id, format: '.json'))
end end
def composer_config_repository_name(group_id)
"#{Gitlab.config.gitlab.host}/#{group_id}"
end
def packages_list_data(type, resource) def packages_list_data(type, resource)
{ {
resource_id: resource.id, resource_id: resource.id,
......
# frozen_string_literal: true
module StartupjsHelper
def page_startup_graphql_calls
@graphql_startup_calls
end
def add_page_startup_graphql_call(query, variables = {})
@graphql_startup_calls ||= []
file_location = File.join(Rails.root, "app/graphql/queries/#{query}.query.graphql")
return unless File.exist?(file_location)
query_str = File.read(file_location)
@graphql_startup_calls << { query: query_str, variables: variables }
end
end
...@@ -11,6 +11,7 @@ class ApplicationSetting < ApplicationRecord ...@@ -11,6 +11,7 @@ class ApplicationSetting < ApplicationRecord
ignore_column :instance_statistics_visibility_private, remove_with: '13.6', remove_after: '2020-10-22' ignore_column :instance_statistics_visibility_private, remove_with: '13.6', remove_after: '2020-10-22'
ignore_column :snowplow_iglu_registry_url, remove_with: '13.6', remove_after: '2020-11-22' ignore_column :snowplow_iglu_registry_url, remove_with: '13.6', remove_after: '2020-11-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana' 'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
...@@ -437,6 +438,14 @@ class ApplicationSetting < ApplicationRecord ...@@ -437,6 +438,14 @@ class ApplicationSetting < ApplicationRecord
!!(sourcegraph_url =~ /\Ahttps:\/\/(www\.)?sourcegraph\.com/) !!(sourcegraph_url =~ /\Ahttps:\/\/(www\.)?sourcegraph\.com/)
end end
def instance_review_permitted?
users_count = Rails.cache.fetch('limited_users_count', expires_in: 1.day) do
::User.limit(INSTANCE_REVIEW_MIN_USERS + 1).count(:all)
end
users_count >= INSTANCE_REVIEW_MIN_USERS
end
def self.create_from_defaults def self.create_from_defaults
check_schema! check_schema!
......
...@@ -27,7 +27,7 @@ module Ci ...@@ -27,7 +27,7 @@ module Ci
# rubocop:enable Cop/ActiveRecordSerialize # rubocop:enable Cop/ActiveRecordSerialize
state_machine :status do state_machine :status do
after_transition created: :pending do |bridge| after_transition [:created, :manual] => :pending do |bridge|
next unless bridge.downstream_project next unless bridge.downstream_project
bridge.run_after_commit do bridge.run_after_commit do
...@@ -46,6 +46,10 @@ module Ci ...@@ -46,6 +46,10 @@ module Ci
event :scheduled do event :scheduled do
transition all => :scheduled transition all => :scheduled
end end
event :actionize do
transition created: :manual
end
end end
def self.retry(bridge, current_user) def self.retry(bridge, current_user)
...@@ -126,9 +130,27 @@ module Ci ...@@ -126,9 +130,27 @@ module Ci
false false
end end
def playable?
return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
action? && !archived? && manual?
end
def action? def action?
false return false unless ::Gitlab::Ci::Features.manual_bridges_enabled?(project)
%w[manual].include?(self.when)
end
# rubocop: disable CodeReuse/ServiceClass
# We don't need it but we are taking `job_variables_attributes` parameter
# to make it consistent with `Ci::Build#play` method.
def play(current_user, job_variables_attributes = nil)
Ci::PlayBridgeService
.new(project, current_user)
.execute(self)
end end
# rubocop: enable CodeReuse/ServiceClass
def artifacts? def artifacts?
false false
...@@ -185,6 +207,10 @@ module Ci ...@@ -185,6 +207,10 @@ module Ci
[] []
end end
def target_revision_ref
downstream_pipeline_params.dig(:target_revision, :ref)
end
private private
def cross_project_params def cross_project_params
......
...@@ -74,8 +74,8 @@ class Commit ...@@ -74,8 +74,8 @@ class Commit
sha[0..MIN_SHA_LENGTH] sha[0..MIN_SHA_LENGTH]
end end
def diff_safe_lines def diff_safe_lines(project: nil)
Gitlab::Git::DiffCollection.default_limits[:max_lines] Gitlab::Git::DiffCollection.default_limits(project: project)[:max_lines]
end end
def diff_hard_limit_files(project: nil) def diff_hard_limit_files(project: nil)
......
...@@ -4,6 +4,7 @@ class Environment < ApplicationRecord ...@@ -4,6 +4,7 @@ class Environment < ApplicationRecord
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include ReactiveCaching include ReactiveCaching
include FastDestroyAll::Helpers include FastDestroyAll::Helpers
include Presentable
self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds self.reactive_cache_lifetime = 55.seconds
......
...@@ -51,3 +51,5 @@ module IncidentManagement ...@@ -51,3 +51,5 @@ module IncidentManagement
end end
end end
end end
IncidentManagement::ProjectIncidentManagementSetting.prepend_if_ee('EE::IncidentManagement::ProjectIncidentManagementSetting')
...@@ -298,6 +298,7 @@ class Project < ApplicationRecord ...@@ -298,6 +298,7 @@ class Project < ApplicationRecord
# bulk that doesn't involve loading the rows into memory. As a result we're # bulk that doesn't involve loading the rows into memory. As a result we're
# still using `dependent: :destroy` here. # still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :processables, class_name: 'Ci::Processable', inverse_of: :project
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName' has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project
......
...@@ -37,7 +37,11 @@ module Terraform ...@@ -37,7 +37,11 @@ module Terraform
end end
def latest_file def latest_file
versioning_enabled ? latest_version&.file : file if versioning_enabled?
latest_version&.file
else
latest_version&.file || file
end
end end
def locked? def locked?
...@@ -46,13 +50,56 @@ module Terraform ...@@ -46,13 +50,56 @@ module Terraform
def update_file!(data, version:) def update_file!(data, version:)
if versioning_enabled? if versioning_enabled?
new_version = versions.build(version: version) create_new_version!(data: data, version: version)
new_version.assign_attributes(created_by_user: locked_by_user, file: data) elsif latest_version.present?
new_version.save! migrate_legacy_version!(data: data, version: version)
else else
self.file = data self.file = data
save! save!
end end
end end
private
##
# If a Terraform state was created before versioning support was
# introduced, it will have a single version record whose file
# uses a legacy naming scheme in object storage. To update
# these states and versions to use the new behaviour, we must do
# the following when creating the next version:
#
# * Read the current, non-versioned file from the old location.
# * Update the :versioning_enabled flag, which determines the
# naming scheme
# * Resave the existing file with the updated name and location,
# using a version number one prior to the new version
# * Create the new version as normal
#
# This migration only needs to happen once for each state, from
# then on the state will behave as if it was always versioned.
#
# The code can be removed in the next major version (14.0), after
# which any states that haven't been migrated will need to be
# recreated: https://gitlab.com/gitlab-org/gitlab/-/issues/258960
def migrate_legacy_version!(data:, version:)
current_file = latest_version.file.read
current_version = parse_serial(current_file) || version - 1
update!(versioning_enabled: true)
reload_latest_version.update!(version: current_version, file: CarrierWaveStringFile.new(current_file))
create_new_version!(data: data, version: version)
end
def create_new_version!(data:, version:)
new_version = versions.build(version: version, created_by_user: locked_by_user)
new_version.assign_attributes(file: data)
new_version.save!
end
def parse_serial(file)
Gitlab::Json.parse(file)["serial"]
rescue JSON::ParserError
end
end end
end end
# frozen_string_literal: true
module Ci
class BridgePolicy < CommitStatusPolicy
condition(:can_update_downstream_branch) do
::Gitlab::UserAccess.new(@user, container: @subject.downstream_project)
.can_update_branch?(@subject.target_revision_ref)
end
rule { can_update_downstream_branch }.enable :play_job
end
end
...@@ -60,6 +60,8 @@ module Ci ...@@ -60,6 +60,8 @@ module Ci
rule { can?(:update_build) & terminal }.enable :create_build_terminal rule { can?(:update_build) & terminal }.enable :create_build_terminal
rule { can?(:update_build) }.enable :play_job
rule { is_web_ide_terminal & can?(:create_web_ide_terminal) & (admin | owner_of_job) }.policy do rule { is_web_ide_terminal & can?(:create_web_ide_terminal) & (admin | owner_of_job) }.policy do
enable :read_web_ide_terminal enable :read_web_ide_terminal
enable :update_web_ide_terminal enable :update_web_ide_terminal
......
# frozen_string_literal: true
class EnvironmentPresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
presents :environment
def path
if Feature.enabled?(:expose_environment_path_in_alert_details, project)
project_environment_path(project, self)
end
end
end
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class LabelPresenter < Gitlab::View::Presenter::Delegated class LabelPresenter < Gitlab::View::Presenter::Delegated
presents :label presents :label
delegate :name, :full_name, to: :label_subject, prefix: :subject
def edit_path def edit_path
case label case label
...@@ -39,8 +40,8 @@ class LabelPresenter < Gitlab::View::Presenter::Delegated ...@@ -39,8 +40,8 @@ class LabelPresenter < Gitlab::View::Presenter::Delegated
label.is_a?(ProjectLabel) label.is_a?(ProjectLabel)
end end
def subject_name def label_subject
label.subject.name @label_subject ||= label.subject
end end
private private
......
# frozen_string_literal: true
module Ci
class PlayBridgeService < ::BaseService
def execute(bridge)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, bridge)
bridge.tap do |bridge|
bridge.user = current_user
bridge.enqueue!
end
end
end
end
...@@ -3,9 +3,7 @@ ...@@ -3,9 +3,7 @@
module Ci module Ci
class PlayBuildService < ::BaseService class PlayBuildService < ::BaseService
def execute(build, job_variables_attributes = nil) def execute(build, job_variables_attributes = nil)
unless can?(current_user, :update_build, build) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, build)
raise Gitlab::Access::AccessDeniedError
end
# Try to enqueue the build, otherwise create a duplicate. # Try to enqueue the build, otherwise create a duplicate.
# #
......
...@@ -9,12 +9,12 @@ module Ci ...@@ -9,12 +9,12 @@ module Ci
end end
def execute(stage) def execute(stage)
stage.builds.manual.each do |build| stage.processables.manual.each do |processable|
next unless build.playable? next unless processable.playable?
build.play(current_user) processable.play(current_user)
rescue Gitlab::Access::AccessDeniedError rescue Gitlab::Access::AccessDeniedError
logger.error(message: 'Unable to play manual action', build_id: build.id) logger.error(message: 'Unable to play manual action', processable_id: processable.id)
end end
end end
......
...@@ -7,7 +7,7 @@ module Members ...@@ -7,7 +7,7 @@ module Members
def execute(source) def execute(source)
return error(s_('AddMember|No users specified.')) if params[:user_ids].blank? return error(s_('AddMember|No users specified.')) if params[:user_ids].blank?
user_ids = params[:user_ids].split(',').uniq user_ids = params[:user_ids].split(',').uniq.flatten
return error(s_("AddMember|Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if return error(s_("AddMember|Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
user_limit && user_ids.size > user_limit user_limit && user_ids.size > user_limit
......
...@@ -7,6 +7,8 @@ module Users ...@@ -7,6 +7,8 @@ module Users
end end
def execute(user) def execute(user)
return error('An internal user cannot be blocked', 403) if user.internal?
if user.block if user.block
after_block_hook(user) after_block_hook(user)
success success
......
...@@ -2,12 +2,22 @@ ...@@ -2,12 +2,22 @@
module Terraform module Terraform
class VersionedStateUploader < StateUploader class VersionedStateUploader < StateUploader
delegate :terraform_state, to: :model
def filename def filename
"#{model.version}.tfstate" if terraform_state.versioning_enabled?
"#{model.version}.tfstate"
else
"#{model.uuid}.tfstate"
end
end end
def store_dir def store_dir
Gitlab::HashedPath.new(model.uuid, root_hash: project_id) if terraform_state.versioning_enabled?
Gitlab::HashedPath.new(model.uuid, root_hash: project_id)
else
project_id.to_s
end
end end
end end
end end
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
- @metric.cards.each do |card| - @metric.cards.each do |card|
= render 'card', card: card = render 'card', card: card
.devops-steps.d-none.d-lg-block.d-xl-block .devops-steps.d-none.d-lg-block
- @metric.idea_to_production_steps.each_with_index do |step, index| - @metric.idea_to_production_steps.each_with_index do |step, index|
.devops-step{ class: "devops-#{score_level(step.percentage_score)}-score" } .devops-step{ class: "devops-#{score_level(step.percentage_score)}-score" }
= custom_icon("i2p_step_#{index + 1}") = custom_icon("i2p_step_#{index + 1}")
......
...@@ -182,7 +182,7 @@ ...@@ -182,7 +182,7 @@
%li Access Git repositories %li Access Git repositories
%br %br
= link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' } = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
- else - elsif !@user.internal?
.card.border-warning .card.border-warning
.card-header.bg-warning.text-white .card-header.bg-warning.text-white
Block this user Block this user
......
...@@ -24,5 +24,14 @@ ...@@ -24,5 +24,14 @@
%td= subscription.created_at %td= subscription.created_at
%td= link_to 'Remove', jira_connect_subscription_path(subscription), class: 'remove-subscription' %td= link_to 'Remove', jira_connect_subscription_path(subscription), class: 'remove-subscription'
%p
%strong Browser limitations:
Adding a namespace currently works only in browsers that allow cross site cookies. Please make sure to use
%a{ href: 'https://www.mozilla.org/en-US/firefox/', target: '_blank', rel: 'noopener noreferrer' } Firefox
or
%a{ href: 'https://www.google.com/chrome/index.html', target: '_blank', rel: 'noopener noreferrer' } Google Chrome
or enable cross-site cookies in your browser when adding a namespace.
%a{ href: 'https://gitlab.com/gitlab-org/gitlab/-/issues/263509', target: '_blank', rel: 'noopener noreferrer' } Learn more
= page_specific_javascript_tag('jira_connect.js') = page_specific_javascript_tag('jira_connect.js')
- add_page_specific_style 'page_bundles/jira_connect' - add_page_specific_style 'page_bundles/jira_connect'
...@@ -69,7 +69,8 @@ ...@@ -69,7 +69,8 @@
= Gon::Base.render_data(nonce: content_security_policy_nonce) = Gon::Base.render_data(nonce: content_security_policy_nonce)
= javascript_include_tag locale_path unless I18n.locale == :en = javascript_include_tag locale_path unless I18n.locale == :en
= webpack_bundle_tag "sentry" if Gitlab.config.sentry.enabled -# Temporarily commented out to investigate performance: https://gitlab.com/gitlab-org/gitlab/-/issues/251179
-# = webpack_bundle_tag "sentry" if Gitlab.config.sentry.enabled
= webpack_bundle_tag 'performance_bar' if performance_bar_enabled? = webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
= yield :page_specific_javascripts = yield :page_specific_javascripts
......
- return unless page_startup_api_calls.present? - return unless page_startup_api_calls.present? || page_startup_graphql_calls.present?
= javascript_tag nonce: true do = javascript_tag nonce: true do
:plain :plain
var gl = window.gl || {}; var gl = window.gl || {};
gl.startup_calls = #{page_startup_api_calls.to_json}; gl.startup_calls = #{page_startup_api_calls.to_json};
gl.startup_graphql_calls = #{page_startup_graphql_calls.to_json};
if (gl.startup_calls && window.fetch) { if (gl.startup_calls && window.fetch) {
Object.keys(gl.startup_calls).forEach(apiCall => { Object.keys(gl.startup_calls).forEach(apiCall => {
// fetch won’t send cookies in older browsers, unless you set the credentials init option. // fetch won’t send cookies in older browsers, unless you set the credentials init option.
...@@ -14,3 +16,21 @@ ...@@ -14,3 +16,21 @@
}; };
}); });
} }
if (gl.startup_graphql_calls && window.fetch) {
const url = `#{api_graphql_url}`
const opts = {
method: "POST",
headers: { "Content-Type": "application/json", 'X-CSRF-Token': "#{form_authenticity_token}" },
};
gl.startup_graphql_calls = gl.startup_graphql_calls.map(call => ({
operationName: call.query.match(/^query (.+)\(/)[1],
fetchCall: fetch(url, {
...opts,
credentials: 'same-origin',
body: JSON.stringify(call)
})
}))
}
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile) - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
%li.d-md-none %li.d-md-none
= render 'shared/user_dropdown_contributing_link' = render 'shared/user_dropdown_contributing_link'
= render_if_exists 'shared/user_dropdown_instance_review' = render 'shared/user_dropdown_instance_review'
- if Gitlab.com_but_not_canary? - if Gitlab.com_but_not_canary?
%li.d-md-none %li.d-md-none
= link_to _("Switch to GitLab Next"), "https://next.gitlab.com/" = link_to _("Switch to GitLab Next"), "https://next.gitlab.com/"
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile) - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
%li %li
= render 'shared/user_dropdown_contributing_link' = render 'shared/user_dropdown_contributing_link'
= render_if_exists 'shared/user_dropdown_instance_review' = render 'shared/user_dropdown_instance_review'
- if Gitlab.com_but_not_canary? - if Gitlab.com_but_not_canary?
%li %li
= link_to _("Switch to GitLab Next"), "https://next.gitlab.com/" = link_to _("Switch to GitLab Next"), "https://next.gitlab.com/"
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